diff --git a/src/BookReader.js b/src/BookReader.js index a7e6dea1b..1175d7a57 100644 --- a/src/BookReader.js +++ b/src/BookReader.js @@ -1976,9 +1976,12 @@ BookReader.prototype.queryStringFromParams = function( // the browser seems not to handle with the text fragment if (newParams.get('text')) { newParams.delete('text'); - textFragmentParam = `text=${this.urlPlugin.retrieveTextFragment(currQueryString)}`; + textFragmentParam = `text=${this.urlPlugin.retrieveTextFragment(currQueryString, 'text')}`; + } + if (newParams.get('dIndex')) { + newParams.delete('dIndex'); + textFragmentParam += `&dIndex=${this.urlPlugin.retrieveDIndex(currQueryString)}`; } - // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString // Note: This method returns the query string without the question mark. let result = newParams.toString(); diff --git a/src/css/_TextSelection.scss b/src/css/_TextSelection.scss index b1cbff4c7..bd14cd8cc 100644 --- a/src/css/_TextSelection.scss +++ b/src/css/_TextSelection.scss @@ -54,13 +54,6 @@ } } -// Style URI TextFragments, eg #:~:text=example -.BRtextLayer ::target-text { - // Similar colour to the default one used in Safari, Firefox. Note Chrome uses a purple colour - background-color: hsla(45, 80%, 66%, 0.6); - color: transparent; -} - .BRtranslateLayer ::selection { background: hsla(210, 74%, 62%, 0.4); } @@ -209,4 +202,82 @@ width: auto; margin-left: 4px; opacity: 1; -} \ No newline at end of file +} + +.br-annotate-menu__root { + width: auto; + height: auto; + opacity: 1; + border-radius: 12px; + z-index: 1; + display: block; + position: absolute; + padding: 10px; + background-color: black; +} + +.br-annotate-menu__icon { + display: inline-block; + flex-shrink: 0; + width: 12px; + height: 12px; +} + +.br-annotate-menu__label { + width: fit-content; + font-size:12px; +} + +.br-annotate-menu__textarea { + margin: 5px 0 5px 0; + width: 90%; + height: 50%; + max-width: 90%; + resize: none; +} + +.br-annotate-menu__displayAnnotation { + margin: 0 0 5px 0; + word-break: break-word; + display: inline-block; + height: 100%; + width: 100%; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: gray white; + font-style: italic; +} + +.br-annotate-menu__div { + margin: 0 0 0 5px; +} + +.br-annotate-menu__color { + width: 25px; + height: 20px; + margin: 0 0 0 5px; +} + +.br-annotate-menu__comment { + margin: 5px 0 5px 0; + padding: 5px 0 5px 5px; + width: 90%; + // height: max-content; + height: 50%; + max-width: 90%; + background-color: #c8af91; + font-size: 12px; + border-radius: 5px; +} + +.BRtextLayer .BRhighlight { + background-color: yellow; + pointer-events: all; +} + +// Style URI TextFragments, eg #:~:text=example +.BRtextLayer .BRhighlight--target-text, .BRtextLayer ::target-text { + // Similar colour to the default one used in Safari, Firefox. Note Chrome uses a purple colour + background-color: hsla(45, 80%, 66%, 0.6); + color: transparent; +} diff --git a/src/plugins/plugin.experiments.js b/src/plugins/plugin.experiments.js index 0d14bbd7b..b1f8db0a7 100644 --- a/src/plugins/plugin.experiments.js +++ b/src/plugins/plugin.experiments.js @@ -51,7 +51,7 @@ export class ExperimentsPlugin extends BookReaderPlugin { localStorageKey: 'BrExperiments', /** The experiments that should be shown in the experiments panel */ - enabledExperiments: ['translate', 'copyLinkToHighlight'], + enabledExperiments: ['translate', 'copyLinkToHighlight', 'annotateHighlight'], } /** @type {ExperimentModel[]} */ @@ -60,7 +60,6 @@ export class ExperimentsPlugin extends BookReaderPlugin { name = 'copyLinkToHighlight'; title = 'Copy to Selection URL'; description = 'Share text selection via URL'; - learnMore = 'none'; icon = null; enabled = false; async enable ({ manual = false }) { @@ -72,6 +71,21 @@ export class ExperimentsPlugin extends BookReaderPlugin { }); } }(), + new class extends ExperimentModel { + name = 'annotateHighlight'; + title = 'Highlight and annotate'; + description = 'Create private highlights and annotations for this book'; + icon = null; + enabled = false; + async enable ({ manual = false }) { + this.br.plugins.textSelection.enableHighlightMenu(); + } + async disable() { + sleep(0).then(() => { + window.location.reload(); + }); + } + }(), new class extends ExperimentModel { name = 'translate'; title = 'Translate Plugin'; diff --git a/src/plugins/plugin.text_selection.js b/src/plugins/plugin.text_selection.js index 58187af28..89761aa60 100644 --- a/src/plugins/plugin.text_selection.js +++ b/src/plugins/plugin.text_selection.js @@ -68,6 +68,11 @@ export class TextSelectionPlugin extends BookReaderPlugin { this.textSelectionManager.renderSelectionMenu(); } + enableHighlightMenu() { + this.textSelectionManager.highlightAnnotationEnabled = true; + this.textSelectionManager.renderHighlightMenu(); + } + /** * @override * @param {PageContainer} pageContainer diff --git a/src/plugins/url/UrlPlugin.js b/src/plugins/url/UrlPlugin.js index f965674ba..38b99f3f6 100644 --- a/src/plugins/url/UrlPlugin.js +++ b/src/plugins/url/UrlPlugin.js @@ -190,15 +190,10 @@ export class UrlPlugin { } /** - * Get the hash out of the current URL. Also augments it with the text - * from the main part of the URL, since that is not readable by JS - * from the actual hash - * @returns + * Get the hash out of the current URL */ getHash() { - const text = this.retrieveTextFragment(window.location.search); - const textFragment = text ? `:~:text=${text[0]}` : ''; - return `${window.location.hash.slice(1)}${textFragment}`; + return window.location.hash.slice(1); } /** @@ -208,4 +203,59 @@ export class UrlPlugin { retrieveTextFragment(urlString) { return urlString.match(/(?<=[&?]?text=)[^&]*/); } + + retrieveDIndex(urlString) { + return urlString.match(/(?<=[&?]?dIndex=)[^&]*/); + } + + /** + * @param {string} urlString + * @param {string} type + * @returns {string} + */ + + retrieveHighlightContext(urlString, type) { + const regexString = new RegExp(String.raw`(?<=[&?]?${type}=)[^&]*`, "gis"); + return urlString.match(regexString); + } + + parseToText(urlString) { + const quoteMatch = urlString.match(/(?<=[&?]?text=)[^&]*/); + let dIndex = urlString.match(/(?<=[&?]?dIndex=)[^&]*/); + let quote, prefix, suffix; + if (quoteMatch) { + const prefixMatch = quoteMatch[0].match(/.*?(?=(-,))/); + const suffixMatch = quoteMatch[0].match(/(?<=,-).*/); + if (prefixMatch) prefix = decodeURIComponent(prefixMatch[0]); + if (suffixMatch) suffix = decodeURIComponent(suffixMatch[0]); + if (prefixMatch && suffixMatch) { + quote = decodeURIComponent(quoteMatch[0].match(/(?<=(-,)).*(?=(,-))/)[0]); + } else if (prefixMatch && !suffixMatch) { // Prefix only + quote = decodeURIComponent(quoteMatch[0].match(/(?<=-,).*/)[0]); + } else if (!prefixMatch && suffixMatch) { // Suffix only + quote = decodeURIComponent(quoteMatch[0].match(/^.*?(?=,-)/)[0]); + } else { // Somehow no prefix or suffix + quote = decodeURIComponent(quoteMatch[0]); + } + } + if (dIndex) dIndex = decodeURIComponent(dIndex[0]); + return {prefix, quote, suffix, dIndex}; + } } + +/** + * @typedef {Object} BookReaderTextFragment + * An extension of the fields defined by the browser-native TextFragment; + * See https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments + * + * @property {string} prefix + * @property {string} quote + * @property {string} suffix + * @property {string} dIndex Page index + * @property {string} dPageNum Page num ; eg. asserted page number or the n-prefixed page index + */ + +/** + * @typedef {BookReaderTextFragment} BookReaderSavedHighlight + * @property {string} uuid + */ diff --git a/src/plugins/url/plugin.url.js b/src/plugins/url/plugin.url.js index 6f9368ded..b64f02cfc 100644 --- a/src/plugins/url/plugin.url.js +++ b/src/plugins/url/plugin.url.js @@ -2,7 +2,7 @@ import { UrlPlugin } from "./UrlPlugin.js"; import { sleep } from "../../BookReader/utils.js"; - +import { convertRangeToDOMSelection } from "../../util/TextSelectionManager.js"; /** * Plugin for URL management in BookReader * Note read more about the url "fragment" here: @@ -43,10 +43,6 @@ BookReader.prototype.setup = (function(super_) { this.locationPollId = null; this.oldLocationHash = null; this.oldUserHash = null; - // Should include the :~:text= prefix - this.textFragment = null; - // Tracks the original textFragment page num when first loaded - this.textFragmentPage = null; }; })(BookReader.prototype.setup); @@ -146,22 +142,18 @@ BookReader.prototype.urlUpdateFragment = function() { }, {}); // eg 'page/3/mode/2up'; no query params (in hash mode, it might have /search/term) - // Does NOT have the :~:text fragment const newFragment = this.fragmentFromParams(params, this.options.urlMode); const newFragmentWithSlash = newFragment === '' ? '' : `/${newFragment}`; // eg 'page/3/mode/2up'; no query params - // WILL CONTAIN the :~:text fragment in hash mode (!) const currFragment = this.urlReadFragment(); // This should have both ?q=foo&text=bar (and any other params) as an encoded string const currQueryString = this.getLocationSearch(); // Eg ?q=foo&text=bar; only query params, no fragment const newQueryString = this.queryStringFromParams(params, currQueryString, this.options.urlMode); - - // NOTE: If ?text is in the URL, we will fire fragment change events on every render; which is - // not desireable, but currently don't have a way to handle re-writing ?text to the hash text - // fragment form, :~:text=foo. - const hasTextParam = this.urlPlugin.retrieveTextFragment(currQueryString); - if (currFragment === newFragment && currQueryString === newQueryString && !hasTextParam) { + // Adding a check to the urlMode for now. Without the check, the highlight does not work if shared and the page is refreshed + if (currFragment === newFragment && currQueryString === newQueryString + && !newQueryString.includes('text=') + ) { return; } @@ -170,19 +162,13 @@ BookReader.prototype.urlUpdateFragment = function() { this.options.urlMode = 'hash'; } else { const baseWithoutSlash = this.options.urlHistoryBasePath.replace(/\/+$/, ''); - const textFragment = this.urlPlugin.retrieveTextFragment(newQueryString); + this.targetTextFragment = this.urlPlugin.parseToText(newQueryString); + console.log("from history mode", this.targetTextFragment); const newUrlPath = `${baseWithoutSlash}${newFragmentWithSlash}${newQueryString}`; - const extractedPage = this.urlPlugin.urlStringToUrlState(newFragmentWithSlash)?.page; - if (!this.textFragmentPage && textFragment) { - this.textFragmentPage = extractedPage ? extractedPage : null; - this.textFragment = `:~:text=${textFragment}`; - } + try { window.history.replaceState({}, null, newUrlPath); this.oldLocationHash = newFragment + newQueryString; - if (textFragment) { - this.oldLocationHash += `:~:text=${textFragment[0]}`; - } } catch (e) { // DOMException on Chrome when in sandboxed iframe this.options.urlMode = 'hash'; @@ -192,22 +178,10 @@ BookReader.prototype.urlUpdateFragment = function() { if (this.options.urlMode === 'hash') { const newQueryStringSearch = this.urlParamsFiltersOnlySearch(this.readQueryString()); - let textFragment = this.urlPlugin.retrieveTextFragment(this.readQueryString()); - const extractedPage = this.urlPlugin.urlStringToUrlState(newFragmentWithSlash)?.page; - - if (textFragment) { - textFragment = `:~:text=${textFragment[0]}`; - } else { - textFragment = ''; - } - if (!this.textFragmentPage && textFragment) { - this.textFragmentPage = extractedPage ? extractedPage : null; - this.textFragment = textFragment; - } else if (this.textFragmentPage && extractedPage != this.textFragmentPage) { - textFragment = ''; - } - window.location.replace('#' + newFragment + newQueryStringSearch + textFragment); - this.oldLocationHash = newFragment + newQueryStringSearch + textFragment; + this.targetTextFragment = this.urlPlugin.parseToText(this.readQueryString()); + console.log("from hash mode", this.targetTextFragment); + window.location.replace('#' + newFragment + newQueryStringSearch); + this.oldLocationHash = newFragment + newQueryStringSearch; } }; @@ -245,24 +219,24 @@ BookReader.prototype.urlReadHashFragment = function() { return window.location.hash.substr(1); }; export class BookreaderUrlPlugin extends BookReader { + /** @type {import('./UrlPlugin.js').BookReaderTextFragment} */ + targetTextFragment; + init() { if (this.options.enableUrlPlugin) { this.urlPlugin = new UrlPlugin(this.options); const location = this.getLocationSearch(); if (location.includes("text=")) { this.on('textLayerVisible', async (_, {pageContainerEl}) => { - const visiblePageNum = pageContainerEl.getAttribute('data-page-num'); - - // Hack: More time mode 1up page "settle down" from user scrolling - await sleep(this.mode === 1 ? 900 : 100); - - // No textFragment found or the textFragment stored doesn't match current visible page loaded - if (!this.textFragment || this.textFragmentPage !== visiblePageNum) return; - if (this.options.urlMode === 'history') { - window.location.replace(`#${this.textFragment}`); - } else { - // for urlMode hash, textFragment is stored in oldLocationHash already - window.location.replace(`#${this.oldLocationHash}`); + const hasTargetText = this.targetTextFragment?.dIndex === pageContainerEl.getAttribute('data-index'); + if (hasTargetText) { + if (!this.targetTextFragment['dPageNum']) { + this.targetTextFragment['dPageNum'] = pageContainerEl.getAttribute('data-page-num'); + } + // Hack: More time mode 1up page "settle down" from user scrolling + await sleep(this.mode === 1 ? 900 : 400); + console.log("testing this textFragment", this.targetTextFragment); + convertRangeToDOMSelection(this.targetTextFragment); } }); } diff --git a/src/util/TextSelectionManager.js b/src/util/TextSelectionManager.js index 16335e58f..114bbe385 100644 --- a/src/util/TextSelectionManager.js +++ b/src/util/TextSelectionManager.js @@ -14,7 +14,7 @@ export class TextSelectionManager { selectMenu; /** @type {boolean} */ selectionMenuEnabled = false; - + highlightAnnotationEnabled = false; /** * @param {string} layer Selector for the text layer to manage * @param {import('../BookReader.js').default} br @@ -32,8 +32,10 @@ export class TextSelectionManager { this.selectionObserver = new SelectionObserver(this.layer, this._onSelectionChange); this.options.maxProtectedWords = maxWords ? maxWords : 200; - this.selectMenu = new BRSelectMenu(br); + this.selectMenu = new BRSelectMenu(br, selectionElement, this.selectionMenuEnabled, this.highlightAnnotationEnabled); + this.annotationMenu = new BRAnnotationMenu(br); this.selectMenu.className = "br-select-menu__root"; + this.annotationMenu.className = "br-annotate-menu__root"; } init() { @@ -54,8 +56,13 @@ export class TextSelectionManager { // hide the button as user changes their selection if (this.mouseIsDown) { this.selectMenu.hideMenu(); + this.annotationMenu.hideAnnotationMenu(); } else if (window.getSelection().toString()) { this.selectMenu.showMenu(); + const selectedElement = window.getSelection()?.anchorNode; + if (selectedElement.classList?.contains('BRhighlight')) { + this.getHighlightedNodes(selectedElement); + } } } @@ -71,6 +78,10 @@ export class TextSelectionManager { if (this.selectionMenuEnabled) { this.renderSelectionMenu(); } + if (this.highlightAnnotationEnabled) { + this.renderHighlightMenu(); + } + if (this.br.protected) { document.addEventListener('selectionchange', this._limitSelection); // Prevent right clicking when selected text @@ -97,8 +108,23 @@ export class TextSelectionManager { } renderSelectionMenu() { - if (document.querySelector('.br-select-menu__option')) return; - document.body.append(this.selectMenu); + this.selectMenu.copyHighlightEnabled = true; + if (this.highlightAnnotationEnabled) { + this.selectMenu.requestUpdate(); + } else { + document.body.append(this.selectMenu); + document.body.append(this.annotationMenu); + } + } + + renderHighlightMenu() { + this.selectMenu.highlightAnnotationEnabled = true; + if (this.selectionMenuEnabled) { + this.selectMenu.requestUpdate(); + } else { + document.body.append(this.selectMenu); + document.body.append(this.annotationMenu); + } } /** * @param {'started' | 'cleared' | 'focusChanged'} type @@ -163,6 +189,7 @@ export class TextSelectionManager { $(textLayer).on("mousedown.textSelectPluginHandler", (event) => { this.mouseIsDown = true; this.selectMenu.hideMenu(); + this.annotationMenu.hideAnnotationMenu(); if ($(event.target).is(this.selectionElement.join(", "))) { event.stopPropagation(); } @@ -197,6 +224,7 @@ export class TextSelectionManager { this.mouseIsDown = true; event.stopPropagation(); this.selectMenu.hideMenu(); + this.annotationMenu.hideAnnotationMenu(); }); // Prevent page flip on click @@ -208,6 +236,13 @@ export class TextSelectionManager { }); } + getHighlightedNodes(element) { + const highlightIdentifier = retrieveUUID(element); + const highlightNodes = document.querySelectorAll(`.${highlightIdentifier}`); + this.selectMenu.nodesForRemoval = highlightNodes; + this.selectMenu.requestUpdate(); + } + _limitSelection = () => { const selection = window.getSelection(); if (!selection.rangeCount) return; @@ -255,45 +290,18 @@ export class TextSelectionManager { * @returns {string} */ export function createTextFragmentUrlParam(selection, contextElements) { - // 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. - - // :~:text=[prefix-,]textStart[,textEnd][,-suffix] - const highlightedText = selection.toString().replace(/[\s]+/g, " ").trim().split(" "); + console.log("this is the selection", selection, selection.toString()); const direction = selection.direction; - const startNode = direction == 'backward' ? selection.focusNode : selection.anchorNode; - const endNode = direction == 'backward' ? selection.anchorNode : selection.focusNode; - // If text selection begins or ends with a space, we look for the next eligible word to serve as the start or end word - const startWord = startNode.textContent.replace(/[\s]+/g, "") ? startNode.textContent : highlightedText[0]; - const endWord = endNode.textContent.replace(/[\s]+/g, "") ? endNode.textContent : highlightedText[highlightedText.length - 1]; - - const textStartRe = RegExp.escape(startWord); - const textEndRe = RegExp.escape(endWord); - - // 's' regex modifier ensures the `.` also captures newline characters - // Need to use lookahead/lookbehind assertions to allow for overlapping quotes (i.e. multiple "Holmes" on the same page) - const startPhraseMatchRe = new RegExp(String.raw`(?<=(${textStartRe}).*?)(${textEndRe})`, "gis"); - const endPhraseMatchRe = new RegExp(String.raw`(${textStartRe})(?=.*?(${textEndRe}))`, "gis"); - - // Duplicated spaces in pageLayer.textContent for some reason - const selectionContext = contextElements - .map((el) => el.textContent) - .join(' ') - .replace(/\s+/g, " "); - const startPhraseFoundMatches = selectionContext.matchAll(startPhraseMatchRe).toArray(); - const endPhraseFoundMatches = selectionContext.matchAll(endPhraseMatchRe).toArray(); - if (startPhraseFoundMatches.length == 1 && endPhraseFoundMatches.length == 1) { - // If `startWord...endWord` quote is unambiguous and only occurs once, no prefix-/-suffix is needed for the URL param - return `text=${encodeURIComponent(startWord)},${encodeURIComponent(endWord)}`; - } - - // Need to add some additional context to `startWord...endWord` by including surrounding words before and after the keywords + let startNode = direction == 'backward' ? selection.focusNode : selection.anchorNode; + let endNode = direction == 'backward' ? selection.anchorNode : selection.focusNode; + console.log("this is endNode", endNode.textContent); const preStartRange = document.createRange(); preStartRange.setStart(contextElements[0].firstElementChild, 0); preStartRange.setEnd(startNode, 0); + const endRangeLength = endNode.nodeName.toLowerCase() === 'span' ? 1 : endNode.textContent.length; const postEndRange = document.createRange(); - postEndRange.setStart(endNode, endNode.textContent.length); + postEndRange.setStart(endNode, endRangeLength); const lastWordOfPageEl = getLastMostElement(contextElements[contextElements.length - 1]); postEndRange.setEnd(lastWordOfPageEl, Math.max(0, lastWordOfPageEl.textContent.length - 1)); @@ -310,7 +318,30 @@ export function createTextFragmentUrlParam(selection, contextElements) { // Partially selected words need to be captured completely // Guarantee that all whitespace is replaced with just one space and that the first/last word of the highlight is not a space const fullHighlight = selection.toString().replace(/\s+/g, " ").trim().split(/\s/g); + console.log("this is fullHighlight", fullHighlight); // Capture start/end words that may be partially highlighted + console.log("startNode.textContent", startNode.textContent); + console.log("what is endNode", endNode?.parentElement.classList); + if (endNode?.parentElement?.classList.contains("BRwordElement--hyphen")) { + // const test = []; + // const testRange = selection.getRangeAt(0); + // let temp; + // for (const el of walkBetweenNodes(testRange.startContainer, testRange.endContainer)) { + // console.log("Looking at this el", el); + // if (el.nodeType === 'text') + // if (temp) { + // let testString = temp.textContent + el.textContent; + // console.log("this is testString", testString); + // test.push(temp.textContent + el.textContent); + // } + // if (el?.classList.contains("--hyphen")) { + // temp = el; + // } else { + // test.push(el.textContent); + // } + // } + // do something here + } if (startNode.textContent.trim().length != 0) { if (!startNode.textContent.includes(fullHighlight[0])) { fullHighlight.unshift(startNode.textContent); @@ -318,6 +349,7 @@ export function createTextFragmentUrlParam(selection, contextElements) { fullHighlight[0] = startNode.textContent; } } + console.log("endNode.textContent", endNode.textContent); if (endNode.textContent.trim().length != 0) { if (!endNode.textContent.includes(fullHighlight[fullHighlight.length - 1])) { fullHighlight.push(endNode.textContent); @@ -325,17 +357,15 @@ export function createTextFragmentUrlParam(selection, contextElements) { fullHighlight[fullHighlight.length - 1] = endNode.textContent; } - let quote = [fullHighlight.join(" ")]; - if (fullHighlight.length > 6) { - quote = [fullHighlight.slice(0, 3).join(" "), fullHighlight.slice(-3).join(" ")]; - } + const quote = encodeURIComponent(fullHighlight.join(" ")); - const textFragmentArr = []; - if (prefix) textFragmentArr.push(`${prefix}-`); - textFragmentArr.push(...quote); - if (suffix) textFragmentArr.push(`-${suffix}`); + let prefixString = ''; + let suffixString = ''; + const pageString = `&dIndex=${startNode.parentElement.closest(".BRpagecontainer").getAttribute('data-index')}`; - return `text=${textFragmentArr.map(encodeURIComponent).join(',')}`; + if (prefix) prefixString = `${encodeURIComponent(prefix)}-,`; + if (suffix) suffixString = `,-${encodeURIComponent(suffix)}`; + return `text=${prefixString}${quote}${suffixString}${pageString}`; } /** @@ -413,14 +443,191 @@ export function* walkBetweenNodes(start, end) { yield* walk(start); } +@customElement('br-annotate-menu') +class BRAnnotationMenu extends LitElement { + /** @type {import('../BookReader.js').default} */ + br; + allowAnnotationEditing = false; + currentNodes; + DEFAULT_HIGHLIGHT_COLOR = "#cab400"; + + constructor(br) { + super(); + this.br = br; + } + + createRenderRoot() { + return this; + } + + getAnnotationText() { + if (!this.currentNodes) return null; + const nodesUUID = retrieveUUID(this.currentNodes[0]); + return getEntryLocalStorage(nodesUUID, "annotation"); + } + + checkAnnotationEditing() { + const storedAnnotation = this.getAnnotationText(); + if (storedAnnotation) { + this.allowAnnotationEditing = false; + } else { + this.allowAnnotationEditing = true; + } + } + + getHighlightColor() { + if (!this.currentNodes) return this.DEFAULT_HIGHLIGHT_COLOR; + const nodesUUID = retrieveUUID(this.currentNodes[0]); + const highlightColor = getEntryLocalStorage(nodesUUID, "highlightColor"); + if (highlightColor) { + return highlightColor; + } + return this.DEFAULT_HIGHLIGHT_COLOR; + } + + showExistingAnnotation() { + return html` +
+ + + `; + } + + showTextEditArea() { + return html` + + + + `; + } + + render() { + return html` + + + ${this.allowAnnotationEditing ? this.showTextEditArea() : this.showExistingAnnotation()} + `; + } + + /** + * + * @param {} e + */ + handleColorChange(e) { + const nodeUUID = retrieveUUID(this.currentNodes[0]); + $(`.${nodeUUID}`).css("background-color", `${e.target.value}`); + adjustEntryLocalStorage(nodeUUID, "highlightColor", e.target.value); + } + + handleDeleteHighlight(e) { + if (this.currentNodes) { + const uuid = retrieveUUID(this.currentNodes[0]); + for (const ele of this.currentNodes) { + const tempText = ele.textContent; + const parent = ele.parentElement; + if (parent.classList.contains('BRwordElement') || parent.classList.contains('BRspace')) { + ele.backgroundColor = 'none'; + ele.remove(); + parent.textContent = tempText; + } + } + removeEntryLocalStorage(uuid); + this.clearCurrentNodes(); + this.hideAnnotationMenu(); + } + } + + handleEditAnnotation(e) { + e.stopPropagation(); + e.preventDefault(); + this.allowAnnotationEditing = true; + this.requestUpdate(); + } + + handleSaveAnnotation(e) { + e.stopPropagation(); + e.preventDefault(); + const inputEle = document.querySelector('#annotateTextArea'); + if (inputEle.value) { + const currentUUID = retrieveUUID(this.currentNodes[0]); + adjustEntryLocalStorage(currentUUID, "annotation", inputEle.value); + this.hideAnnotationMenu(); + } + } + + handleInputClick(e) { + e.stopImmediatePropagation(); + } + + showAnnotationMenu(nodes) { + this.currentNodes = nodes; + const identifier = retrieveUUID(nodes[0]); + const selectedQuoteNodes = document.querySelectorAll(`.${identifier}`); + + const firstNode = selectedQuoteNodes[0]; + const lastNode = selectedQuoteNodes[selectedQuoteNodes.length - 1]; + + const highlightRange = document.createRange(); + highlightRange.setStart(firstNode, 0); + highlightRange.setEnd(lastNode, 1); + + const currentSelection = window.getSelection(); + currentSelection?.removeAllRanges(); + currentSelection?.addRange(highlightRange); + + const lastNodeBoundary = lastNode.getBoundingClientRect(); + const pageContainerBoundary = lastNode.closest(".BRpagecontainer").getBoundingClientRect(); + this.requestUpdate(); + const annoButtonWidth = pageContainerBoundary.width - 50; + const annoButtonLeft = pageContainerBoundary.left; + + + this.style.backgroundColor = 'black'; + this.style.width = `${annoButtonWidth}px`; + this.style.height = `${Math.max(pageContainerBoundary.height / 5, 120)}px`; + this.style.top = `${lastNodeBoundary.top + lastNodeBoundary.height + 5}px`; + this.style.left = `${annoButtonLeft}px`; + this.style.display = 'block'; + this.checkAnnotationEditing(); + this.requestUpdate(); + } + + hideAnnotationMenu = () => { + this.style.display = 'none'; + return; + } + + clearCurrentNodes = () => { + this.currentNodes = null; + this.requestUpdate(); + } +} + @customElement('br-select-menu') class BRSelectMenu extends LitElement { /** @type {import('../BookReader.js').default} */ br; + selectionElement; + copyHighlightEnabled; + highlightAnnotationEnabled; - constructor(br) { + constructor(br, selectionElement, copyHighlightEnabled, highlightAnnotationEnabled) { super(); this.br = br; + this.selectionElement = selectionElement; + this.copyHighlightEnabled = copyHighlightEnabled; + this.highlightAnnotationEnabled = highlightAnnotationEnabled; } /** @override */ @@ -429,24 +636,78 @@ class BRSelectMenu extends LitElement { return this; } - render() { + addShareHighlightHTML() { return html` -