@@ -218,13 +218,12 @@ function Highlighter(options = hhDefaultOptions) {
218218 const highlightInfo = highlightsById [ highlightId ] ;
219219 let range = getCorrectedRangeObj ( highlightId ) ;
220220 const rangeParagraphs = this . annotatableContainer . querySelectorAll ( `#${ highlightInfo . rangeParagraphIds . join ( ', #' ) } ` ) ;
221- const isReadOnly = ( options . drawingMode === 'inserted-marks' ) || highlightInfo . readOnly ;
222221 const wasDrawnAsReadOnly = this . annotatableContainer . querySelector ( `[data-highlight-id="${ highlightId } "][data-read-only]` ) ;
223222
224223 // Remove old highlight elements and styles
225- if ( ! wasDrawnAsReadOnly || ( wasDrawnAsReadOnly && ! isReadOnly ) ) undrawHighlight ( highlightInfo ) ;
224+ if ( ! wasDrawnAsReadOnly || ( wasDrawnAsReadOnly && ! highlightInfo . readOnly ) ) undrawHighlight ( highlightInfo ) ;
226225
227- if ( isReadOnly ) {
226+ if ( highlightInfo . readOnly || options . drawingMode === 'inserted-marks' ) {
228227 // Don't redraw a read-only highlight
229228 if ( wasDrawnAsReadOnly ) continue ;
230229
@@ -234,24 +233,31 @@ function Highlighter(options = hhDefaultOptions) {
234233 const textNodeIter = document . createNodeIterator ( range . commonAncestorContainer , NodeFilter . SHOW_TEXT ) ;
235234 const relevantTextNodes = [ ] ;
236235 while ( node = textNodeIter . nextNode ( ) ) {
237- if ( range . intersectsNode ( node ) && node !== range . startContainer && node . textContent !== '' && ! node . parentElement . closest ( 'rt' ) ) relevantTextNodes . push ( node ) ;
236+ if ( range . intersectsNode ( node ) && node !== range . startContainer && node . textContent !== '' && ! node . parentElement . closest ( 'rt' ) && node . parentElement . closest ( options . paragraphSelector ) ) relevantTextNodes . push ( node ) ;
238237 if ( node === range . endContainer ) break ;
239238 }
239+ const overlappingHighlightIds = new Set ( ) ;
240240 for ( let tn = 0 ; tn < relevantTextNodes . length ; tn ++ ) {
241241 const textNode = relevantTextNodes [ tn ] ;
242+ if ( textNode . parentElement . dataset . highlightId ) {
243+ overlappingHighlightIds . add ( textNode . parentElement . dataset . highlightId )
244+ }
242245 const styledMark = document . createElement ( 'mark' ) ;
243246 styledMark . dataset . highlightId = highlightId ;
244- styledMark . dataset . readOnly = '' ;
247+ if ( highlightInfo . readOnly ) styledMark . dataset . readOnly = '' ;
245248 styledMark . dataset . color = highlightInfo . color ;
246249 styledMark . dataset . style = highlightInfo . style ;
247250 if ( tn === 0 ) styledMark . dataset . start = '' ;
248251 if ( tn === relevantTextNodes . length - 1 ) styledMark . dataset . end = '' ;
249252 textNode . before ( styledMark ) ;
250253 styledMark . appendChild ( textNode ) ;
251254 }
252- rangeParagraphs . forEach ( p => { p . normalize ( ) ; } ) ;
253- // Update the highlight's stored range object (because the DOM changed)
255+
256+ // Update highlight ranges that were invalidated by the DOM change
254257 range = getCorrectedRangeObj ( highlightId ) ;
258+ for ( const overlappingHighlightId of overlappingHighlightIds ) {
259+ getCorrectedRangeObj ( overlappingHighlightId ) ;
260+ }
255261 } else {
256262 // Draw highlights with Custom Highlight API
257263 if ( options . drawingMode === 'highlight-api' && supportsHighlightApi ) {
@@ -286,7 +292,7 @@ function Highlighter(options = hhDefaultOptions) {
286292 }
287293
288294 // Update wrapper (for read-only highlights only)
289- if ( isReadOnly && ! wasDrawnAsReadOnly ) {
295+ if ( highlightInfo . readOnly && ! wasDrawnAsReadOnly ) {
290296 if ( highlightInfo . wrapper && ( options . wrappers [ highlightInfo . wrapper ] ?. start || options . wrappers [ highlightInfo . wrapper ] ?. end ) ) {
291297 const addWrapper = ( edge , range , htmlString ) => {
292298 htmlString = `<span class="hh-wrapper-${ edge } " data-highlight-id="${ highlightId } " data-color="${ highlightInfo . color } " data-style="${ highlightInfo . style } ">${ htmlString } </span>`
@@ -443,7 +449,10 @@ function Highlighter(options = hhDefaultOptions) {
443449 changes : appearanceChanges . concat ( boundsChanges ) ,
444450 }
445451
446- this . drawHighlights ( [ highlightId ] ) ;
452+ if ( highlightId !== activeHighlightId || options . drawingMode !== 'inserted-marks' ) {
453+ this . drawHighlights ( [ highlightId ] ) ;
454+ }
455+
447456 if ( highlightId === activeHighlightId && appearanceChanges . length > 0 ) {
448457 updateSelectionUi ( 'appearance' ) ;
449458 } else if ( triggeredByUserAction && highlightId !== activeHighlightId ) {
@@ -460,14 +469,25 @@ function Highlighter(options = hhDefaultOptions) {
460469 // Activate a highlight by ID
461470 this . activateHighlight = ( highlightId ) => {
462471 const highlightToActivate = highlightsById [ highlightId ] ;
463- if ( options . drawingMode === 'inserted-marks' || highlightToActivate . readOnly ) {
472+ if ( highlightToActivate . readOnly ) {
464473 // If the highlight is read-only, return events, but don't actually activate it
465474 this . annotatableContainer . dispatchEvent ( new CustomEvent ( 'hh:highlightactivate' , { detail : { highlight : highlightToActivate } } ) ) ;
466475 this . annotatableContainer . dispatchEvent ( new CustomEvent ( 'hh:highlightdeactivate' , { detail : { highlight : highlightToActivate } } ) ) ;
467476 return ;
468477 }
469478 const selection = window . getSelection ( ) ;
470479 const highlightRange = highlightToActivate . rangeObj . cloneRange ( ) ;
480+
481+ // Hide <mark> highlights and wrappers while the highlight is active. This prevents it from getting visually out of sync with the selection UI (mark highlights and wrappers aren't redrawn while the highlight is active, because DOM manipulation can make the selection UI unstable).
482+ for ( const element of this . annotatableContainer . querySelectorAll ( `[data-highlight-id="${ highlightId } "]:not(g)` ) ) {
483+ if ( element . tagName . toLowerCase ( ) === 'mark' ) {
484+ element . dataset . color = '' ;
485+ element . dataset . style = '' ;
486+ } else {
487+ element . style . display = 'none' ;
488+ }
489+ }
490+
471491 activeHighlightId = highlightId ;
472492 updateSelectionUi ( 'appearance' ) ;
473493 selection . setBaseAndExtent ( highlightRange . startContainer , highlightRange . startOffset , highlightRange . endContainer , highlightRange . endOffset ) ;
@@ -494,6 +514,9 @@ function Highlighter(options = hhDefaultOptions) {
494514 }
495515 updateSelectionUi ( 'appearance' ) ;
496516 if ( deactivatedHighlight ) {
517+ if ( options . drawingMode === 'inserted-marks' ) {
518+ this . drawHighlights ( [ deactivatedHighlight . highlightId ] ) ;
519+ }
497520 this . annotatableContainer . dispatchEvent ( new CustomEvent ( 'hh:highlightdeactivate' , { detail : {
498521 highlight : deactivatedHighlight ,
499522 } } ) ) ;
@@ -802,20 +825,46 @@ function Highlighter(options = hhDefaultOptions) {
802825 const undrawHighlight = ( highlightInfo ) => {
803826 const highlightId = highlightInfo . highlightId ;
804827
805- // Remove HTML and SVG elements
806- if ( document . querySelector ( '[data-highlight-id]' ) ) {
807- this . annotatableContainer . querySelectorAll ( `[data-highlight-id="${ highlightId } "]` ) . forEach ( element => {
808- if ( element . hasAttribute ( 'data-read-only' ) ) {
828+ // Remove <mark> highlights and HTML wrappers
829+ if ( this . annotatableContainer . querySelector ( `[data-highlight-id="${ highlightId } "]:not(g)` ) ) {
830+ const overlappingHighlightIds = new Set ( ) ;
831+ this . annotatableContainer . querySelectorAll ( `[data-highlight-id="${ highlightId } "]:not(g)` ) . forEach ( element => {
832+ if ( element . parentElement . dataset . highlightId ) {
833+ overlappingHighlightIds . add ( element . parentElement . dataset . highlightId ) ;
834+ }
835+ for ( const childHighlight of element . querySelectorAll ( '[data-highlight-id]' ) ) {
836+ overlappingHighlightIds . add ( childHighlight . dataset . highlightId ) ;
837+ }
838+ if ( element . tagName . toLowerCase ( ) === 'mark' ) {
809839 element . outerHTML = element . innerHTML ;
810840 } else {
811841 element . remove ( ) ;
812842 }
813843 } ) ;
814- const rangeParagraphs = this . annotatableContainer . querySelectorAll ( `#${ highlightInfo . rangeParagraphIds . join ( ', #' ) } ` ) ;
844+
845+ // Redraw overlapping highlights
846+ overlappingHighlightIds . delete ( highlightId ) ;
847+ if ( overlappingHighlightIds . size > 0 ) {
848+ for ( const overlappingHighlightId of overlappingHighlightIds ) {
849+ undrawHighlight ( highlightsById [ overlappingHighlightId ] ) ;
850+ }
851+ this . drawHighlights ( overlappingHighlightIds ) ;
852+ }
853+
854+ // Normalize text nodes
855+ const rangeParagraphs = this . annotatableContainer . querySelectorAll ( `#${ highlightInfo . startParagraphId } , #${ highlightInfo . endParagraphId } ` ) ;
815856 rangeParagraphs . forEach ( p => { p . normalize ( ) ; } ) ;
816857 getCorrectedRangeObj ( highlightId ) ;
817858 }
818859
860+ // Remove SVG highlights
861+ if ( svgBackground . querySelector ( `g[data-highlight-id="${ highlightId } "]` ) ) {
862+ const overlappingHighlightIds = new Set ( ) ;
863+ svgBackground . querySelectorAll ( `g[data-highlight-id="${ highlightId } "]` ) . forEach ( element => {
864+ element . remove ( ) ;
865+ } ) ;
866+ }
867+
819868 // Remove Highlight API highlights
820869 if ( supportsHighlightApi && CSS . highlights . has ( highlightId ) ) {
821870 const ruleIndexesToDelete = [ ] ;
0 commit comments