@@ -5,14 +5,16 @@ import { customElement } from 'lit/decorators.js';
55import '@internetarchive/icon-share' ;
66
77export class TextSelectionManager {
8- /** @type {BRSelectMenu } */
9- selectMenu ;
10- selectionMenuEnabled ;
118 options = {
129 // Current Translation plugin implementation does not have words, will limit to one BRlineElement for now
1310 maxProtectedWords : 200 ,
1411 }
1512
13+ /** @type {BRSelectMenu } */
14+ selectMenu ;
15+ /** @type {boolean } */
16+ selectionMenuEnabled = false ;
17+
1618 /**
1719 * @param {string } layer Selector for the text layer to manage
1820 * @param {import('../BookReader.js').default } br
@@ -29,8 +31,8 @@ export class TextSelectionManager {
2931 this . selectionElement = selectionElement ;
3032 this . selectionObserver = new SelectionObserver ( this . layer , this . _onSelectionChange ) ;
3133 this . options . maxProtectedWords = maxWords ? maxWords : 200 ;
32- this . selectMenu = new BRSelectMenu ( br ) ;
3334
35+ this . selectMenu = new BRSelectMenu ( br ) ;
3436 this . selectMenu . className = "br-select-menu__root" ;
3537 }
3638
@@ -66,7 +68,9 @@ export class TextSelectionManager {
6668 // Need attach + detach methods to toggle w/ Translation plugin
6769 attach ( ) {
6870 this . selectionObserver . attach ( ) ;
69- this . renderSelectionMenu ( ) ;
71+ if ( this . selectionMenuEnabled ) {
72+ this . renderSelectionMenu ( ) ;
73+ }
7074 if ( this . br . protected ) {
7175 document . addEventListener ( 'selectionchange' , this . _limitSelection ) ;
7276 // Prevent right clicking when selected text
@@ -94,9 +98,7 @@ export class TextSelectionManager {
9498
9599 renderSelectionMenu ( ) {
96100 if ( document . querySelector ( '.br-select-menu__option' ) ) return ;
97- if ( this . selectionMenuEnabled ) {
98- document . body . append ( this . selectMenu ) ;
99- }
101+ document . body . append ( this . selectMenu ) ;
100102 }
101103 /**
102104 * @param {'started' | 'cleared' | 'focusChanged' } type
@@ -244,18 +246,18 @@ export class TextSelectionManager {
244246 } ;
245247}
246248
247- /** TODO ->
248- * 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.
249- */
250249/**
251- * Builds a URL string in the format of a TextFragment within the URL params, which differs from browser "Copy to link to highlighted text" format
252- * Does not include the fragment directive ( `:~:`) and is not after the URL hash
250+ * Builds a TextFragment string from a given text selection.
251+ * Note does not include the fragment directive `:~:` or # symbol
253252 * See https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments
254- * @param {Selection } selection - document.getSelection()
255- * @param {HTMLElement } pageLayer - anchorNode.parentElement.closest('.BRtextLayer')
256- * @returns {string } - i.e. http://127.0.0.1:8000/BookReaderDemo/demo-internetarchive.html?ocaid=adventureofsherl0000unse&text=undefined,undefined#page/10/mode/2up
253+ * @param {Selection } selection currently selected text, eg ` document.getSelection()`
254+ * @param {HTMLElement[] } contextElements elements providing context for the selection
255+ * @returns {string }
257256 */
258- export function createTextFragmentUrlParam ( selection , pageLayer ) {
257+ export function createTextFragmentUrlParam ( selection , contextElements ) {
258+ // TODO: Can import something that handles this more gracefully? see -
259+ // https://web.dev/articles/text-fragments#:~:text=In%20its%20simplest%20form%2C%20the%20syntax%20of,percent%2Dencoded%20text%20I%20want%20to%20link%20to.
260+
259261 // :~:text=[prefix-,]textStart[,textEnd][,-suffix]
260262 const highlightedText = selection . toString ( ) . replace ( / [ \s ] + / g, " " ) . trim ( ) . split ( " " ) ;
261263 const direction = selection . direction ;
@@ -274,37 +276,27 @@ export function createTextFragmentUrlParam(selection, pageLayer) {
274276 const endPhraseMatchRe = new RegExp ( String . raw `(${ textStartRe } )(?=.*?(${ textEndRe } ))` , "gis" ) ;
275277
276278 // Duplicated spaces in pageLayer.textContent for some reason
277- const wholePageText = Array . from ( document . querySelectorAll ( '.BRpage-visible' ) )
278- . map ( ( item ) => item . textContent )
279+ const selectionContext = contextElements
280+ . map ( ( el ) => el . textContent )
279281 . join ( ' ' )
280- . replace ( / \s + / g, " " ) || pageLayer . textContent . replace ( / \s + / g , " " ) ;
281- const startPhraseFoundMatches = wholePageText . matchAll ( startPhraseMatchRe ) . toArray ( ) ;
282- const endPhraseFoundMatches = wholePageText . matchAll ( endPhraseMatchRe ) . toArray ( ) ;
282+ . replace ( / \s + / g, " " ) ;
283+ const startPhraseFoundMatches = selectionContext . matchAll ( startPhraseMatchRe ) . toArray ( ) ;
284+ const endPhraseFoundMatches = selectionContext . matchAll ( endPhraseMatchRe ) . toArray ( ) ;
283285 if ( startPhraseFoundMatches . length == 1 && endPhraseFoundMatches . length == 1 ) {
284286 // If `startWord...endWord` quote is unambiguous and only occurs once, no prefix-/-suffix is needed for the URL param
285287 return `text=${ encodeURIComponent ( startWord ) } ,${ encodeURIComponent ( endWord ) } ` ;
286288 }
287289
288290 // Need to add some additional context to `startWord...endWord` by including surrounding words before and after the keywords
289291 const preStartRange = document . createRange ( ) ;
290-
291- const previousPageContainer = pageLayer . parentElement ?. previousElementSibling ;
292- if ( previousPageContainer ?. classList . contains ( "BRpage-visible" ) ) {
293- preStartRange . setStart ( previousPageContainer , 0 ) ;
294- } else {
295- preStartRange . setStart ( pageLayer . firstElementChild , 0 ) ;
296- }
292+ preStartRange . setStart ( contextElements [ 0 ] . firstElementChild , 0 ) ;
297293 preStartRange . setEnd ( startNode , 0 ) ;
294+
298295 const postEndRange = document . createRange ( ) ;
299296 postEndRange . setStart ( endNode , endNode . textContent . length ) ;
300- const nextPageContainer = pageLayer . parentElement . nextElementSibling ;
301- if ( nextPageContainer ?. classList . contains ( "BRpage-visible" ) ) {
302- const nextPageLastWord = getLastestElement ( nextPageContainer ) ;
303- postEndRange . setEnd ( nextPageLastWord , Math . max ( 0 , nextPageLastWord . textContent . length - 1 ) ) ;
304- } else {
305- const lastWordOfPageEl = getLastestElement ( pageLayer ) ;
306- postEndRange . setEnd ( lastWordOfPageEl , Math . max ( 0 , lastWordOfPageEl . textContent . length - 1 ) ) ;
307- }
297+ const lastWordOfPageEl = getLastMostElement ( contextElements [ contextElements . length - 1 ] ) ;
298+ postEndRange . setEnd ( lastWordOfPageEl , Math . max ( 0 , lastWordOfPageEl . textContent . length - 1 ) ) ;
299+
308300 // prefixes/suffixes cannot contain paragraph breaks, words that are from more than one line break away should not be included
309301 const prefix = getLastWords ( 3 , preStartRange . toString ( ) )
310302 . replace ( / [ ] + / g, " " )
@@ -446,33 +438,38 @@ class BRSelectMenu extends LitElement {
446438 ` ;
447439 }
448440
441+ /**
442+ * @param {MouseEvent } e
443+ */
449444 handleCopyLinkToHighlight ( e ) {
450445 e . preventDefault ( ) ;
451- const currentUrl = window . location ;
452- let currentParams = this . br . readQueryString ( ) ;
446+
447+ const currentParams = this . br . readQueryString ( ) ;
453448 const currentSelection = window . getSelection ( ) ;
454449 /** @type {HTMLElement } */
455450 const textLayer = currentSelection . anchorNode . parentElement . closest ( '.BRtextLayer' ) ;
456- // To do - updateResumeValue + getCookiePath in plugin.resume.js overrides the adjustedUrlPageNumPath, check how to workaround this
457- const adjustedUrlPageNumPath = currentUrl . pathname . toString ( ) . replace ( / (?< = \/ p a g e \/ ) \d + (? = \/ ) / , textLayer . parentElement . getAttribute ( 'data-page-num' ) ) ;
451+ const textFragmentUrlParam = createTextFragmentUrlParam ( currentSelection , Array . from ( document . querySelectorAll ( '.BRpage-visible' ) ) ) ;
452+
453+ // Note: Have to do a param construction to avoid url-encoding of commas in the text fragment param
454+ let linkToHighlightParams = currentParams ;
458455 if ( currentParams . includes ( 'text=' ) ) {
459- currentParams = currentParams . replace ( / ( t e x t = ) [ \w \W \d % ] + / , createTextFragmentUrlParam ( currentSelection , textLayer ) ) ;
456+ linkToHighlightParams = currentParams . replace ( / ( t e x t = ) [ \w \W \d % ] + / , textFragmentUrlParam ) ;
460457 } else {
461- if ( this . br . options . urlMode === 'history' ) {
462- currentParams = `?${ createTextFragmentUrlParam ( currentSelection , textLayer ) } ` ;
463- } else {
464- currentParams = `${ currentParams } &${ createTextFragmentUrlParam ( currentSelection , textLayer ) } ` ;
465- }
466- }
467- if ( this . br . options . urlMode === 'history' ) {
468- navigator . clipboard . writeText ( `${ currentUrl . origin } ${ adjustedUrlPageNumPath } ${ currentParams } ` ) ;
469- } else {
470- navigator . clipboard . writeText ( `${ currentUrl . origin } ${ adjustedUrlPageNumPath } ${ currentParams } ${ currentUrl ?. hash } ` ) ;
458+ const sep = linkToHighlightParams ? '&' : '?' ;
459+ linkToHighlightParams += `${ sep } ${ textFragmentUrlParam } ` ;
471460 }
461+
462+ const currentUrl = window . location ;
463+ // TODO - updateResumeValue + getCookiePath in plugin.resume.js overrides the adjustedUrlPageNumPath, check how to workaround this
464+ // TODO - won't work with hash mode
465+ const adjustedUrlPageNumPath = currentUrl . pathname . toString ( ) . replace ( / (?< = \/ p a g e \/ ) \d + (? = \/ ) / , textLayer . parentElement . getAttribute ( 'data-page-num' ) ) ;
466+
467+ const linkToHighlight = `${ currentUrl . origin } ${ adjustedUrlPageNumPath } ${ linkToHighlightParams } ${ currentUrl ?. hash || '' } ` ;
468+ navigator . clipboard . writeText ( linkToHighlight ) ;
472469 }
473470
474471 showMenu ( ) {
475- if ( this . br . plugins . translate ) return ;
472+ if ( this . br . plugins . translate ?. userToggleTranslate ) return ;
476473 const currentSelection = window . getSelection ( ) ;
477474 const start = currentSelection . anchorNode . parentElement ;
478475 const end = currentSelection . focusNode . parentElement ; // will always be a text node
@@ -490,7 +487,7 @@ class BRSelectMenu extends LitElement {
490487 this . style . left = `${ hlButtonLeft } px` ;
491488 this . style . zIndex = '1' ;
492489 this . style . position = 'absolute' ;
493- this . style . display = 'inline ' ;
490+ this . style . display = 'block ' ;
494491 }
495492
496493 hideMenu = ( ) => {
@@ -527,7 +524,7 @@ export function getLastWords(numWords, text) {
527524 * @param {HTMLElement | Element } parent
528525 * @returns {Node }
529526 */
530- export function getLastestElement ( parent ) {
527+ export function getLastMostElement ( parent ) {
531528 while ( parent . lastElementChild ) {
532529 parent = parent . lastElementChild ;
533530 }
0 commit comments