@readium/navigator-html-injectables 2.6.3 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{getCssSelector as i}from"@readium/shared";import{TextQuoteAnchor as l}from"../vendor/hypothesis/anchoring/types.js";function a(o){const t=o.tagName.toUpperCase();return t==="IMG"||t==="VIDEO"||t==="AUDIO"||t==="IFRAME"||t==="OBJECT"||t==="EMBED"||t==="CANVAS"}function g(o,t){try{const r=t.locations,f=t.text;if(f&&f.highlight){let e;r&&i(r)&&(e=o.querySelector(i(r))),e||(e=o.body);const n=new l(e,f.highlight,{prefix:f.before,suffix:f.after});try{return n.toRange()}catch{return console.warn("Quote not found:",n),null}}if(r){let e=null;if(!e&&i(r)&&(e=o.querySelector(i(r))),!e&&r.fragments){for(const n of r.fragments)if(e=o.getElementById(n),e)break}if(e){const n=o.createRange();return e.childNodes.length===0||a(e)?(n.selectNode(e),n):(n.setStartBefore(e),n.setEndAfter(e),n)}}}catch(r){console.error(r)}return null}export{g as rangeFromLocator};
1
+ import{getDomRange as u,getTextFragment as d,getCssSelector as f}from"@readium/shared";import{processTextFragmentDirective as g}from"@readium/helpers";import{TextQuoteAnchor as m}from"../vendor/hypothesis/anchoring/types.js";function h(n){const t=n.tagName.toUpperCase();return t==="IMG"||t==="VIDEO"||t==="AUDIO"||t==="IFRAME"||t==="OBJECT"||t==="EMBED"||t==="CANVAS"}function a(n,t){const r=t&&f(t);let s=null;if(r)try{s=n.querySelector(r)}catch(e){console.warn(`Invalid cssSelector: ${r}`,e)}return s??n.body??n.documentElement}function i(n,t){let r;try{r=n.querySelector(t.cssSelector)}catch(e){return console.error(`Invalid domRange cssSelector: ${t.cssSelector}`,e),null}if(!r)return console.error(`Can't resolve domRange cssSelector: ${t.cssSelector}`),null;let s=0;for(const e of r.childNodes)if(e.nodeType===Node.TEXT_NODE){if(s===t.textNodeIndex)return{node:e,offset:t.charOffset};s++}return console.error(`Can't resolve domRange textNodeIndex ${t.textNodeIndex} for selector: ${t.cssSelector}`),null}function R(n,t){try{const r=t.locations,s=t.text;if(r){const e=u(r);if(e){const o=i(n,e.start),c=e.end?i(n,e.end):o;if(o&&c)try{const l=n.createRange();return o.offset!==void 0?l.setStart(o.node,o.offset):l.setStartBefore(o.node),c.offset!==void 0?l.setEnd(c.node,c.offset):l.setEndBefore(c.node),l}catch(l){console.warn("Invalid domRange, falling back:",l)}}}if(r){const e=d(r);if(e){const o=a(n,r),c=g(e,n,o);if(c.length>0)return c[0]}}if(s&&s.highlight){const e=a(n,r),o=new m(e,s.highlight,{prefix:s.before,suffix:s.after});try{return o.toRange()}catch{return console.warn("Quote not found:",o),null}}if(r){let e=null;if(!e&&f(r)&&(e=n.querySelector(f(r))),!e&&r.fragments){for(const o of r.fragments)if(e=n.getElementById(o),e)break}if(e){const o=n.createRange();return e.childNodes.length===0||h(e)?(o.selectNode(e),o):(o.setStartBefore(e),o.setEndAfter(e),o)}}}catch(r){console.error(r)}return null}export{R as rangeFromLocator};
@@ -1,24 +1,24 @@
1
- import{Locator as P}from"@readium/shared";import{Module as N}from"./Module.js";import{rangeFromLocator as D}from"../helpers/locator.js";import{getClientRectsNoOverlap as $,rectContainsPoint as z,getTextClientRects as U}from"../helpers/rect.js";import{getProperty as I}from"../helpers/css.js";import{sML as j,adjustColorForContrast as R,getContrastingTextColor as O,colorToRgba as M,isDarkColor as V}from"@readium/helpers";import{makeWritingContext as H}from"../helpers/document.js";import{sanitizeHTML as q}from"../helpers/sanitize.js";function T(k){switch(k){case c.Mask:return"rgba(255, 255, 255, 0.5)";case c.Highlight:case c.HighlightUnderline:return"#FFFF00";default:return"#FF0000"}}const c={Highlight:"highlight",HighlightUnderline:"highlightUnderline",Underline:"underline",Strikethrough:"strikethrough",Outline:"outline",TextColor:"textColor",Mask:"mask",Template:"template"};var X=(k=>(k.Wrap="wrap",k.Viewport="viewport",k.Bounds="bounds",k.Page="page",k))(X||{}),G=(k=>(k.Boxes="boxes",k.Bounds="bounds",k))(G||{});const Y=()=>"Highlight"in window,F=["IMG","IMAGE","AUDIO","VIDEO","SVG"];class Z{constructor(t,i,e,s){this.wnd=t,this.comms=i,this.id=e,this.name=s,this.items=[],this.lastItemId=0,this.container=void 0,this._activatable=!1,this._hoverable=!1,this.hoveredItem=void 0,this.experimentalHighlights=!1,this._tintSubKeys=new Map,this._subKeyCounter=0,this.maskSvg=void 0,this.shadowHost=void 0,this.shadowRoot=void 0,this.currentRender=0,Y()&&(this.experimentalHighlights=!0,this.notTextFlag=new Map),this.activationHandler=this.handleActivation.bind(this),this.wnd.document.addEventListener("pointerup",this.activationHandler),this.hoverHandler=this.handleHover.bind(this),this.wnd.document.addEventListener("pointermove",this.hoverHandler)}get activatable(){return this._activatable}set activatable(t){this._activatable=t}get hoverable(){return this._hoverable}set hoverable(t){if(this._hoverable=t,!t&&this.hoveredItem){const i=this.hoveredItem.range.getBoundingClientRect(),e=this.wnd.devicePixelRatio;this.comms.send("decoration_pointer_leave",{decorationId:this.hoveredItem.decoration.id,group:this.name,rect:{top:i.top*e,left:i.left*e,width:i.width*e,height:i.height*e}}),this.hoveredItem=void 0}}add(t){const i=`${this.id}-${this.lastItemId++}`,e=D(this.wnd.document,t.locator);if(!e){this.comms.log("Can't locate DOM range for decoration",t);return}const s=e.commonAncestorContainer;if(s.nodeType!==Node.TEXT_NODE&&this.experimentalHighlights&&(F.includes(s.nodeName.toUpperCase())&&this.notTextFlag?.set(i,!0),e.cloneContents().querySelector(F.join(", ").toLowerCase())&&this.notTextFlag?.set(i,!0),(s.textContent?.trim()||"").length===0&&this.notTextFlag?.set(i,!0)),this.experimentalHighlights&&!this.notTextFlag?.has(i)){const r=a=>{for(;a&&a.nodeType===Node.ELEMENT_NODE;){if(a.namespaceURI?.includes("svg"))return!0;a=a.parentNode}return!1};(r(e.startContainer)||r(e.endContainer))&&this.notTextFlag?.set(i,!0)}if(this.experimentalHighlights){const{type:r}=t.style,{layout:a,width:g,expand:f}=t.style;r!==c.TextColor&&(r===c.Outline||r===c.Template||r===c.Mask||a!==void 0&&a!=="boxes"||g!==void 0&&g!=="wrap"||f)&&this.notTextFlag?.set(i,!0)}const o={decoration:t,id:i,range:e,hitRects:[],clickableElements:void 0,container:void 0};this.items.push(o),this.layout(o),o.hitRects=this.clientRectsToDocCoords($(o.range,!1,!1,(o.decoration.style.expand??0)+this.hitGap())),this.renderLayout([o])}remove(t){const i=this.items.findIndex(o=>o.decoration.id===t);if(i<0)return;const e=this.items[i],s=e.decoration.style?.type===c.Mask;if(this.items.splice(i,1),e.clickableElements=void 0,e.container&&(e.container.remove(),e.container=void 0),this.experimentalHighlights&&!this.notTextFlag?.has(e.id)&&e.highlightSubKey){const o=this.wnd.CSS.highlights;o.get(e.highlightSubKey)?.delete(e.range),this.items.some(a=>a.highlightSubKey===e.highlightSubKey)||o.delete(e.highlightSubKey);const r=this.wnd.document.getElementById(`${this.id}-style`);r&&this._rebuildHighlightStylesheet(r)}this.notTextFlag?.delete(e.id),this.hoveredItem===e&&(this.hoveredItem=void 0),s&&this.updateSharedMask()}update(t){this.remove(t.id),this.add(t)}clear(){this.clearContainer(),this.items.length=0,this.notTextFlag?.clear(),this.hoveredItem=void 0,this.maskSvg&&(this.maskSvg.remove(),this.maskSvg=void 0),this.shadowHost&&(this.shadowHost.remove(),this.shadowHost=void 0,this.shadowRoot=void 0)}destroy(){this.clear(),this.wnd.document.removeEventListener("pointerup",this.activationHandler),this.wnd.document.removeEventListener("pointermove",this.hoverHandler)}clientRectsToDocCoords(t){const i=H(this.wnd),e=i.xDocOffset,s=i.yDocOffset;return e===0&&s===0?t:t.map(o=>({left:o.left+e,top:o.top+s,right:o.right+e,bottom:o.bottom+s,width:o.width,height:o.height}))}pointerToDocCoords(t){const i=H(this.wnd);return{docX:t.clientX+i.xDocOffset,docY:t.clientY+i.yDocOffset}}effectiveZoom(){if(!j.UA.Blink)return 1;const t=parseFloat(this.wnd.getComputedStyle(this.wnd.document.documentElement).zoom),i=parseFloat(this.wnd.getComputedStyle(this.wnd.document.body).zoom);return(t||1)*(i||1)}hitGap(){return 2*this.effectiveZoom()}handleActivation(t){if(!this._activatable)return;const{docX:i,docY:e}=this.pointerToDocCoords(t),s=this.wnd.devicePixelRatio;for(const o of this.items){let r;if(o.decoration.style.type===c.Template)for(const a of o.clickableElements??[]){const g=a.getBoundingClientRect();if(z(g,t.clientX,t.clientY,0)){r=g;break}}else for(const a of o.hitRects)if(z(a,i,e,0)){r=o.range.getBoundingClientRect();break}if(r){this.comms.send("decoration_activated",{decorationId:o.decoration.id,group:this.name,rect:{top:r.top*s,left:r.left*s,width:r.width*s,height:r.height*s},point:{x:t.clientX*s,y:t.clientY*s}});return}}}handleHover(t){if(!this._hoverable)return;const{docX:i,docY:e}=this.pointerToDocCoords(t),s=this.wnd.devicePixelRatio;let o,r;for(const a of this.items){if(a.decoration.style.type===c.Template)for(const g of a.clickableElements??[]){const f=g.getBoundingClientRect();if(z(f,t.clientX,t.clientY,0)){o=a,r=f;break}}else for(const g of a.hitRects)if(z(g,i,e,0)){o=a,r=a.range.getBoundingClientRect();break}if(o)break}if(o!==this.hoveredItem){if(this.hoveredItem){const g=this.hoveredItem.range.commonAncestorContainer.isConnected?this.hoveredItem.range.getBoundingClientRect():null;this.comms.send("decoration_pointer_leave",{decorationId:this.hoveredItem.decoration.id,group:this.name,rect:g?{top:g.top*s,left:g.left*s,width:g.width*s,height:g.height*s}:void 0,point:{x:t.clientX*s,y:t.clientY*s}})}this.hoveredItem=o,o&&r&&this.comms.send("decoration_pointer_enter",{decorationId:o.decoration.id,group:this.name,rect:{top:r.top*s,left:r.left*s,width:r.width*s,height:r.height*s},point:{x:t.clientX*s,y:t.clientY*s}})}}requestLayout(){this.wnd.cancelAnimationFrame(this.currentRender),this.clearContainer(),this.wnd.document.fonts.ready.then(()=>{this.currentRender=this.wnd.requestAnimationFrame(()=>{this.items.forEach(t=>{this.layout(t),t.hitRects=this.clientRectsToDocCoords($(t.range,!1,!1,(t.decoration.style.expand??0)+this.hitGap()))}),this.renderLayout(this.items),this.updateSharedMask()})})}experimentalLayout(t){const i=this.requireContainer(!0),e=this.wnd.CSS.highlights,s=t.decoration.style,o=s.type??c.Highlight,r=s.tint??T(o),a=s.width,g=s.layout,f=this._getSubKey(o,r);t.highlightSubKey&&(e.get(t.highlightSubKey)?.delete(t.range),t.highlightSubKey!==f&&!this.items.some(l=>l!==t&&l.highlightSubKey===t.highlightSubKey)&&e.delete(t.highlightSubKey)),t.highlightSubKey=f;let w;e.has(f)?w=e.get(f):(w=new this.wnd.Highlight,e.set(f,w));const h=(n,l)=>this.wnd.document.caretPositionFromPoint?.(n,l)??null;if(o===c.TextColor&&(g==="bounds"||a==="bounds"||a==="page")){const n=H(this.wnd);if(n.isVertical)console.warn("Vertical writing detected: caretPositionFromPoint has known bugs, falling back to original range"),w.add(t.range);else{const l=t.range.getBoundingClientRect();let y,b;a==="page"?(y=Math.floor(n.inlineStart(l)/n.pageInlineSize)*n.pageInlineSize,b=n.pageInlineSize):(y=n.inlineStart(l),b=n.inlineSize(l));const v=h(y,n.blockStart(l)+1),S=h(y+b,n.blockStart(l)+n.blockSize(l)-1);if(v&&S){const x=this.wnd.document.createRange();x.setStart(v.offsetNode,v.offset),x.setEnd(S.offsetNode,S.offset),w.add(x),t.range=x}else w.add(t.range)}}else w.add(t.range);const d=this.getBackgroundColor(),m=s.enforceContrast!==!1?R(r,d):r;let p;switch(o){case c.Underline:p=`::highlight(${f}) {
1
+ import{Locator as D}from"@readium/shared";import{Module as P}from"./Module.js";import{rangeFromLocator as N}from"../helpers/locator.js";import{getClientRectsNoOverlap as T,rectContainsPoint as $,getTextClientRects as U}from"../helpers/rect.js";import{getProperty as I}from"../helpers/css.js";import{sML as j,adjustColorForContrast as E,getContrastingTextColor as O,colorToRgba as M,isDarkColor as q}from"@readium/helpers";import{makeWritingContext as R}from"../helpers/document.js";import{sanitizeHTML as V}from"../helpers/sanitize.js";function z(S){switch(S){case c.Mask:return"rgba(255, 255, 255, 0.5)";case c.Highlight:case c.HighlightUnderline:return"#FFFF00";default:return"#FF0000"}}const c={Highlight:"highlight",HighlightUnderline:"highlightUnderline",Underline:"underline",Strikethrough:"strikethrough",Outline:"outline",TextColor:"textColor",Mask:"mask",Template:"template"};var G=(S=>(S.Wrap="wrap",S.Viewport="viewport",S.Bounds="bounds",S.Page="page",S))(G||{}),X=(S=>(S.Boxes="boxes",S.Bounds="bounds",S))(X||{});const Z=()=>"Highlight"in window,F=["IMG","IMAGE","AUDIO","VIDEO","SVG"];class Y{constructor(e,t,i,o){this.wnd=e,this.comms=t,this.id=i,this.name=o,this.items=[],this.lastItemId=0,this.container=void 0,this._activatable=!1,this._hoverable=!1,this.hoveredItem=void 0,this.experimentalHighlights=!1,this._tintSubKeys=new Map,this._subKeyCounter=0,this.maskSvg=void 0,this.shadowHost=void 0,this.shadowRoot=void 0,this.currentRender=0,Z()&&(this.experimentalHighlights=!0,this.notTextFlag=new Map),this.activationHandler=this.handleActivation.bind(this),this.wnd.document.addEventListener("pointerup",this.activationHandler),this.hoverHandler=this.handleHover.bind(this),this.wnd.document.addEventListener("pointermove",this.hoverHandler)}get activatable(){return this._activatable}set activatable(e){this._activatable=e}get hoverable(){return this._hoverable}get hasOverlayItems(){return!this.experimentalHighlights||(this.notTextFlag?.size??0)>0}set hoverable(e){if(this._hoverable=e,!e&&this.hoveredItem){const t=this.hoveredItem.range.getBoundingClientRect(),i=this.wnd.devicePixelRatio;this.comms.send("decoration_pointer_leave",{decorationId:this.hoveredItem.decoration.id,group:this.name,rect:{top:t.top*i,left:t.left*i,width:t.width*i,height:t.height*i}}),this.hoveredItem=void 0}}add(e){const t=`${this.id}-${this.lastItemId++}`,i=N(this.wnd.document,e.locator);if(!i){this.comms.log("Can't locate DOM range for decoration",e);return}const o=i.commonAncestorContainer;if(o.nodeType!==Node.TEXT_NODE&&this.experimentalHighlights&&(F.includes(o.nodeName.toUpperCase())&&this.notTextFlag?.set(t,!0),i.cloneContents().querySelector(F.join(", ").toLowerCase())&&this.notTextFlag?.set(t,!0),(o.textContent?.trim()||"").length===0&&this.notTextFlag?.set(t,!0)),this.experimentalHighlights&&!this.notTextFlag?.has(t)){const n=r=>{for(;r&&r.nodeType===Node.ELEMENT_NODE;){if(r.namespaceURI?.includes("svg"))return!0;r=r.parentNode}return!1};(n(i.startContainer)||n(i.endContainer))&&this.notTextFlag?.set(t,!0)}if(this.experimentalHighlights){const{type:n}=e.style,{layout:r,width:p,expand:a}=e.style;n!==c.TextColor&&(n===c.Outline||n===c.Template||n===c.Mask||r!==void 0&&r!=="boxes"||p!==void 0&&p!=="wrap"||a)&&this.notTextFlag?.set(t,!0)}const s={decoration:e,id:t,range:i,hitRects:[],clickableElements:void 0,container:void 0};this.items.push(s),this.layout(s),s.hitRects=this.clientRectsToDocCoords(T(s.range,!1,!1,(s.decoration.style.expand??0)+this.hitGap())),this.renderLayout([s])}remove(e){const t=this.items.findIndex(s=>s.decoration.id===e);if(t<0)return;const i=this.items[t],o=i.decoration.style?.type===c.Mask;if(this.items.splice(t,1),i.clickableElements=void 0,i.container&&(i.container.remove(),i.container=void 0),this.experimentalHighlights&&!this.notTextFlag?.has(i.id)&&i.highlightSubKey){const s=this.wnd.CSS.highlights;s.get(i.highlightSubKey)?.delete(i.range),this.items.some(r=>r.highlightSubKey===i.highlightSubKey)||s.delete(i.highlightSubKey);const n=this.wnd.document.getElementById(`${this.id}-style`);n&&this._rebuildHighlightStylesheet(n)}this.notTextFlag?.delete(i.id),this.hoveredItem===i&&(this.hoveredItem=void 0),o&&this.updateSharedMask()}update(e){this.remove(e.id),this.add(e)}clear(){this.clearContainer(),this.items.length=0,this.notTextFlag?.clear(),this.hoveredItem=void 0,this.maskSvg&&(this.maskSvg.remove(),this.maskSvg=void 0),this.shadowHost&&(this.shadowHost.remove(),this.shadowHost=void 0,this.shadowRoot=void 0)}destroy(){this.clear(),this.wnd.document.removeEventListener("pointerup",this.activationHandler),this.wnd.document.removeEventListener("pointermove",this.hoverHandler)}clientRectsToDocCoords(e,t=R(this.wnd)){const i=t.xDocOffset,o=t.yDocOffset;return i===0&&o===0?e:e.map(s=>({left:s.left+i,top:s.top+o,right:s.right+i,bottom:s.bottom+o,width:s.width,height:s.height}))}pointerToDocCoords(e){const t=R(this.wnd);return{docX:e.clientX+t.xDocOffset,docY:e.clientY+t.yDocOffset}}effectiveZoom(){if(!j.UA.Blink)return 1;const e=parseFloat(this.wnd.getComputedStyle(this.wnd.document.documentElement).zoom),t=parseFloat(this.wnd.getComputedStyle(this.wnd.document.body).zoom);return(e||1)*(t||1)}hitGap(){return 2*this.effectiveZoom()}handleActivation(e){if(!this._activatable)return;const{docX:t,docY:i}=this.pointerToDocCoords(e),o=this.wnd.devicePixelRatio;for(const s of this.items){let n;if(s.decoration.style.type===c.Template)for(const r of s.clickableElements??[]){const p=r.getBoundingClientRect();if($(p,e.clientX,e.clientY,0)){n=p;break}}else for(const r of s.hitRects)if($(r,t,i,0)){n=s.range.getBoundingClientRect();break}if(n){this.comms.send("decoration_activated",{decorationId:s.decoration.id,group:this.name,rect:{top:n.top*o,left:n.left*o,width:n.width*o,height:n.height*o},point:{x:e.clientX*o,y:e.clientY*o}});return}}}handleHover(e){if(!this._hoverable)return;const{docX:t,docY:i}=this.pointerToDocCoords(e),o=this.wnd.devicePixelRatio;let s,n;for(const r of this.items){if(r.decoration.style.type===c.Template)for(const p of r.clickableElements??[]){const a=p.getBoundingClientRect();if($(a,e.clientX,e.clientY,0)){s=r,n=a;break}}else for(const p of r.hitRects)if($(p,t,i,0)){s=r,n=r.range.getBoundingClientRect();break}if(s)break}if(s!==this.hoveredItem){if(this.hoveredItem){const p=this.hoveredItem.range.commonAncestorContainer.isConnected?this.hoveredItem.range.getBoundingClientRect():null;this.comms.send("decoration_pointer_leave",{decorationId:this.hoveredItem.decoration.id,group:this.name,rect:p?{top:p.top*o,left:p.left*o,width:p.width*o,height:p.height*o}:void 0,point:{x:e.clientX*o,y:e.clientY*o}})}this.hoveredItem=s,s&&n&&this.comms.send("decoration_pointer_enter",{decorationId:s.decoration.id,group:this.name,rect:{top:n.top*o,left:n.left*o,width:n.width*o,height:n.height*o},point:{x:e.clientX*o,y:e.clientY*o}})}}requestLayout(){this.wnd.cancelAnimationFrame(this.currentRender),this.clearContainer(),this.wnd.document.fonts.ready.then(()=>{this.currentRender=this.wnd.requestAnimationFrame(()=>{this.items.forEach(e=>{this.layout(e),e.hitRects=this.clientRectsToDocCoords(T(e.range,!1,!1,(e.decoration.style.expand??0)+this.hitGap()))}),this.renderLayout(this.items),this.updateSharedMask()})})}repositionOverlays(){let e=!1;const t=[],i=R(this.wnd);this.items.forEach(o=>{if(!(this.experimentalHighlights&&!this.notTextFlag?.has(o.id))){if(o.decoration.style?.type===c.Mask){e=!0;return}this.repositionItem(o,i)||(o.container?.remove(),this.layout(o),t.push(o)),o.hitRects=this.clientRectsToDocCoords(T(o.range,!1,!1,(o.decoration.style.expand??0)+this.hitGap()),i)}}),t.length&&this.renderLayout(t),e&&this.updateSharedMask()}experimentalLayout(e){const t=this.requireContainer(!0),i=this.wnd.CSS.highlights,o=e.decoration.style,s=o.type??c.Highlight,n=o.tint??z(s),r=o.width,p=o.layout,a=this._getSubKey(s,n);e.highlightSubKey&&(i.get(e.highlightSubKey)?.delete(e.range),e.highlightSubKey!==a&&!this.items.some(f=>f!==e&&f.highlightSubKey===e.highlightSubKey)&&i.delete(e.highlightSubKey)),e.highlightSubKey=a;let g;i.has(a)?g=i.get(a):(g=new this.wnd.Highlight,i.set(a,g));const h=(d,f)=>this.wnd.document.caretPositionFromPoint?.(d,f)??null;if(s===c.TextColor&&(p==="bounds"||r==="bounds"||r==="page")){const d=R(this.wnd);if(d.isVertical)console.warn("Vertical writing detected: caretPositionFromPoint has known bugs, falling back to original range"),g.add(e.range);else{const f=e.range.getBoundingClientRect();let k,v;r==="page"?(k=Math.floor(d.inlineStart(f)/d.pageInlineSize)*d.pageInlineSize,v=d.pageInlineSize):(k=d.inlineStart(f),v=d.inlineSize(f));const y=h(k,d.blockStart(f)+1),w=h(k+v,d.blockStart(f)+d.blockSize(f)-1);if(y&&w){const C=this.wnd.document.createRange();C.setStart(y.offsetNode,y.offset),C.setEnd(w.offsetNode,w.offset),g.add(C),e.range=C}else g.add(e.range)}}else g.add(e.range);const l=this.getBackgroundColor(),b=o.enforceContrast!==!1?E(n,l):n;let m;switch(s){case c.Underline:m=`::highlight(${a}) {
2
2
  text-decoration: underline;
3
- text-decoration-color: ${m};
3
+ text-decoration-color: ${b};
4
4
  text-decoration-thickness: 0.1em;
5
- }`;break;case c.Strikethrough:p=`::highlight(${f}) {
5
+ }`;break;case c.Strikethrough:m=`::highlight(${a}) {
6
6
  text-decoration: line-through;
7
- text-decoration-color: ${m};
7
+ text-decoration-color: ${b};
8
8
  text-decoration-thickness: 0.1em;
9
- }`;break;case c.Outline:p=`::highlight(${f}) {
10
- outline: 2px solid ${m};
9
+ }`;break;case c.Outline:m=`::highlight(${a}) {
10
+ outline: 2px solid ${b};
11
11
  outline-offset: 1px;
12
- }`;break;case c.TextColor:p=`::highlight(${f}) {
13
- color: ${m};
14
- }`;break;case c.HighlightUnderline:{const{r:n,g:l,b:y}=M(m),b=`rgba(${n}, ${l}, ${y}, 0.3)`;p=`::highlight(${f}) {
15
- color: ${O(m,d)};
16
- background-color: ${b};
12
+ }`;break;case c.TextColor:m=`::highlight(${a}) {
13
+ color: ${b};
14
+ }`;break;case c.HighlightUnderline:{const{r:d,g:f,b:k}=M(b),v=`rgba(${d}, ${f}, ${k}, 0.3)`;m=`::highlight(${a}) {
15
+ color: ${O(b,l)};
16
+ background-color: ${v};
17
17
  text-decoration: underline;
18
- text-decoration-color: ${m};
18
+ text-decoration-color: ${b};
19
19
  text-decoration-thickness: 0.1em;
20
- }`;break}case c.Highlight:default:p=`::highlight(${f}) {
21
- color: ${O(m,d)};
22
- background-color: ${m};
23
- }`}t.highlightCSS=p,this._rebuildHighlightStylesheet(i)}_getSubKey(t,i){const e=`${t}::${i}`;let s=this._tintSubKeys.get(e);return s||(s=`${this.id}--${this._subKeyCounter++}`,this._tintSubKeys.set(e,s)),s}_rebuildHighlightStylesheet(t){const i=new Set,e=[];for(const s of this.items)s.highlightSubKey&&s.highlightCSS&&!i.has(s.highlightSubKey)&&(i.add(s.highlightSubKey),e.push(s.highlightCSS));t.innerHTML=e.join(`
24
- `)}layout(t){if(this.experimentalHighlights&&!this.notTextFlag?.has(t.id))return this.experimentalLayout(t);const i=this.wnd.document.createElement("div");i.setAttribute("id",t.id),i.dataset.highlightId=t.decoration.id,i.style.setProperty("pointer-events","none");const e=H(this.wnd),s=1/this.effectiveZoom(),o=t.decoration.style.expand??0,r=(h,d,u,m=0)=>{const p=t.decoration?.style?.width,n=d;switch(p){case"viewport":{const l=Math.floor(e.inlineStart(n)/e.viewportInlineSize)*e.viewportInlineSize;e.applyPosition(h,l+e.inlineScrollOffset+m,e.blockStart(n)+e.blockScrollOffset,e.viewportInlineSize-2*m,e.blockSize(n),s);break}case"page":{const l=Math.floor(e.inlineStart(n)/e.pageInlineSize)*e.pageInlineSize;e.applyPosition(h,l+e.inlineScrollOffset+m,e.blockStart(n)+e.blockScrollOffset,e.pageInlineSize-2*m,e.blockSize(n),s);break}case"bounds":{e.applyPosition(h,e.inlineStart(u)+e.inlineScrollOffset,e.blockStart(n)+e.blockScrollOffset,e.inlineSize(u),e.blockSize(n),s);break}default:e.applyPosition(h,e.inlineStart(n)+e.inlineScrollOffset,e.blockStart(n)+e.blockScrollOffset,e.inlineSize(n),e.blockSize(n),s)}},a=t.range.getBoundingClientRect(),g=t.decoration.style,f=(()=>{if(g.type!==c.Outline)return 0;const h=g.width;return h==="page"||h==="viewport"?3:0})();let w;if(g.type===c.Template){g.stylesheet&&this.injectCustomStylesheet(g.stylesheet);const h=q(this.wnd,g.element);if(!h){t.container=i,t.clickableElements=[];return}h.style.setProperty("pointer-events","none"),w=h}else{const h=g,d=h.type??c.Highlight,u=h.tint??T(d);if(d===c.TextColor){t.container=i,t.clickableElements=[];return}if(d===c.Mask){t.container=i,t.clickableElements=[],this.updateSharedMask();return}const m=this.getCurrentDarkMode(),p=this.getBackgroundColor(),n=h.enforceContrast!==!1,l=(()=>{switch(d){case c.Underline:{const v=n?R(u,p):u,S=h.layout==="bounds",[x,E]=e.isVertical?["border-right","border-left"]:["border-bottom","border-top"];return[S?`${E}: 0.1em solid ${v} !important`:null,`${x}: 0.1em solid ${v} !important`,"background-color: transparent !important","box-sizing: border-box !important"].filter(Boolean).join("; ")}case c.Strikethrough:{const v=n?R(u,p):u;return h.layout==="bounds"?[`background: repeating-linear-gradient(-45deg, transparent, transparent 19px, ${v} 19px, ${v} 20px) !important`,"background-color: transparent !important","box-sizing: border-box !important"].join("; "):[`background-color: ${v} !important`,"box-sizing: border-box !important"].join("; ")}case c.Outline:return[`outline: 2px solid ${n?R(u,p):u} !important`,"outline-offset: 1px !important","background-color: transparent !important","box-sizing: border-box !important"].join("; ");case c.HighlightUnderline:{const v=n?R(u,p):u,{r:S,g:x,b:E}=M(v),_=`rgba(${S}, ${x}, ${E}, 0.3)`,B=h.layout==="bounds",[K,L]=e.isVertical?["border-right","border-left"]:["border-bottom","border-top"];return[`background-color: ${_} !important`,B?`${L}: 0.1em solid ${v} !important`:null,`${K}: 0.1em solid ${v} !important`,"box-sizing: border-box !important"].filter(Boolean).join("; ")}case c.Highlight:default:return[`background-color: ${n?R(u,p):u} !important`,`mix-blend-mode: ${m?"exclusion":"multiply"} !important`,"opacity: 1 !important","box-sizing: border-box !important"].join("; ")}})(),y=this.wnd.document.createElement("template");y.innerHTML=`<div data-readium="true" class="readium-${d}" style="${l}"></div>`.trim(),w=y.content.firstElementChild}if(t.decoration?.style?.layout==="bounds"){const h=w.cloneNode(!0);h.style.setProperty("pointer-events","none");const d=o?{left:a.left-o,right:a.right+o,top:a.top-o,bottom:a.bottom+o,width:a.width+o*2,height:a.height+o*2}:a;r(h,d,a,f),i.append(h)}else{const h=g.type,d=h===c.Underline||h===c.Strikethrough,u=h===c.Strikethrough,m=d?U(t.range,["rt","rp"]):t.range;let p=$(m,!0,e.isVertical,d?0:o);p=p.sort((n,l)=>e.isVertical?(e.isVertLR?1:-1)*(n.left-l.left):n.top-l.top);for(let n of p){const l=w.cloneNode(!0);l.style.setProperty("pointer-events","none");let y=n;if(u){const b=e.blockSize(n)*.1,S=e.blockStart(n)+e.blockSize(n)/2-b/2;y=e.isVertical?{left:S,right:S+b,top:n.top,bottom:n.bottom,width:b,height:n.height}:{top:S,bottom:S+b,left:n.left,right:n.right,height:b,width:n.width}}o&&d&&(y=e.isVertical?{...y,top:y.top-o,bottom:y.bottom+o,height:y.height+o*2}:{...y,left:y.left-o,right:y.right+o,width:y.width+o*2}),r(l,y,a,f),i.append(l)}}t.container=i,t.clickableElements=Array.from(i.querySelectorAll("[data-activable='1']")),t.clickableElements.length||(t.clickableElements=Array.from(i.children))}renderLayout(t){this.wnd.cancelAnimationFrame(this.currentRender),this.currentRender=this.wnd.requestAnimationFrame(()=>{if(t=t.filter(e=>!this.experimentalHighlights||!!this.notTextFlag?.has(e.id)),!t||t.length===0)return;this.requireContainer().append(...t.map(e=>e.container).filter(e=>!!e))})}requireContainer(t=!1){if(t){let i;return this.wnd.document.getElementById(`${this.id}-style`)?i=this.wnd.document.getElementById(`${this.id}-style`):(i=this.wnd.document.createElement("style"),i.dataset.readium="true",i.id=`${this.id}-style`,this.wnd.document.head.appendChild(i)),i}return this.container||(this.shadowRoot||(this.shadowHost=this.wnd.document.createElement("div"),this.shadowHost.style.cssText="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none",this.wnd.document.body.appendChild(this.shadowHost),this.shadowRoot=this.shadowHost.attachShadow({mode:"open"})),this.container=this.wnd.document.createElement("div"),this.container.setAttribute("id",this.id),this.container.dataset.group=this.name,this.container.dataset.readium="true",this.container.style.setProperty("pointer-events","none"),this.container.style.display="contents",this.shadowRoot.appendChild(this.container)),this.container}getCurrentDarkMode(){return I(this.wnd,"--USER__appearance")==="readium-night-on"||V(this.getBackgroundColor())}getBackgroundColor(){return I(this.wnd,"--USER__backgroundColor")||this.wnd.getComputedStyle(this.wnd.document.documentElement).getPropertyValue("background-color")}updateSharedMask(){const t=this.items.filter(d=>d.decoration.style?.type===c.Mask);if(t.length===0){this.maskSvg&&(this.maskSvg.remove(),this.maskSvg=void 0),this.shadowRoot&&(this.shadowRoot.innerHTML="");return}const i=H(this.wnd),e=1/this.effectiveZoom(),s=this.wnd.document.documentElement,o=s.scrollWidth,r=s.scrollHeight,a=[];for(const d of t){const u=d.decoration.style,m=u.layout??"boxes",p=u.width??"wrap",n=u.expand??0,l=d.range.getBoundingClientRect(),y=m==="bounds"?[n?{left:l.left-n,top:l.top-n,right:l.right+n,bottom:l.bottom+n,width:l.width+n*2,height:l.height+n*2}:l]:$(d.range,!1,!1,n);for(const b of y){let v;switch(p){case"viewport":{const S=Math.floor(i.inlineStart(b)/i.viewportInlineSize)*i.viewportInlineSize;v=i.toRect(S,i.blockStart(b),i.viewportInlineSize,i.blockSize(b));break}case"page":{const S=Math.floor(i.inlineStart(b)/i.pageInlineSize)*i.pageInlineSize;v=i.toRect(S,i.blockStart(b),i.pageInlineSize,i.blockSize(b));break}case"bounds":{v=i.toRect(i.inlineStart(l),i.blockStart(b),i.inlineSize(l),i.blockSize(b));break}default:v=i.toRect(i.inlineStart(b),i.blockStart(b),i.inlineSize(b),i.blockSize(b))}a.push(v)}}const g=[`M0 0 H${o} V${r} H0 Z`,...a.map(d=>{const u=(d.left+i.xDocOffset)*e,m=(d.top+i.yDocOffset)*e,p=(d.right+i.xDocOffset)*e,n=(d.bottom+i.yDocOffset)*e;return`M${u} ${m} H${p} V${n} H${u} Z`})].join(" "),f="http://www.w3.org/2000/svg";if(!this.maskSvg){this.shadowRoot||(this.shadowHost=this.wnd.document.createElement("div"),this.shadowHost.style.cssText="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none",this.wnd.document.body.appendChild(this.shadowHost),this.shadowRoot=this.shadowHost.attachShadow({mode:"open"})),this.maskSvg=this.wnd.document.createElementNS(f,"svg"),this.maskSvg.style.cssText=`position:absolute;top:0;left:0;width:${o}px;height:${r}px;pointer-events:none;z-index:9999`,this.maskSvg.dataset.readium="true";const d=this.wnd.document.createElementNS(f,"defs"),u=this.wnd.document.createElementNS(f,"clipPath"),m=`${this.id}-mask-clip`;u.setAttribute("id",m),u.setAttribute("clipPathUnits","userSpaceOnUse");const p=this.wnd.document.createElementNS(f,"path");p.setAttribute("clip-rule","evenodd"),u.appendChild(p),d.appendChild(u),this.maskSvg.appendChild(d);const n=this.wnd.document.createElementNS(f,"rect");n.setAttribute("id",`${this.id}-mask-rect`),n.setAttribute("clip-path",`url(#${m})`),n.style.pointerEvents="none",this.maskSvg.appendChild(n),this.shadowRoot.appendChild(this.maskSvg)}this.maskSvg.style.width=`${o}px`,this.maskSvg.style.height=`${r}px`;const w=this.maskSvg.querySelector("path");w&&w.setAttribute("d",g);const h=this.maskSvg.querySelector("rect");if(h){const u=t[0].decoration.style.tint,m=u??this.getBackgroundColor()??T(c.Mask),p=u?"1":"0.5";h.setAttribute("x","0"),h.setAttribute("y","0"),h.setAttribute("width",String(o)),h.setAttribute("height",String(r)),h.setAttribute("fill",m),h.setAttribute("fill-opacity",p)}}injectCustomStylesheet(t){const i=`${this.id}-custom-style`;let e=this.wnd.document.getElementById(i);e||(e=this.wnd.document.createElement("style"),e.id=i,e.dataset.readium="true",this.wnd.document.head.appendChild(e)),e.innerHTML=t}clearContainer(){if(this.experimentalHighlights){const t=this.wnd.CSS.highlights;for(const i of this._tintSubKeys.values())t.delete(i);this._tintSubKeys.clear(),this._subKeyCounter=0}this.wnd.document.getElementById(`${this.id}-custom-style`)?.remove(),this.container&&(this.container.remove(),this.container=void 0)}}const C=class C extends N{constructor(){super(...arguments),this.resizeFrame=0,this.lastGroupId=0,this.groups=new Map,this.handleResizer=this.handleResize.bind(this)}cleanup(){this.groups.forEach(t=>t.destroy()),this.groups.clear()}updateHighlightStyles(){this.groups.forEach(t=>{t.requestLayout()})}handleResize(){this.wnd.clearTimeout(this.resizeFrame),this.resizeFrame=this.wnd.setTimeout(()=>{this.groups.forEach(t=>{t.experimentalHighlights||t.requestLayout()})},50)}mount(t,i){return this.wnd=t,i.register("decorate",C.moduleName,(e,s)=>{const o=e;(o.action==="add"||o.action==="update")&&o.decoration.locator&&(o.decoration.locator=P.deserialize(o.decoration.locator)),this.groups.has(o.group)||this.groups.set(o.group,new Z(t,i,`readium-decoration-${this.lastGroupId++}`,o.group));const r=this.groups.get(o.group);switch(o.action){case"add":r?.add(o.decoration);break;case"remove":r?.remove(o.decoration.id);break;case"clear":r?.clear();break;case"update":r?.update(o.decoration);break}s(!0)}),i.register("decoration_activatable",C.moduleName,(e,s)=>{const o=e,r=this.groups.get(o.group);r&&(r.activatable=o.activatable),s(!0)}),i.register("decoration_hoverable",C.moduleName,(e,s)=>{const o=e,r=this.groups.get(o.group);r&&(r.hoverable=o.hoverable),s(!0)}),this.resizeObserver=new ResizeObserver(()=>t.requestAnimationFrame(()=>this.handleResize())),this.resizeObserver.observe(t.document.documentElement),t.addEventListener("orientationchange",this.handleResizer),t.addEventListener("resize",this.handleResizer),this.styleObserver=new MutationObserver(e=>{e.some(o=>o.type==="attributes"&&o.attributeName==="style"&&o.oldValue!==o.target.getAttribute("style"))&&this.updateHighlightStyles()}),this.styleObserver.observe(t.document.documentElement,{attributes:!0,attributeFilter:["style"],attributeOldValue:!0}),i.log("Decorator Mounted"),!0}unmount(t,i){return t.removeEventListener("orientationchange",this.handleResizer),t.removeEventListener("resize",this.handleResizer),i.unregisterAll(C.moduleName),this.resizeObserver.disconnect(),this.styleObserver.disconnect(),this.cleanup(),i.log("Decorator Unmounted"),!0}};C.moduleName="decorator";let A=C;export{G as DecorationLayout,c as DecorationStyleType,X as DecorationWidth,A as Decorator};
20
+ }`;break}case c.Highlight:default:m=`::highlight(${a}) {
21
+ color: ${O(b,l)};
22
+ background-color: ${b};
23
+ }`}e.highlightCSS=m,this._rebuildHighlightStylesheet(t)}_getSubKey(e,t){const i=`${e}::${t}`;let o=this._tintSubKeys.get(i);return o||(o=`${this.id}--${this._subKeyCounter++}`,this._tintSubKeys.set(i,o)),o}_rebuildHighlightStylesheet(e){const t=new Set,i=[];for(const o of this.items)o.highlightSubKey&&o.highlightCSS&&!t.has(o.highlightSubKey)&&(t.add(o.highlightSubKey),i.push(o.highlightCSS));e.innerHTML=i.join(`
24
+ `)}positionElement(e,t,i,o,s,n,r=0){const p=e.decoration?.style?.width,a=s;switch(p){case"viewport":{const g=Math.floor(t.inlineStart(a)/t.viewportInlineSize)*t.viewportInlineSize;t.applyPosition(o,g+t.inlineScrollOffset+r,t.blockStart(a)+t.blockScrollOffset,t.viewportInlineSize-2*r,t.blockSize(a),i);break}case"page":{const g=Math.floor(t.inlineStart(a)/t.pageInlineSize)*t.pageInlineSize;t.applyPosition(o,g+t.inlineScrollOffset+r,t.blockStart(a)+t.blockScrollOffset,t.pageInlineSize-2*r,t.blockSize(a),i);break}case"bounds":{t.applyPosition(o,t.inlineStart(n)+t.inlineScrollOffset,t.blockStart(a)+t.blockScrollOffset,t.inlineSize(n),t.blockSize(a),i);break}default:t.applyPosition(o,t.inlineStart(a)+t.inlineScrollOffset,t.blockStart(a)+t.blockScrollOffset,t.inlineSize(a),t.blockSize(a),i)}}computeOverlayRects(e,t,i,o){if(e.decoration?.style?.layout==="bounds")return[o?{left:i.left-o,right:i.right+o,top:i.top-o,bottom:i.bottom+o,width:i.width+o*2,height:i.height+o*2}:i];const n=e.decoration.style.type,r=n===c.Underline||n===c.Strikethrough,p=n===c.Strikethrough,a=r?U(e.range,["rt","rp"]):e.range;let g=T(a,!0,t.isVertical,r?0:o);return g=g.sort((h,l)=>t.isVertical?(t.isVertLR?1:-1)*(h.left-l.left):h.top-l.top),g.map(h=>{let l=h;if(p){const u=t.blockSize(h)*.1,m=t.blockStart(h)+t.blockSize(h)/2-u/2;l=t.isVertical?{left:m,right:m+u,top:h.top,bottom:h.bottom,width:u,height:h.height}:{top:m,bottom:m+u,left:h.left,right:h.right,height:u,width:h.width}}return o&&r&&(l=t.isVertical?{...l,top:l.top-o,bottom:l.bottom+o,height:l.height+o*2}:{...l,left:l.left-o,right:l.right+o,width:l.width+o*2}),l})}repositionItem(e,t){if(!e.container)return!0;const i=e.decoration.style;if(i.type!==c.Template){const g=i.type??c.Highlight;if(g===c.TextColor||g===c.Mask)return!0}const o=1/this.effectiveZoom(),s=i.expand??0,n=e.range.getBoundingClientRect(),r=(()=>{if(i.type!==c.Outline)return 0;const g=i.width;return g==="page"||g==="viewport"?3:0})(),p=this.computeOverlayRects(e,t,n,s),a=Array.from(e.container.children);return a.length!==p.length?!1:(a.forEach((g,h)=>this.positionElement(e,t,o,g,p[h],n,r)),!0)}layout(e){if(this.experimentalHighlights&&!this.notTextFlag?.has(e.id))return this.experimentalLayout(e);const t=this.wnd.document.createElement("div");t.setAttribute("id",e.id),t.dataset.highlightId=e.decoration.id,t.style.setProperty("pointer-events","none");const i=R(this.wnd),o=1/this.effectiveZoom(),s=e.decoration.style.expand??0,n=e.range.getBoundingClientRect(),r=e.decoration.style,p=(()=>{if(r.type!==c.Outline)return 0;const h=r.width;return h==="page"||h==="viewport"?3:0})();let a;if(r.type===c.Template){r.stylesheet&&this.injectCustomStylesheet(r.stylesheet);const h=V(this.wnd,r.element);if(!h){e.container=t,e.clickableElements=[];return}h.style.setProperty("pointer-events","none"),a=h}else{const h=r,l=h.type??c.Highlight,u=h.tint??z(l);if(l===c.TextColor){e.container=t,e.clickableElements=[];return}if(l===c.Mask){e.container=t,e.clickableElements=[],this.updateSharedMask();return}const b=this.getCurrentDarkMode(),m=this.getBackgroundColor(),d=h.enforceContrast!==!1,f=(()=>{switch(l){case c.Underline:{const y=d?E(u,m):u,w=h.layout==="bounds",[C,x]=i.isVertical?["border-right","border-left"]:["border-bottom","border-top"];return[w?`${x}: 0.1em solid ${y} !important`:null,`${C}: 0.1em solid ${y} !important`,"background-color: transparent !important","box-sizing: border-box !important"].filter(Boolean).join("; ")}case c.Strikethrough:{const y=d?E(u,m):u;return h.layout==="bounds"?[`background: repeating-linear-gradient(-45deg, transparent, transparent 19px, ${y} 19px, ${y} 20px) !important`,"background-color: transparent !important","box-sizing: border-box !important"].join("; "):[`background-color: ${y} !important`,"box-sizing: border-box !important"].join("; ")}case c.Outline:return[`outline: 2px solid ${d?E(u,m):u} !important`,"outline-offset: 1px !important","background-color: transparent !important","box-sizing: border-box !important"].join("; ");case c.HighlightUnderline:{const y=d?E(u,m):u,{r:w,g:C,b:x}=M(y),_=`rgba(${w}, ${C}, ${x}, 0.3)`,B=h.layout==="bounds",[L,K]=i.isVertical?["border-right","border-left"]:["border-bottom","border-top"];return[`background-color: ${_} !important`,B?`${K}: 0.1em solid ${y} !important`:null,`${L}: 0.1em solid ${y} !important`,"box-sizing: border-box !important"].filter(Boolean).join("; ")}case c.Highlight:default:return[`background-color: ${d?E(u,m):u} !important`,`mix-blend-mode: ${b?"exclusion":"multiply"} !important`,"opacity: 1 !important","box-sizing: border-box !important","transform: translateZ(0) !important"].join("; ")}})(),k=this.wnd.document.createElement("template");k.innerHTML=`<div data-readium="true" class="readium-${l}" style="${f}"></div>`.trim(),a=k.content.firstElementChild}const g=this.computeOverlayRects(e,i,n,s);for(const h of g){const l=a.cloneNode(!0);l.style.setProperty("pointer-events","none"),this.positionElement(e,i,o,l,h,n,p),t.append(l)}e.container=t,e.clickableElements=Array.from(t.querySelectorAll("[data-activable='1']")),e.clickableElements.length||(e.clickableElements=Array.from(t.children))}renderLayout(e){this.wnd.cancelAnimationFrame(this.currentRender),this.currentRender=this.wnd.requestAnimationFrame(()=>{if(e=e.filter(i=>!this.experimentalHighlights||!!this.notTextFlag?.has(i.id)),!e||e.length===0)return;this.requireContainer().append(...e.map(i=>i.container).filter(i=>!!i))})}requireContainer(e=!1){if(e){let t;return this.wnd.document.getElementById(`${this.id}-style`)?t=this.wnd.document.getElementById(`${this.id}-style`):(t=this.wnd.document.createElement("style"),t.dataset.readium="true",t.id=`${this.id}-style`,this.wnd.document.head.appendChild(t)),t}return this.container||(this.shadowRoot||(this.shadowHost=this.wnd.document.createElement("div"),this.shadowHost.style.cssText="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none",this.wnd.document.body.appendChild(this.shadowHost),this.shadowRoot=this.shadowHost.attachShadow({mode:"open"})),this.container=this.wnd.document.createElement("div"),this.container.setAttribute("id",this.id),this.container.dataset.group=this.name,this.container.dataset.readium="true",this.container.style.setProperty("pointer-events","none"),this.container.style.display="contents",this.shadowRoot.appendChild(this.container)),this.container}getCurrentDarkMode(){return I(this.wnd,"--USER__appearance")==="readium-night-on"||q(this.getBackgroundColor())}getBackgroundColor(){return I(this.wnd,"--USER__backgroundColor")||this.wnd.getComputedStyle(this.wnd.document.documentElement).getPropertyValue("background-color")}updateSharedMask(){const e=this.items.filter(l=>l.decoration.style?.type===c.Mask);if(e.length===0){this.maskSvg&&(this.maskSvg.remove(),this.maskSvg=void 0),this.shadowRoot&&(this.shadowRoot.innerHTML="");return}const t=R(this.wnd),i=1/this.effectiveZoom(),o=this.wnd.document.documentElement,s=o.scrollWidth,n=o.scrollHeight,r=[];for(const l of e){const u=l.decoration.style,b=u.layout??"boxes",m=u.width??"wrap",d=u.expand??0,f=l.range.getBoundingClientRect(),k=b==="bounds"?[d?{left:f.left-d,top:f.top-d,right:f.right+d,bottom:f.bottom+d,width:f.width+d*2,height:f.height+d*2}:f]:T(l.range,!1,!1,d);for(const v of k){let y;switch(m){case"viewport":{const w=Math.floor(t.inlineStart(v)/t.viewportInlineSize)*t.viewportInlineSize;y=t.toRect(w,t.blockStart(v),t.viewportInlineSize,t.blockSize(v));break}case"page":{const w=Math.floor(t.inlineStart(v)/t.pageInlineSize)*t.pageInlineSize;y=t.toRect(w,t.blockStart(v),t.pageInlineSize,t.blockSize(v));break}case"bounds":{y=t.toRect(t.inlineStart(f),t.blockStart(v),t.inlineSize(f),t.blockSize(v));break}default:y=t.toRect(t.inlineStart(v),t.blockStart(v),t.inlineSize(v),t.blockSize(v))}r.push(y)}}const p=[`M0 0 H${s} V${n} H0 Z`,...r.map(l=>{const u=(l.left+t.xDocOffset)*i,b=(l.top+t.yDocOffset)*i,m=(l.right+t.xDocOffset)*i,d=(l.bottom+t.yDocOffset)*i;return`M${u} ${b} H${m} V${d} H${u} Z`})].join(" "),a="http://www.w3.org/2000/svg";if(!this.maskSvg){this.shadowRoot||(this.shadowHost=this.wnd.document.createElement("div"),this.shadowHost.style.cssText="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none",this.wnd.document.body.appendChild(this.shadowHost),this.shadowRoot=this.shadowHost.attachShadow({mode:"open"})),this.maskSvg=this.wnd.document.createElementNS(a,"svg"),this.maskSvg.style.cssText=`position:absolute;top:0;left:0;width:${s}px;height:${n}px;pointer-events:none;z-index:9999`,this.maskSvg.dataset.readium="true";const l=this.wnd.document.createElementNS(a,"defs"),u=this.wnd.document.createElementNS(a,"clipPath"),b=`${this.id}-mask-clip`;u.setAttribute("id",b),u.setAttribute("clipPathUnits","userSpaceOnUse");const m=this.wnd.document.createElementNS(a,"path");m.setAttribute("clip-rule","evenodd"),u.appendChild(m),l.appendChild(u),this.maskSvg.appendChild(l);const d=this.wnd.document.createElementNS(a,"rect");d.setAttribute("id",`${this.id}-mask-rect`),d.setAttribute("clip-path",`url(#${b})`),d.style.pointerEvents="none",this.maskSvg.appendChild(d),this.shadowRoot.appendChild(this.maskSvg)}this.maskSvg.style.width=`${s}px`,this.maskSvg.style.height=`${n}px`;const g=this.maskSvg.querySelector("path");g&&g.setAttribute("d",p);const h=this.maskSvg.querySelector("rect");if(h){const u=e[0].decoration.style.tint,b=u??this.getBackgroundColor()??z(c.Mask),m=u?"1":"0.5";h.setAttribute("x","0"),h.setAttribute("y","0"),h.setAttribute("width",String(s)),h.setAttribute("height",String(n)),h.setAttribute("fill",b),h.setAttribute("fill-opacity",m)}}injectCustomStylesheet(e){const t=`${this.id}-custom-style`;let i=this.wnd.document.getElementById(t);i||(i=this.wnd.document.createElement("style"),i.id=t,i.dataset.readium="true",this.wnd.document.head.appendChild(i)),i.innerHTML=e}clearContainer(){if(this.experimentalHighlights){const e=this.wnd.CSS.highlights;for(const t of this._tintSubKeys.values())e.delete(t);this._tintSubKeys.clear(),this._subKeyCounter=0}this.wnd.document.getElementById(`${this.id}-custom-style`)?.remove(),this.container&&(this.container.remove(),this.container=void 0)}}const H=class H extends P{constructor(){super(...arguments),this.resizeFrame=0,this.lastGroupId=0,this.groups=new Map,this.handleResizer=this.handleResize.bind(this),this.scrollScheduled=!1,this.handleScroller=this.handleScroll.bind(this)}cleanup(){this.groups.forEach(e=>e.destroy()),this.groups.clear()}updateHighlightStyles(){this.groups.forEach(e=>{e.requestLayout()})}handleResize(){this.wnd.clearTimeout(this.resizeFrame),this.resizeFrame=this.wnd.setTimeout(()=>{this.groups.forEach(e=>e.requestLayout())},50)}handleScroll(){this.scrollScheduled||(this.scrollScheduled=!0,this.wnd.requestAnimationFrame(()=>{this.scrollScheduled=!1,this.groups.forEach(e=>{e.hasOverlayItems&&e.repositionOverlays()})}))}mount(e,t){return this.wnd=e,t.register("decorate",H.moduleName,(i,o)=>{const s=i;(s.action==="add"||s.action==="update")&&s.decoration.locator&&(s.decoration.locator=D.deserialize(s.decoration.locator)),this.groups.has(s.group)||this.groups.set(s.group,new Y(e,t,`readium-decoration-${this.lastGroupId++}`,s.group));const n=this.groups.get(s.group);switch(s.action){case"add":n?.add(s.decoration);break;case"remove":n?.remove(s.decoration.id);break;case"clear":n?.clear();break;case"update":n?.update(s.decoration);break}o(!0)}),t.register("decoration_activatable",H.moduleName,(i,o)=>{const s=i,n=this.groups.get(s.group);n&&(n.activatable=s.activatable),o(!0)}),t.register("decoration_hoverable",H.moduleName,(i,o)=>{const s=i,n=this.groups.get(s.group);n&&(n.hoverable=s.hoverable),o(!0)}),this.resizeObserver=new ResizeObserver(()=>e.requestAnimationFrame(()=>this.handleResize())),this.resizeObserver.observe(e.document.documentElement),e.addEventListener("orientationchange",this.handleResizer),e.addEventListener("resize",this.handleResizer),e.addEventListener("scroll",this.handleScroller,{passive:!0,capture:!0}),this.styleObserver=new MutationObserver(i=>{i.some(s=>s.type==="attributes"&&s.attributeName==="style"&&s.oldValue!==s.target.getAttribute("style"))&&this.updateHighlightStyles()}),this.styleObserver.observe(e.document.documentElement,{attributes:!0,attributeFilter:["style"],attributeOldValue:!0}),t.log("Decorator Mounted"),!0}unmount(e,t){return e.removeEventListener("orientationchange",this.handleResizer),e.removeEventListener("resize",this.handleResizer),e.removeEventListener("scroll",this.handleScroller,{capture:!0}),t.unregisterAll(H.moduleName),this.resizeObserver.disconnect(),this.styleObserver.disconnect(),this.cleanup(),t.log("Decorator Unmounted"),!0}};H.moduleName="decorator";let A=H;export{X as DecorationLayout,c as DecorationStyleType,G as DecorationWidth,A as Decorator};
@@ -1 +1 @@
1
- import{Module as l}from"../Module.js";const i=class i extends l{constructor(){super(...arguments),this.mediaPlayingCount=0,this.allAnimations=new Set}wndOnErr(e){this.comms?.send("error",{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno})}unblock(e){for(e._readium_blockEvents=!1;e._readium_blockedEvents?.length>0;){const t=e._readium_blockedEvents.shift();switch(t[0]){case 0:Reflect.apply(t[1],t[2],t[3]);break;case 1:const a=t[1],n=t[2];e.removeEventListener(a.type,e._readium_eventBlocker,!0);const s=new Event(a.type,{bubbles:a.bubbles,cancelable:a.cancelable});n?n.dispatchEvent(s):e.dispatchEvent(s);break}}}onMediaPlayEvent(){this.mediaPlayingCount++,this.comms?.send("media_play",this.mediaPlayingCount)}onMediaPauseEvent(){this.mediaPlayingCount>0&&this.mediaPlayingCount--,this.comms?.send("media_pause",this.mediaPlayingCount)}pauseAllMedia(e){const t=e.document.querySelectorAll("audio,video");for(let a=0;a<t.length;a++)t[a].pause()}mount(e,t){this.comms=t,e.addEventListener("error",this.wndOnErr,!1),Reflect.defineProperty(e.navigator,"epubReadingSystem",{value:{name:"readium-ts-toolkit",version:"2.6.3",hasFeature:(n,s="")=>{switch(n){case"dom-manipulation":return!0;case"layout-changes":return!0;case"touch-events":return!0;case"mouse-events":return!0;case"keyboard-events":return!0;case"spine-scripting":return!0;case"embedded-web-content":return!0;default:return!1}}},writable:!1}),"getAnimations"in e.document&&e.document.getAnimations().forEach(n=>{n.cancel(),this.allAnimations.add(n)}),t.register("activate",i.moduleName,(n,s)=>{this.allAnimations.forEach(r=>{r.cancel(),r.play()}),s(!0)}),t.register("unfocus",i.moduleName,(n,s)=>{this.pauseAllMedia(e),this.allAnimations.forEach(r=>r.pause()),s(!0)});const a=e.document.querySelectorAll("audio,video");for(let n=0;n<a.length;n++){const s=a[n];s.addEventListener("play",this.onMediaPlayEvent,{passive:!0}),s.addEventListener("pause",this.onMediaPauseEvent,{passive:!0})}return t.log("Setup Mounted"),!0}unmount(e,t){return e.removeEventListener("error",this.wndOnErr),e.removeEventListener("play",this.onMediaPlayEvent),e.removeEventListener("pause",this.onMediaPauseEvent),this.allAnimations.forEach(a=>a.cancel()),this.allAnimations.clear(),t.log("Setup Unmounted"),!0}};i.moduleName="setup";let o=i;export{o as Setup};
1
+ import{Module as l}from"../Module.js";const i=class i extends l{constructor(){super(...arguments),this.mediaPlayingCount=0,this.allAnimations=new Set}wndOnErr(e){this.comms?.send("error",{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno})}unblock(e){for(e._readium_blockEvents=!1;e._readium_blockedEvents?.length>0;){const t=e._readium_blockedEvents.shift();switch(t[0]){case 0:Reflect.apply(t[1],t[2],t[3]);break;case 1:const a=t[1],n=t[2];e.removeEventListener(a.type,e._readium_eventBlocker,!0);const s=new Event(a.type,{bubbles:a.bubbles,cancelable:a.cancelable});n?n.dispatchEvent(s):e.dispatchEvent(s);break}}}onMediaPlayEvent(){this.mediaPlayingCount++,this.comms?.send("media_play",this.mediaPlayingCount)}onMediaPauseEvent(){this.mediaPlayingCount>0&&this.mediaPlayingCount--,this.comms?.send("media_pause",this.mediaPlayingCount)}pauseAllMedia(e){const t=e.document.querySelectorAll("audio,video");for(let a=0;a<t.length;a++)t[a].pause()}mount(e,t){this.comms=t,e.addEventListener("error",this.wndOnErr,!1),Reflect.defineProperty(e.navigator,"epubReadingSystem",{value:{name:"readium-ts-toolkit",version:"2.7.1",hasFeature:(n,s="")=>{switch(n){case"dom-manipulation":return!0;case"layout-changes":return!0;case"touch-events":return!0;case"mouse-events":return!0;case"keyboard-events":return!0;case"spine-scripting":return!0;case"embedded-web-content":return!0;default:return!1}}},writable:!1}),"getAnimations"in e.document&&e.document.getAnimations().forEach(n=>{n.cancel(),this.allAnimations.add(n)}),t.register("activate",i.moduleName,(n,s)=>{this.allAnimations.forEach(r=>{r.cancel(),r.play()}),s(!0)}),t.register("unfocus",i.moduleName,(n,s)=>{this.pauseAllMedia(e),this.allAnimations.forEach(r=>r.pause()),s(!0)});const a=e.document.querySelectorAll("audio,video");for(let n=0;n<a.length;n++){const s=a[n];s.addEventListener("play",this.onMediaPlayEvent,{passive:!0}),s.addEventListener("pause",this.onMediaPauseEvent,{passive:!0})}return t.log("Setup Mounted"),!0}unmount(e,t){return e.removeEventListener("error",this.wndOnErr),e.removeEventListener("play",this.onMediaPlayEvent),e.removeEventListener("pause",this.onMediaPauseEvent),this.allAnimations.forEach(a=>a.cancel()),this.allAnimations.clear(),t.log("Setup Unmounted"),!0}};i.moduleName="setup";let o=i;export{o as Setup};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator-html-injectables",
3
- "version": "2.6.3",
3
+ "version": "2.7.1",
4
4
  "type": "module",
5
5
  "description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator",
6
6
  "author": "readium",
@@ -44,8 +44,8 @@
44
44
  "node": ">=18"
45
45
  },
46
46
  "dependencies": {
47
- "@readium/helpers": "1.0.0",
48
- "@readium/shared": "2.4.0"
47
+ "@readium/helpers": "1.1.0",
48
+ "@readium/shared": "2.5.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "css-selector-generator": "^3.8.0",
@@ -1,4 +1,5 @@
1
- import { getCssSelector, Locator } from "@readium/shared";
1
+ import { DomRangePoint, getCssSelector, getDomRange, getTextFragment, Locator, LocatorLocations } from "@readium/shared";
2
+ import { processTextFragmentDirective } from "@readium/helpers";
2
3
  import { TextQuoteAnchor } from "../vendor/hypothesis/anchoring/types.ts";
3
4
 
4
5
  function isReplacedLikeElement(element: Element): boolean {
@@ -6,19 +7,103 @@ function isReplacedLikeElement(element: Element): boolean {
6
7
  return tagName === "IMG" || tagName === "VIDEO" || tagName === "AUDIO" || tagName === "IFRAME" || tagName === "OBJECT" || tagName === "EMBED" || tagName === "CANVAS";
7
8
  }
8
9
 
10
+ // Root used for text-based searches: the cssSelector-referenced element when
11
+ // present and resolvable, otherwise the whole document body.
12
+ function resolveTextSearchRoot(doc: Document, locations: LocatorLocations | undefined): Element {
13
+ const cssSelector = locations && getCssSelector(locations);
14
+ let root: Element | null = null;
15
+ if (cssSelector) {
16
+ try {
17
+ root = doc.querySelector(cssSelector);
18
+ } catch (error) {
19
+ console.warn(`Invalid cssSelector: ${cssSelector}`, error);
20
+ }
21
+ }
22
+ return root ?? doc.body ?? doc.documentElement;
23
+ }
24
+
25
+ function resolveDomRangePoint(doc: Document, point: DomRangePoint): { node: Text; offset?: number } | null {
26
+ let container: Element | null;
27
+ try {
28
+ container = doc.querySelector(point.cssSelector);
29
+ } catch (error) {
30
+ console.error(`Invalid domRange cssSelector: ${point.cssSelector}`, error);
31
+ return null;
32
+ }
33
+ if (!container) {
34
+ console.error(`Can't resolve domRange cssSelector: ${point.cssSelector}`);
35
+ return null;
36
+ }
37
+
38
+ // textNodeIndex counts only direct-child text nodes (spec: html.md "child Text node").
39
+ let index = 0;
40
+ for (const child of container.childNodes) {
41
+ if (child.nodeType === Node.TEXT_NODE) {
42
+ if (index === point.textNodeIndex) return { node: child as Text, offset: point.charOffset };
43
+ index++;
44
+ }
45
+ }
46
+
47
+ console.error(`Can't resolve domRange textNodeIndex ${point.textNodeIndex} for selector: ${point.cssSelector}`);
48
+ return null;
49
+ }
50
+
9
51
  // Based on the kotlin-toolkit code
10
52
  export function rangeFromLocator(doc: Document, locator: Locator) {
11
53
  try {
12
54
  const locations = locator.locations;
13
55
  const text = locator.text;
14
- if (text && text.highlight) {
15
- let root;
16
- if (locations && getCssSelector(locations)) {
17
- root = doc.querySelector(getCssSelector(locations)!);
56
+
57
+ // Tried before text.highlight: a domRange pinpoints an exact node/offset,
58
+ // which disambiguates between multiple occurrences of the same quote that
59
+ // a text-based search alone cannot tell apart.
60
+ if (locations) {
61
+ const domRange = getDomRange(locations);
62
+ if (domRange) {
63
+ const start = resolveDomRangePoint(doc, domRange.start);
64
+ const end = domRange.end ? resolveDomRangePoint(doc, domRange.end) : start;
65
+ if (start && end) {
66
+ try {
67
+ const range = doc.createRange();
68
+
69
+ if (start.offset !== undefined) {
70
+ range.setStart(start.node, start.offset);
71
+ } else {
72
+ range.setStartBefore(start.node);
73
+ }
74
+
75
+ if (end.offset !== undefined) {
76
+ range.setEnd(end.node, end.offset);
77
+ } else {
78
+ range.setEndBefore(end.node);
79
+ }
80
+
81
+ return range;
82
+ } catch (error) {
83
+ console.warn("Invalid domRange, falling back:", error);
84
+ }
85
+ }
18
86
  }
19
- if (!root) {
20
- root = doc.body;
87
+ }
88
+
89
+ // Tried before text.highlight for the same reason as domRange: a
90
+ // ":~:text=" fragment is a strict superset of what text.highlight can
91
+ // express (it also supports a textStart...textEnd range), so checking
92
+ // text.highlight first would mean this branch is rarely reached.
93
+ if (locations) {
94
+ const fragmentDirective = getTextFragment(locations);
95
+
96
+ if (fragmentDirective) {
97
+ const root = resolveTextSearchRoot(doc, locations);
98
+ const results = processTextFragmentDirective(fragmentDirective, doc, root);
99
+ if (results.length > 0) {
100
+ return results[0]!;
101
+ }
21
102
  }
103
+ }
104
+
105
+ if (text && text.highlight) {
106
+ const root = resolveTextSearchRoot(doc, locations);
22
107
 
23
108
  const anchor = new TextQuoteAnchor(root, text.highlight, {
24
109
  prefix: text.before,
@@ -6,7 +6,7 @@ import { ModuleName } from "./ModuleLibrary.ts";
6
6
  import { Rect, getClientRectsNoOverlap, getTextClientRects, rectContainsPoint } from "../helpers/rect.ts";
7
7
  import { getProperty } from "../helpers/css.ts";
8
8
  import { isDarkColor, getContrastingTextColor, adjustColorForContrast, colorToRgba } from "@readium/helpers";
9
- import { makeWritingContext } from "../helpers/document.ts";
9
+ import { makeWritingContext, WritingContext } from "../helpers/document.ts";
10
10
  import { sML } from "@readium/helpers";
11
11
  import { sanitizeHTML } from "../helpers/sanitize.ts";
12
12
 
@@ -167,6 +167,11 @@ class DecorationGroup {
167
167
  return this._hoverable;
168
168
  }
169
169
 
170
+ /** True if this group has at least one item rendered via the DOM overlay path (not CSS Highlight API). */
171
+ get hasOverlayItems(): boolean {
172
+ return !this.experimentalHighlights || (this.notTextFlag?.size ?? 0) > 0;
173
+ }
174
+
170
175
  set hoverable(value: boolean) {
171
176
  this._hoverable = value;
172
177
  if (!value && this.hoveredItem) {
@@ -337,8 +342,7 @@ class DecorationGroup {
337
342
  this.wnd.document.removeEventListener("pointermove", this.hoverHandler);
338
343
  }
339
344
 
340
- private clientRectsToDocCoords(rects: Rect[]): Rect[] {
341
- const ctx = makeWritingContext(this.wnd);
345
+ private clientRectsToDocCoords(rects: Rect[], ctx: WritingContext = makeWritingContext(this.wnd)): Rect[] {
342
346
  const dx = ctx.xDocOffset;
343
347
  const dy = ctx.yDocOffset;
344
348
  if (dx === 0 && dy === 0) return rects;
@@ -501,6 +505,32 @@ class DecorationGroup {
501
505
  });
502
506
  }
503
507
 
508
+ /**
509
+ * Cheap scroll-driven pass: repositions existing overlay elements in place instead of
510
+ * requestLayout()'s full destroy-and-rebuild, only falling back to a full layout()
511
+ * per item when geometry has genuinely changed shape (a real reflow).
512
+ */
513
+ repositionOverlays() {
514
+ let hasMaskItem = false;
515
+ const relaidOut: DecorationItem[] = [];
516
+ const ctx = makeWritingContext(this.wnd);
517
+ this.items.forEach(item => {
518
+ if (this.experimentalHighlights && !this.notTextFlag?.has(item.id)) return;
519
+ if (item.decoration.style?.type === DecorationStyleType.Mask) {
520
+ hasMaskItem = true;
521
+ return;
522
+ }
523
+ if (!this.repositionItem(item, ctx)) {
524
+ item.container?.remove();
525
+ this.layout(item);
526
+ relaidOut.push(item);
527
+ }
528
+ item.hitRects = this.clientRectsToDocCoords(getClientRectsNoOverlap(item.range, false, false, ((item.decoration.style as BuiltinDecorationStyle).expand ?? 0) + this.hitGap()), ctx);
529
+ });
530
+ if (relaidOut.length) this.renderLayout(relaidOut);
531
+ if (hasMaskItem) this.updateSharedMask();
532
+ }
533
+
504
534
  private experimentalLayout(item: DecorationItem) {
505
535
  const stylesheet = this.requireContainer(true) as HTMLStyleElement;
506
536
  const cssHighlights = (this.wnd as any).CSS.highlights as Map<string, any>;
@@ -655,6 +685,131 @@ class DecorationGroup {
655
685
  stylesheet.innerHTML = rules.join("\n");
656
686
  }
657
687
 
688
+ /**
689
+ * Positions a single overlay element according to the decoration's width mode.
690
+ */
691
+ private positionElement(item: DecorationItem, ctx: WritingContext, iz: number, element: HTMLElement, rect: Rect, boundingRect: DOMRect, inlineInset = 0) {
692
+ const w = item.decoration?.style?.width;
693
+ const r = rect;
694
+ switch (w) {
695
+ case DecorationWidth.Viewport: {
696
+ const snap = Math.floor(ctx.inlineStart(r) / ctx.viewportInlineSize) * ctx.viewportInlineSize;
697
+ ctx.applyPosition(element, snap + ctx.inlineScrollOffset + inlineInset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.viewportInlineSize - 2 * inlineInset, ctx.blockSize(r), iz);
698
+ break;
699
+ }
700
+ case DecorationWidth.Page: {
701
+ const snap = Math.floor(ctx.inlineStart(r) / ctx.pageInlineSize) * ctx.pageInlineSize;
702
+ ctx.applyPosition(element, snap + ctx.inlineScrollOffset + inlineInset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.pageInlineSize - 2 * inlineInset, ctx.blockSize(r), iz);
703
+ break;
704
+ }
705
+ case DecorationWidth.Bounds: {
706
+ ctx.applyPosition(element, ctx.inlineStart(boundingRect) + ctx.inlineScrollOffset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.inlineSize(boundingRect), ctx.blockSize(r), iz);
707
+ break;
708
+ }
709
+ default: {
710
+ ctx.applyPosition(element, ctx.inlineStart(r) + ctx.inlineScrollOffset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.inlineSize(r), ctx.blockSize(r), iz);
711
+ }
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Computes the ordered list of rects an overlay item's elements should occupy
717
+ * (one rect for Bounds layout, one per merged client rect for Boxes layout).
718
+ * Shared between the full build path (layout) and the cheap reposition path.
719
+ */
720
+ private computeOverlayRects(item: DecorationItem, ctx: WritingContext, boundingRect: DOMRect, expand: number): Rect[] {
721
+ if (item.decoration?.style?.layout === DecorationLayout.Bounds) {
722
+ const boundsRect: Rect = expand ? {
723
+ left: boundingRect.left - expand,
724
+ right: boundingRect.right + expand,
725
+ top: boundingRect.top - expand,
726
+ bottom: boundingRect.bottom + expand,
727
+ width: boundingRect.width + expand * 2,
728
+ height: boundingRect.height + expand * 2,
729
+ } : boundingRect;
730
+ return [boundsRect];
731
+ }
732
+
733
+ // Fall back to "boxes" value for layout.
734
+ // For underline/strikethrough, pre-filter to text-node rects only so Ruby
735
+ // (rt/rp) glyphs don't produce a separate decoration segment above the base text.
736
+ const decoStyle = item.decoration.style;
737
+ const decoType = (decoStyle as BuiltinDecorationStyle).type;
738
+ const isLineDecoration = decoType === DecorationStyleType.Underline
739
+ || decoType === DecorationStyleType.Strikethrough;
740
+ const isStrikethrough = decoType === DecorationStyleType.Strikethrough;
741
+ const rectSource = isLineDecoration
742
+ ? getTextClientRects(item.range, ["rt", "rp"])
743
+ : item.range;
744
+ // Line decorations (underline/strikethrough) don't expand in the block axis —
745
+ // expand extends endpoints along the inline axis only.
746
+ let clientRects = getClientRectsNoOverlap(
747
+ rectSource,
748
+ true, // doNotMergeHorizontallyAlignedRects
749
+ ctx.isVertical, // doNotMergeVerticallyAlignedRects
750
+ isLineDecoration ? 0 : expand
751
+ );
752
+
753
+ clientRects = clientRects.sort((r1, r2) => {
754
+ if (ctx.isVertical) {
755
+ // vertical-rl: rightmost column first; vertical-lr: leftmost first
756
+ const factor = ctx.isVertLR ? 1 : -1;
757
+ return factor * (r1.left - r2.left);
758
+ }
759
+ return r1.top - r2.top;
760
+ });
761
+
762
+ return clientRects.map(clientRect => {
763
+ let posRect: Rect = clientRect;
764
+ if (isStrikethrough) {
765
+ // Thin the rect to ~10% of block size, centred on the mid-line.
766
+ const thickness = ctx.blockSize(clientRect) * 0.1;
767
+ const blockMid = ctx.blockStart(clientRect) + ctx.blockSize(clientRect) / 2;
768
+ const bs = blockMid - thickness / 2;
769
+ posRect = ctx.isVertical
770
+ ? { left: bs, right: bs + thickness, top: clientRect.top, bottom: clientRect.bottom, width: thickness, height: clientRect.height }
771
+ : { top: bs, bottom: bs + thickness, left: clientRect.left, right: clientRect.right, height: thickness, width: clientRect.width };
772
+ }
773
+ if (expand && isLineDecoration) {
774
+ posRect = ctx.isVertical
775
+ ? { ...posRect, top: posRect.top - expand, bottom: posRect.bottom + expand, height: posRect.height + expand * 2 }
776
+ : { ...posRect, left: posRect.left - expand, right: posRect.right + expand, width: posRect.width + expand * 2 };
777
+ }
778
+ return posRect;
779
+ });
780
+ }
781
+
782
+ /**
783
+ * Cheaply repositions an already-rendered overlay item's existing elements in place,
784
+ * without destroying and rebuilding them. Returns false if the element count no
785
+ * longer matches the current geometry (a genuine reflow happened), signalling the
786
+ * caller to fall back to a full layout().
787
+ */
788
+ private repositionItem(item: DecorationItem, ctx: WritingContext): boolean {
789
+ if (!item.container) return true;
790
+ const decoStyle = item.decoration.style;
791
+ if (decoStyle.type !== DecorationStyleType.Template) {
792
+ const type = (decoStyle as BuiltinDecorationStyle).type ?? DecorationStyleType.Highlight;
793
+ if (type === DecorationStyleType.TextColor || type === DecorationStyleType.Mask) return true;
794
+ }
795
+
796
+ const iz = 1 / this.effectiveZoom();
797
+ const expand = (decoStyle as BuiltinDecorationStyle).expand ?? 0;
798
+ const boundingRect = item.range.getBoundingClientRect();
799
+ const outlineInset = (() => {
800
+ if ((decoStyle as BuiltinDecorationStyle).type !== DecorationStyleType.Outline) return 0;
801
+ const w = (decoStyle as BuiltinDecorationStyle).width;
802
+ return (w === DecorationWidth.Page || w === DecorationWidth.Viewport) ? 3 : 0;
803
+ })();
804
+
805
+ const rects = this.computeOverlayRects(item, ctx, boundingRect, expand);
806
+ const children = Array.from(item.container.children) as HTMLElement[];
807
+ if (children.length !== rects.length) return false;
808
+
809
+ children.forEach((el, i) => this.positionElement(item, ctx, iz, el, rects[i], boundingRect, outlineInset));
810
+ return true;
811
+ }
812
+
658
813
  /**
659
814
  * Layouts a single DecorationItem.
660
815
  * @param item
@@ -677,29 +832,6 @@ class DecorationGroup {
677
832
  const iz = 1 / this.effectiveZoom();
678
833
 
679
834
  const expand = (item.decoration.style as BuiltinDecorationStyle).expand ?? 0;
680
- const positionElement = (element: HTMLElement, rect: Rect, boundingRect: DOMRect, inlineInset = 0) => {
681
- const w = item.decoration?.style?.width;
682
- const r = rect;
683
- switch (w) {
684
- case DecorationWidth.Viewport: {
685
- const snap = Math.floor(ctx.inlineStart(r) / ctx.viewportInlineSize) * ctx.viewportInlineSize;
686
- ctx.applyPosition(element, snap + ctx.inlineScrollOffset + inlineInset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.viewportInlineSize - 2 * inlineInset, ctx.blockSize(r), iz);
687
- break;
688
- }
689
- case DecorationWidth.Page: {
690
- const snap = Math.floor(ctx.inlineStart(r) / ctx.pageInlineSize) * ctx.pageInlineSize;
691
- ctx.applyPosition(element, snap + ctx.inlineScrollOffset + inlineInset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.pageInlineSize - 2 * inlineInset, ctx.blockSize(r), iz);
692
- break;
693
- }
694
- case DecorationWidth.Bounds: {
695
- ctx.applyPosition(element, ctx.inlineStart(boundingRect) + ctx.inlineScrollOffset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.inlineSize(boundingRect), ctx.blockSize(r), iz);
696
- break;
697
- }
698
- default: {
699
- ctx.applyPosition(element, ctx.inlineStart(r) + ctx.inlineScrollOffset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.inlineSize(r), ctx.blockSize(r), iz);
700
- }
701
- }
702
- }
703
835
  const boundingRect = item.range.getBoundingClientRect();
704
836
 
705
837
  const decoStyle = item.decoration.style;
@@ -817,6 +949,8 @@ class DecorationGroup {
817
949
  `mix-blend-mode: ${isDarkMode ? "exclusion" : "multiply"} !important`,
818
950
  "opacity: 1 !important",
819
951
  "box-sizing: border-box !important",
952
+ // Works around an iPadOS Safari bug where mix-blend-mode paints opaque instead of blending.
953
+ "transform: translateZ(0) !important",
820
954
  ].join("; ");
821
955
  }
822
956
  }
@@ -827,70 +961,12 @@ class DecorationGroup {
827
961
  elementTemplate = template.content.firstElementChild!;
828
962
  }
829
963
 
830
- if(item.decoration?.style?.layout === DecorationLayout.Bounds) {
831
- const bounds = elementTemplate.cloneNode(true) as HTMLDivElement;
832
- bounds.style.setProperty("pointer-events", "none");
833
- const boundsRect: Rect = expand ? {
834
- left: boundingRect.left - expand,
835
- right: boundingRect.right + expand,
836
- top: boundingRect.top - expand,
837
- bottom: boundingRect.bottom + expand,
838
- width: boundingRect.width + expand * 2,
839
- height: boundingRect.height + expand * 2,
840
- } : boundingRect;
841
- positionElement(bounds, boundsRect, boundingRect, outlineInset);
842
- itemContainer.append(bounds);
843
- } else {
844
- // Fall back to "boxes" value for layout.
845
- // For underline/strikethrough, pre-filter to text-node rects only so Ruby
846
- // (rt/rp) glyphs don't produce a separate decoration segment above the base text.
847
- const decoType = (decoStyle as BuiltinDecorationStyle).type;
848
- const isLineDecoration = decoType === DecorationStyleType.Underline
849
- || decoType === DecorationStyleType.Strikethrough;
850
- const isStrikethrough = decoType === DecorationStyleType.Strikethrough;
851
- const rectSource = isLineDecoration
852
- ? getTextClientRects(item.range, ["rt", "rp"])
853
- : item.range;
854
- // Line decorations (underline/strikethrough) don't expand in the block axis —
855
- // expand extends endpoints along the inline axis only.
856
- let clientRects = getClientRectsNoOverlap(
857
- rectSource,
858
- true, // doNotMergeHorizontallyAlignedRects
859
- ctx.isVertical, // doNotMergeVerticallyAlignedRects
860
- isLineDecoration ? 0 : expand
861
- );
862
-
863
- clientRects = clientRects.sort((r1, r2) => {
864
- if (ctx.isVertical) {
865
- // vertical-rl: rightmost column first; vertical-lr: leftmost first
866
- const factor = ctx.isVertLR ? 1 : -1;
867
- return factor * (r1.left - r2.left);
868
- }
869
- return r1.top - r2.top;
870
- });
871
-
872
- for (let clientRect of clientRects) {
873
- const line = elementTemplate.cloneNode(true) as HTMLDivElement;
874
- line.style.setProperty("pointer-events", "none");
875
- let posRect: Rect = clientRect;
876
- if (isStrikethrough) {
877
- // Thin the rect to ~10% of block size, centred on the mid-line.
878
- const thickness = ctx.blockSize(clientRect) * 0.1;
879
- const blockMid = ctx.blockStart(clientRect) + ctx.blockSize(clientRect) / 2;
880
- const bs = blockMid - thickness / 2;
881
- posRect = ctx.isVertical
882
- ? { left: bs, right: bs + thickness, top: clientRect.top, bottom: clientRect.bottom, width: thickness, height: clientRect.height }
883
- : { top: bs, bottom: bs + thickness, left: clientRect.left, right: clientRect.right, height: thickness, width: clientRect.width };
884
- }
885
- // Expand line decorations along the inline axis only (extend endpoints, not block size).
886
- if (expand && isLineDecoration) {
887
- posRect = ctx.isVertical
888
- ? { ...posRect, top: posRect.top - expand, bottom: posRect.bottom + expand, height: posRect.height + expand * 2 }
889
- : { ...posRect, left: posRect.left - expand, right: posRect.right + expand, width: posRect.width + expand * 2 };
890
- }
891
- positionElement(line, posRect, boundingRect, outlineInset);
892
- itemContainer.append(line);
893
- }
964
+ const rects = this.computeOverlayRects(item, ctx, boundingRect, expand);
965
+ for (const posRect of rects) {
966
+ const el = elementTemplate.cloneNode(true) as HTMLDivElement;
967
+ el.style.setProperty("pointer-events", "none");
968
+ this.positionElement(item, ctx, iz, el, posRect, boundingRect, outlineInset);
969
+ itemContainer.append(el);
894
970
  }
895
971
 
896
972
  item.container = itemContainer;
@@ -1165,13 +1241,25 @@ export class Decorator extends Module {
1165
1241
  private handleResize() {
1166
1242
  this.wnd.clearTimeout(this.resizeFrame);
1167
1243
  this.resizeFrame = this.wnd.setTimeout(() => {
1168
- this.groups.forEach(g => {
1169
- if(!g.experimentalHighlights) g.requestLayout();
1170
- });
1244
+ this.groups.forEach(g => g.requestLayout());
1171
1245
  }, 50);
1172
1246
  }
1173
1247
  private readonly handleResizer = this.handleResize.bind(this);
1174
1248
 
1249
+ private scrollScheduled = false;
1250
+ private handleScroll() {
1251
+ // rAF-throttled, not debounced: overlays must track every frame, not just settle after scroll stops.
1252
+ if (this.scrollScheduled) return;
1253
+ this.scrollScheduled = true;
1254
+ this.wnd.requestAnimationFrame(() => {
1255
+ this.scrollScheduled = false;
1256
+ this.groups.forEach(g => {
1257
+ if(g.hasOverlayItems) g.repositionOverlays();
1258
+ });
1259
+ });
1260
+ }
1261
+ private readonly handleScroller = this.handleScroll.bind(this);
1262
+
1175
1263
  mount(wnd: Window, comms: IComms): boolean {
1176
1264
  this.wnd = wnd;
1177
1265
 
@@ -1231,6 +1319,10 @@ export class Decorator extends Module {
1231
1319
  this.resizeObserver.observe(wnd.document.documentElement);
1232
1320
  wnd.addEventListener("orientationchange", this.handleResizer);
1233
1321
  wnd.addEventListener("resize", this.handleResizer);
1322
+ // Capture phase: scroll events don't bubble, so a scrollable descendant
1323
+ // (e.g. an app shell's own reading-pane container) would never reach a
1324
+ // bubble-phase listener on wnd.
1325
+ wnd.addEventListener("scroll", this.handleScroller, { passive: true, capture: true });
1234
1326
 
1235
1327
  // Watch for any style change on <html> — covers appearance, background color,
1236
1328
  // font size, line height, margins, and anything else that reflows text.
@@ -1256,6 +1348,7 @@ export class Decorator extends Module {
1256
1348
  unmount(wnd: Window, comms: IComms): boolean {
1257
1349
  wnd.removeEventListener("orientationchange", this.handleResizer);
1258
1350
  wnd.removeEventListener("resize", this.handleResizer);
1351
+ wnd.removeEventListener("scroll", this.handleScroller, { capture: true });
1259
1352
 
1260
1353
  comms.unregisterAll(Decorator.moduleName);
1261
1354
  this.resizeObserver.disconnect();
@@ -115,6 +115,9 @@ export declare class Decorator extends Module {
115
115
  private updateHighlightStyles;
116
116
  private handleResize;
117
117
  private readonly handleResizer;
118
+ private scrollScheduled;
119
+ private handleScroll;
120
+ private readonly handleScroller;
118
121
  mount(wnd: Window, comms: IComms): boolean;
119
122
  unmount(wnd: Window, comms: IComms): boolean;
120
123
  }