-
Notifications
You must be signed in to change notification settings - Fork 475
Text Fragments from highlight experimental feature #1504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| import { SelectionObserver } from "../BookReader/utils/SelectionObserver.js"; | ||
|
|
||
| export class TextSelectionManager { | ||
| hlightBarEl; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inside the constructor, we can construct a single copy of this. Then the click handler should never have to reconstruct it ; it'll just change its position, and remove/append into the document.body or instead display:none . this.highlightBar = document.createElement('br-highlight-bar'); |
||
| options = { | ||
| // Current Translation plugin implementation does not have words, will limit to one BRlineElement for now | ||
| maxProtectedWords: 200, | ||
|
|
@@ -156,17 +157,86 @@ export class TextSelectionManager { | |
| $(textLayer).off(".textSelectPluginHandler"); | ||
|
|
||
| $(textLayer).on("mousedown.textSelectPluginHandler", (event) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mousedown/mouseup appear not to fire on mobile ; not getting the menu appearing on firefox/chrome on android. Maybe we can instead use the helper SelectionObserver class? That fires a started event and a cleared event we can use to show/hide the toolbar. |
||
| if (event.which != 1) return; | ||
| this.mouseIsDown = true; | ||
| if (this.hlightBarEl) { | ||
| this.hlightBarEl.remove(); | ||
| window.getSelection().empty(); // selection is checked at mouseup, cleared here to prevent button from lingering | ||
| } | ||
| event.stopPropagation(); | ||
| }); | ||
|
|
||
| // Prevent page flip on click | ||
| $(textLayer).on('mouseup.textSelectPluginHandler', (event) => { | ||
| this.mouseIsDown = false; | ||
| event.stopPropagation(); | ||
| if (event.which != 1) return; | ||
| this.hlightBarEl?.remove(); | ||
| if (window.getSelection().toString()) { | ||
| this.highlightToolbar(event); | ||
| } else { | ||
| this.hlightBarEl?.remove(); | ||
| } | ||
| }); | ||
|
|
||
| $(document.body).on('mouseup', (_) => { | ||
| this.hlightBarEl?.remove(); | ||
| }); | ||
|
Comment on lines
+182
to
184
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll want to make sure this is only attached once, otherwise it'll cause a memory leak, with the event listener created every time there's a new text layer made. |
||
| } | ||
|
|
||
| highlightToolbar(_) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we will want this toolbar to be a lit element, eg That will let us use reactivity, and do some other things. Useful things to note for lit:
Then all the rendering logic will become const highlightBar = document.createElement('br-highlight-bar'); |
||
| const hlButton = document.createElement('button'); | ||
|
|
||
| if (this.hlightBarEl) { | ||
| this.hlightBarEl.remove(); | ||
| } | ||
| this.hlightBarEl = hlButton; | ||
| const currentSelection = window.getSelection(); | ||
| const start = currentSelection.anchorNode.parentElement; | ||
| const end = currentSelection.focusNode.parentElement; // will always be a text node | ||
| const height = 30; | ||
| const width = 60; | ||
| const selectionTextLayer = start.closest('.BRtextLayer'); | ||
| const startBoundingRect = start.getBoundingClientRect(); | ||
| const endBoundingRect = end.getBoundingClientRect(); | ||
| let hlButtonTop = startBoundingRect.top - height; | ||
| let hlButtonLeft = startBoundingRect.left - width; | ||
| if (currentSelection.direction == 'backward') { | ||
| hlButtonTop = endBoundingRect.top - height; | ||
| hlButtonLeft = endBoundingRect.left - width; | ||
| } | ||
| hlButton.className = "textFragmentCopy"; | ||
|
|
||
| $(hlButton).css({ | ||
| 'top': `${hlButtonTop}px`, | ||
| 'left': `${hlButtonLeft}px`, | ||
| 'width': `${width}px`, | ||
| 'height': `${height}px`, | ||
| 'position': 'absolute', | ||
| 'z-index': 1, | ||
| 'background': `url(..${this.br.imagesBaseURL}translate.svg)`, | ||
| 'background-position': 'center', | ||
| 'background-repeat': 'no-repeat', | ||
| 'background-color': 'darksalmon', | ||
| }); | ||
| $(hlButton).on('mousedown', (event) => { | ||
| event.stopPropagation(); | ||
| const currentUrl = window.location; | ||
| const textContentParams = document.getSelection(); | ||
| let currentParams = this.br.readQueryString(); | ||
| if (currentParams.includes('text')) { | ||
| currentParams = currentParams.replace(/(text=)[\w\W\d%]+/, createParam(textContentParams, selectionTextLayer)); | ||
| } else { | ||
| currentParams = `${currentParams}&${createParam(document.getSelection(), selectionTextLayer)}`; | ||
| } | ||
| navigator.clipboard.writeText(`${currentUrl.origin}${currentUrl.pathname}${currentParams}${currentUrl?.hash}`); | ||
| }); | ||
|
|
||
| $(hlButton).on('mouseup', (event) => { | ||
| event.stopPropagation(); | ||
| }); | ||
| document.body.append(hlButton); | ||
| } | ||
|
|
||
| _limitSelection = () => { | ||
| const selection = window.getSelection(); | ||
|
|
@@ -206,6 +276,118 @@ export class TextSelectionManager { | |
| }; | ||
| } | ||
|
|
||
| /** TODO -> | ||
| * Can import something that handles this more gracefully? see - https://web.dev/articles/text-fragments#:~:text=In%20its%20simplest%20form%2C%20the%20syntax%20of,percent%2Dencoded%20text%20I%20want%20to%20link%20to. | ||
| */ | ||
| /** | ||
| * | ||
| * @param {*} text - document.getSelection() | ||
| * @param {*} pageLayer - anchorNode.parentElement.closest('.BRtextLayer') | ||
| * @returns | ||
| */ | ||
| export function createParam(text, pageLayer = null) { | ||
schu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const highlightedText = text.toString().replaceAll(/[\s]+/g, " ").trim().split(" "); | ||
| const anchorWord = text.anchorNode.textContent; | ||
| const focusWord = text.focusNode.textContent; | ||
| let textStart, textEnd; // :~:text=[prefix-,]textStart[,textEnd][,-suffix] | ||
| const direction = text.direction; | ||
|
|
||
| // Duplicated spaces in pageLayer.textContent for some reason | ||
| const wholePageText = pageLayer.textContent.replaceAll(" ", " "); | ||
| if (direction == 'backward') { | ||
| textStart = focusWord.replaceAll(/[\s]+/g, "") ? focusWord : highlightedText[0]; | ||
| textEnd = anchorWord.replaceAll(/[\s]+/g, "") ? anchorWord : highlightedText[highlightedText.length - 1]; | ||
| } else { | ||
| textStart = anchorWord.replaceAll(/[\s]+/g, "") ? anchorWord : highlightedText[0]; | ||
| textEnd = focusWord.replaceAll(/[\s]+/g, "") ? focusWord : highlightedText[highlightedText.length - 1]; | ||
| } | ||
|
|
||
| const escapedStart = RegExp.escape(textStart); | ||
| const escapedEnd = RegExp.escape(textEnd); | ||
| const testRegExp = new RegExp(String.raw`(?=(${escapedStart}).*?(?:(${escapedEnd})))`, "gi"); | ||
|
|
||
| const foundMatches = wholePageText.matchAll(testRegExp).toArray(); | ||
| if (foundMatches.length == 1) { | ||
| return `text=${textStart},${textEnd}`; | ||
| } | ||
| const preStartRange = document.createRange(); | ||
| const postEndRange = document.createRange(); | ||
| if (direction == 'backward') { | ||
| preStartRange.setStart(pageLayer.firstElementChild, 0); | ||
| preStartRange.setEnd(text.focusNode, 0); | ||
|
|
||
| postEndRange.setStart(text.anchorNode, text.anchorNode.textContent.length); | ||
| postEndRange.setEnd(pageLayer.lastElementChild, pageLayer.lastElementChild.childElementCount); | ||
|
|
||
| } else { | ||
| preStartRange.setStart(pageLayer.firstElementChild, 0); | ||
| preStartRange.setEnd(text.anchorNode, 0); | ||
|
|
||
| postEndRange.setStart(text.focusNode, text.focusNode.textContent.length); | ||
| postEndRange.setEnd(pageLayer.lastElementChild, pageLayer.lastElementChild.childElementCount); | ||
| } | ||
|
|
||
| const startRegex = new RegExp(String.raw`(\s+\S+){1,3}\s*?$`); | ||
| const endRegex = new RegExp(String.raw`^\S*?(\s+\S+){1,3}`); | ||
| // prefixes/suffixes cannot contain paragraph breaks | ||
| const textFragmentArr = []; | ||
| let [prefixes, suffixes] = ["", ""]; | ||
| if (preStartRange.toString().length !== 0) { | ||
| if (!preStartRange.toString().match(startRegex)) { | ||
| prefixes = `${preStartRange.toString() | ||
| .replace(/[ ]+/g, " ") | ||
| .trim() | ||
| .replace(/^[^\n]*\n/gm, "")}-`; | ||
| } else { | ||
| prefixes = `${preStartRange.toString().match(startRegex)[0] | ||
| .replace(/[ ]+/g, " ") | ||
| .trim() | ||
| .replace(/^[^\n]*\n/gm, "")}-`; | ||
| } | ||
| textFragmentArr.push(prefixes); | ||
| } | ||
| if (postEndRange.toString().length !== 0) { | ||
| if (!postEndRange.toString().match(endRegex)) { | ||
| suffixes = `-${postEndRange.toString() | ||
| .replace(/[ ]+/g, " ") | ||
| .trim() | ||
| .replace(/^[^\n]*\n/gm, "")}`; | ||
| } else { | ||
| suffixes = `-${postEndRange.toString().match(endRegex)[0] | ||
| .replace(/[ ]+/g, " ") | ||
| .trim() | ||
| .replace(/\n[^\n]*$/gm, "")}`; | ||
| } | ||
| } | ||
|
|
||
| if (textStart === textEnd) { | ||
| textEnd = ""; | ||
| } | ||
|
|
||
| const selection = window.getSelection(); | ||
schu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const constructHighlight = selection.toString().replace(/[\s]+/g, " ").split(/[ ]+/g); | ||
| if (direction == 'backward') { | ||
| constructHighlight[0] = selection.focusNode.textContent; | ||
| constructHighlight[constructHighlight.length - 1] = selection.anchorNode.textContent; | ||
| } else { | ||
| constructHighlight[0] = selection.anchorNode.textContent; | ||
| constructHighlight[constructHighlight.length - 1] = selection.focusNode.textContent; | ||
| } | ||
| const fullHighlight = constructHighlight.join(" ").trim().split(" "); | ||
| let quote = [fullHighlight.join(" ")]; | ||
| if (fullHighlight.length > 6) { | ||
| if (direction == 'backward') { | ||
| quote = [fullHighlight.slice(0, 3).join(" "), fullHighlight.slice(-3).join(" ")]; | ||
| } else { | ||
| quote = [fullHighlight.slice(0, 3).join(" "), fullHighlight.slice(-3).join(" ")]; | ||
| } | ||
| } | ||
| textFragmentArr.push(...quote); | ||
| if (suffixes.length != 0) { | ||
| textFragmentArr.push(suffixes); | ||
| } | ||
| return `text=${textFragmentArr.map(encodeURIComponent).join(',')}`; | ||
| } | ||
| /** | ||
| * @template T | ||
| * Get the i-th element of an iterable | ||
|
|
@@ -280,3 +462,37 @@ export function* walkBetweenNodes(start, end) { | |
|
|
||
| yield* walk(start); | ||
| } | ||
|
|
||
|
|
||
| export class TextFragment { | ||
schu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** @type {string | null} */ | ||
| prefix | ||
| /** @type {string} */ | ||
| textStart | ||
| /** @type {string | null} */ | ||
| textEnd | ||
| /** @type {string | null} */ | ||
| suffix | ||
|
|
||
| /** @returns {string} */ | ||
| toString() {} | ||
|
|
||
| /** | ||
| * @param {string} urlString | ||
| * @returns {TextFragment} | ||
| **/ | ||
| static fromString(urlString) {} | ||
|
|
||
| /** | ||
| * @param {Selection} selection | ||
| * @returns {TextFragment} | ||
| */ | ||
| static fromSelection(selection) {} | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Prefix “That is easily-, | ||
| * textstart %20 got.” “ And%20 | ||
| * Suffix - help.”“That is | ||
| */ | ||
Uh oh!
There was an error while loading. Please reload this page.