@readium/navigator-html-injectables 2.7.0 → 2.8.0

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{getDomRange as u,getTextFragment as g,getCssSelector as a}from"@readium/shared";import{processTextFragmentDirective as d}from"@readium/helpers";import{TextQuoteAnchor as m}from"../vendor/hypothesis/anchoring/types.js";function h(n){const r=n.tagName.toUpperCase();return r==="IMG"||r==="VIDEO"||r==="AUDIO"||r==="IFRAME"||r==="OBJECT"||r==="EMBED"||r==="CANVAS"}function f(n,r){const o=r&&a(r);let l=null;if(o)try{l=n.querySelector(o)}catch(e){console.warn(`Invalid cssSelector: ${o}`,e)}return l??n.body??n.documentElement}function i(n,r){let o;try{o=n.querySelector(r.cssSelector)}catch(s){return console.error(`Invalid domRange cssSelector: ${r.cssSelector}`,s),null}if(!o)return console.error(`Can't resolve domRange cssSelector: ${r.cssSelector}`),null;const l=n.createTreeWalker(o,NodeFilter.SHOW_TEXT);let e=0,t;for(;t=l.nextNode();){if(e===r.textNodeIndex)return{node:t,offset:r.charOffset};e++}return console.error(`Can't resolve domRange textNodeIndex ${r.textNodeIndex} for selector: ${r.cssSelector}`),null}function E(n,r){try{const o=r.locations,l=r.text;if(o){const e=u(o);if(e){const t=i(n,e.start),s=e.end?i(n,e.end):t;if(t&&s)try{const c=n.createRange();return t.offset!==void 0?c.setStart(t.node,t.offset):c.setStartBefore(t.node),s.offset!==void 0?c.setEnd(s.node,s.offset):c.setEndBefore(s.node),c}catch(c){console.warn("Invalid domRange, falling back:",c)}}}if(o){const e=g(o);if(e){const t=f(n,o),s=d(e,n,t);if(s.length>0)return s[0]}}if(l&&l.highlight){const e=f(n,o),t=new m(e,l.highlight,{prefix:l.before,suffix:l.after});try{return t.toRange()}catch{return console.warn("Quote not found:",t),null}}if(o){let e=null;if(!e&&a(o)&&(e=n.querySelector(a(o))),!e&&o.fragments){for(const t of o.fragments)if(e=n.getElementById(t),e)break}if(e){const t=n.createRange();return e.childNodes.length===0||h(e)?(t.selectNode(e),t):(t.setStartBefore(e),t.setEndAfter(e),t)}}}catch(o){console.error(o)}return null}export{E 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 K}from"@readium/shared";import{Module as N}from"./Module.js";import{rangeFromLocator as P}from"../helpers/locator.js";import{getClientRectsNoOverlap as H,rectContainsPoint as E,getTextClientRects as U}from"../helpers/rect.js";import{getProperty as I}from"../helpers/css.js";import{sML as j,adjustColorForContrast as T,getContrastingTextColor as O,colorToRgba as M,isDarkColor as q}from"@readium/helpers";import{makeWritingContext as z}from"../helpers/document.js";import{sanitizeHTML as V}from"../helpers/sanitize.js";function x(S){switch(S){case d.Mask:return"rgba(255, 255, 255, 0.5)";case d.Highlight:case d.HighlightUnderline:return"#FFFF00";default:return"#FF0000"}}const d={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=P(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!==d.TextColor&&(n===d.Outline||n===d.Template||n===d.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(H(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===d.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=z(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=z(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===d.Template)for(const r of s.clickableElements??[]){const p=r.getBoundingClientRect();if(E(p,e.clientX,e.clientY,0)){n=p;break}}else for(const r of s.hitRects)if(E(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===d.Template)for(const p of r.clickableElements??[]){const a=p.getBoundingClientRect();if(E(a,e.clientX,e.clientY,0)){s=r,n=a;break}}else for(const p of r.hitRects)if(E(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(H(e.range,!1,!1,(e.decoration.style.expand??0)+this.hitGap()))}),this.renderLayout(this.items),this.updateSharedMask()})})}repositionOverlays(){let e=!1;const t=[],i=z(this.wnd);this.items.forEach(o=>{if(!(this.experimentalHighlights&&!this.notTextFlag?.has(o.id))){if(o.decoration.style?.type===d.Mask){e=!0;return}this.repositionItem(o,i)||(o.container?.remove(),this.layout(o),t.push(o)),o.hitRects=this.clientRectsToDocCoords(H(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??d.Highlight,n=o.tint??x(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=(c,f)=>this.wnd.document.caretPositionFromPoint?.(c,f)??null;if(s===d.TextColor&&(p==="bounds"||r==="bounds"||r==="page")){const c=z(this.wnd);if(c.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,y;r==="page"?(k=Math.floor(c.inlineStart(f)/c.pageInlineSize)*c.pageInlineSize,y=c.pageInlineSize):(k=c.inlineStart(f),y=c.inlineSize(f));const v=h(k,c.blockStart(f)+1),w=h(k+y,c.blockStart(f)+c.blockSize(f)-1);if(v&&w){const C=this.wnd.document.createRange();C.setStart(v.offsetNode,v.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?T(n,l):n;let m;switch(s){case d.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 d.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 d.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 d.TextColor:m=`::highlight(${a}) {
13
+ color: ${b};
14
+ }`;break;case d.HighlightUnderline:{const{r:c,g:f,b:k}=M(b),y=`rgba(${c}, ${f}, ${k}, 0.3)`;m=`::highlight(${a}) {
15
+ color: ${O(b,l)};
16
+ background-color: ${y};
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 d.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===d.Underline||n===d.Strikethrough,p=n===d.Strikethrough,a=r?U(e.range,["rt","rp"]):e.range;let g=H(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!==d.Template){const g=i.type??d.Highlight;if(g===d.TextColor||g===d.Mask)return!0}const o=1/this.effectiveZoom(),s=i.expand??0,n=e.range.getBoundingClientRect(),r=(()=>{if(i.type!==d.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=z(this.wnd),o=1/this.effectiveZoom(),s=e.decoration.style.expand??0,n=e.range.getBoundingClientRect(),r=e.decoration.style,p=(()=>{if(r.type!==d.Outline)return 0;const h=r.width;return h==="page"||h==="viewport"?3:0})();let a;if(r.type===d.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??d.Highlight,u=h.tint??x(l);if(l===d.TextColor){e.container=t,e.clickableElements=[];return}if(l===d.Mask){e.container=t,e.clickableElements=[],this.updateSharedMask();return}const b=this.getCurrentDarkMode(),m=this.getBackgroundColor(),c=h.enforceContrast!==!1,f=(()=>{switch(l){case d.Underline:{const v=c?T(u,m):u,w=h.layout==="bounds",[C,$]=i.isVertical?["border-right","border-left"]:["border-bottom","border-top"];return[w?`${$}: 0.1em solid ${v} !important`:null,`${C}: 0.1em solid ${v} !important`,"background-color: transparent !important","box-sizing: border-box !important"].filter(Boolean).join("; ")}case d.Strikethrough:{const v=c?T(u,m):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 d.Outline:return[`outline: 2px solid ${c?T(u,m):u} !important`,"outline-offset: 1px !important","background-color: transparent !important","box-sizing: border-box !important"].join("; ");case d.HighlightUnderline:{const v=c?T(u,m):u,{r:w,g:C,b:$}=M(v),_=`rgba(${w}, ${C}, ${$}, 0.3)`,B=h.layout==="bounds",[D,L]=i.isVertical?["border-right","border-left"]:["border-bottom","border-top"];return[`background-color: ${_} !important`,B?`${L}: 0.1em solid ${v} !important`:null,`${D}: 0.1em solid ${v} !important`,"box-sizing: border-box !important"].filter(Boolean).join("; ")}case d.Highlight:default:return[`background-color: ${c?T(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===d.Mask);if(e.length===0){this.maskSvg&&(this.maskSvg.remove(),this.maskSvg=void 0),this.shadowHost&&!this.container&&(this.shadowHost.remove(),this.shadowRoot=void 0,this.shadowHost=void 0);return}const t=z(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",c=u.expand??0,f=l.range.getBoundingClientRect(),k=b==="bounds"?[c?{left:f.left-c,top:f.top-c,right:f.right+c,bottom:f.bottom+c,width:f.width+c*2,height:f.height+c*2}:f]:H(l.range,!1,!1,c);for(const y of k){let v;switch(m){case"viewport":{const w=Math.floor(t.inlineStart(y)/t.viewportInlineSize)*t.viewportInlineSize;v=t.toRect(w,t.blockStart(y),t.viewportInlineSize,t.blockSize(y));break}case"page":{const w=Math.floor(t.inlineStart(y)/t.pageInlineSize)*t.pageInlineSize;v=t.toRect(w,t.blockStart(y),t.pageInlineSize,t.blockSize(y));break}case"bounds":{v=t.toRect(t.inlineStart(f),t.blockStart(y),t.inlineSize(f),t.blockSize(y));break}default:v=t.toRect(t.inlineStart(y),t.blockStart(y),t.inlineSize(y),t.blockSize(y))}r.push(v)}}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,c=(l.bottom+t.yDocOffset)*i;return`M${u} ${b} H${m} V${c} 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 c=this.wnd.document.createElementNS(a,"rect");c.setAttribute("id",`${this.id}-mask-rect`),c.setAttribute("clip-path",`url(#${b})`),c.style.pointerEvents="none",this.maskSvg.appendChild(c),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()??x(d.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 R=class R extends N{constructor(){super(...arguments),this.observedResizeTargets=new Map,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(),this.observedResizeTargets.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",R.moduleName,(i,o)=>{const s=i;(s.action==="add"||s.action==="update")&&s.decoration.locator&&(s.decoration.locator=K.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_resize",R.moduleName,(i,o)=>{const s=i;s.action==="watch"?this.addDecorationResizeTarget(s.selector):this.removeDecorationResizeTarget(s.selector),o(!0)}),t.register("decoration_activatable",R.moduleName,(i,o)=>{const s=i,n=this.groups.get(s.group);n&&(n.activatable=s.activatable),o(!0)}),t.register("decoration_hoverable",R.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(R.moduleName),this.resizeObserver.disconnect(),this.styleObserver.disconnect(),this.cleanup(),t.log("Decorator Unmounted"),!0}addDecorationResizeTarget(e){const t=this.observedResizeTargets.get(e);t&&this.resizeObserver.unobserve(t);const i=this.wnd.document.querySelector(e);i?(this.resizeObserver.observe(i),this.observedResizeTargets.set(e,i)):this.observedResizeTargets.delete(e)}removeDecorationResizeTarget(e){const t=this.observedResizeTargets.get(e);t&&(this.resizeObserver.unobserve(t),this.observedResizeTargets.delete(e))}};R.moduleName="decorator";let A=R;export{X as DecorationLayout,d 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.7.0",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.8.0",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.7.0",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator",
6
6
  "author": "readium",
package/src/comms/keys.ts CHANGED
@@ -41,6 +41,7 @@ export type CommsCommandKey =
41
41
  // "exact_progress" |
42
42
  "first_visible_locator" |
43
43
  "decorate" |
44
+ "decoration_resize" |
44
45
  "decoration_activatable" |
45
46
  "decoration_hoverable" |
46
47
  "protect" |
@@ -35,14 +35,13 @@ function resolveDomRangePoint(doc: Document, point: DomRangePoint): { node: Text
35
35
  return null;
36
36
  }
37
37
 
38
- const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT);
38
+ // textNodeIndex counts only direct-child text nodes (spec: html.md "child Text node").
39
39
  let index = 0;
40
- let node: Node | null;
41
- while ((node = walker.nextNode())) {
42
- if (index === point.textNodeIndex) {
43
- return { node: node as Text, offset: point.charOffset };
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
44
  }
45
- index++;
46
45
  }
47
46
 
48
47
  console.error(`Can't resolve domRange textNodeIndex ${point.textNodeIndex} for selector: ${point.cssSelector}`);
@@ -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
 
@@ -103,6 +103,10 @@ export type DecoratorRequest =
103
103
  | { group: string; action: "remove"; decoration: Pick<Decoration, "id"> }
104
104
  | { group: string; action: "clear" };
105
105
 
106
+ export type DecorationResizeRequest =
107
+ | { action: "watch"; selector: string }
108
+ | { action: "unwatch"; selector: string };
109
+
106
110
  interface DecorationItem {
107
111
  id: string;
108
112
  decoration: Decoration;
@@ -167,6 +171,11 @@ class DecorationGroup {
167
171
  return this._hoverable;
168
172
  }
169
173
 
174
+ /** True if this group has at least one item rendered via the DOM overlay path (not CSS Highlight API). */
175
+ get hasOverlayItems(): boolean {
176
+ return !this.experimentalHighlights || (this.notTextFlag?.size ?? 0) > 0;
177
+ }
178
+
170
179
  set hoverable(value: boolean) {
171
180
  this._hoverable = value;
172
181
  if (!value && this.hoveredItem) {
@@ -337,8 +346,7 @@ class DecorationGroup {
337
346
  this.wnd.document.removeEventListener("pointermove", this.hoverHandler);
338
347
  }
339
348
 
340
- private clientRectsToDocCoords(rects: Rect[]): Rect[] {
341
- const ctx = makeWritingContext(this.wnd);
349
+ private clientRectsToDocCoords(rects: Rect[], ctx: WritingContext = makeWritingContext(this.wnd)): Rect[] {
342
350
  const dx = ctx.xDocOffset;
343
351
  const dy = ctx.yDocOffset;
344
352
  if (dx === 0 && dy === 0) return rects;
@@ -501,6 +509,32 @@ class DecorationGroup {
501
509
  });
502
510
  }
503
511
 
512
+ /**
513
+ * Cheap scroll-driven pass: repositions existing overlay elements in place instead of
514
+ * requestLayout()'s full destroy-and-rebuild, only falling back to a full layout()
515
+ * per item when geometry has genuinely changed shape (a real reflow).
516
+ */
517
+ repositionOverlays() {
518
+ let hasMaskItem = false;
519
+ const relaidOut: DecorationItem[] = [];
520
+ const ctx = makeWritingContext(this.wnd);
521
+ this.items.forEach(item => {
522
+ if (this.experimentalHighlights && !this.notTextFlag?.has(item.id)) return;
523
+ if (item.decoration.style?.type === DecorationStyleType.Mask) {
524
+ hasMaskItem = true;
525
+ return;
526
+ }
527
+ if (!this.repositionItem(item, ctx)) {
528
+ item.container?.remove();
529
+ this.layout(item);
530
+ relaidOut.push(item);
531
+ }
532
+ item.hitRects = this.clientRectsToDocCoords(getClientRectsNoOverlap(item.range, false, false, ((item.decoration.style as BuiltinDecorationStyle).expand ?? 0) + this.hitGap()), ctx);
533
+ });
534
+ if (relaidOut.length) this.renderLayout(relaidOut);
535
+ if (hasMaskItem) this.updateSharedMask();
536
+ }
537
+
504
538
  private experimentalLayout(item: DecorationItem) {
505
539
  const stylesheet = this.requireContainer(true) as HTMLStyleElement;
506
540
  const cssHighlights = (this.wnd as any).CSS.highlights as Map<string, any>;
@@ -655,6 +689,131 @@ class DecorationGroup {
655
689
  stylesheet.innerHTML = rules.join("\n");
656
690
  }
657
691
 
692
+ /**
693
+ * Positions a single overlay element according to the decoration's width mode.
694
+ */
695
+ private positionElement(item: DecorationItem, ctx: WritingContext, iz: number, element: HTMLElement, rect: Rect, boundingRect: DOMRect, inlineInset = 0) {
696
+ const w = item.decoration?.style?.width;
697
+ const r = rect;
698
+ switch (w) {
699
+ case DecorationWidth.Viewport: {
700
+ const snap = Math.floor(ctx.inlineStart(r) / ctx.viewportInlineSize) * ctx.viewportInlineSize;
701
+ ctx.applyPosition(element, snap + ctx.inlineScrollOffset + inlineInset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.viewportInlineSize - 2 * inlineInset, ctx.blockSize(r), iz);
702
+ break;
703
+ }
704
+ case DecorationWidth.Page: {
705
+ const snap = Math.floor(ctx.inlineStart(r) / ctx.pageInlineSize) * ctx.pageInlineSize;
706
+ ctx.applyPosition(element, snap + ctx.inlineScrollOffset + inlineInset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.pageInlineSize - 2 * inlineInset, ctx.blockSize(r), iz);
707
+ break;
708
+ }
709
+ case DecorationWidth.Bounds: {
710
+ ctx.applyPosition(element, ctx.inlineStart(boundingRect) + ctx.inlineScrollOffset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.inlineSize(boundingRect), ctx.blockSize(r), iz);
711
+ break;
712
+ }
713
+ default: {
714
+ ctx.applyPosition(element, ctx.inlineStart(r) + ctx.inlineScrollOffset, ctx.blockStart(r) + ctx.blockScrollOffset, ctx.inlineSize(r), ctx.blockSize(r), iz);
715
+ }
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Computes the ordered list of rects an overlay item's elements should occupy
721
+ * (one rect for Bounds layout, one per merged client rect for Boxes layout).
722
+ * Shared between the full build path (layout) and the cheap reposition path.
723
+ */
724
+ private computeOverlayRects(item: DecorationItem, ctx: WritingContext, boundingRect: DOMRect, expand: number): Rect[] {
725
+ if (item.decoration?.style?.layout === DecorationLayout.Bounds) {
726
+ const boundsRect: Rect = expand ? {
727
+ left: boundingRect.left - expand,
728
+ right: boundingRect.right + expand,
729
+ top: boundingRect.top - expand,
730
+ bottom: boundingRect.bottom + expand,
731
+ width: boundingRect.width + expand * 2,
732
+ height: boundingRect.height + expand * 2,
733
+ } : boundingRect;
734
+ return [boundsRect];
735
+ }
736
+
737
+ // Fall back to "boxes" value for layout.
738
+ // For underline/strikethrough, pre-filter to text-node rects only so Ruby
739
+ // (rt/rp) glyphs don't produce a separate decoration segment above the base text.
740
+ const decoStyle = item.decoration.style;
741
+ const decoType = (decoStyle as BuiltinDecorationStyle).type;
742
+ const isLineDecoration = decoType === DecorationStyleType.Underline
743
+ || decoType === DecorationStyleType.Strikethrough;
744
+ const isStrikethrough = decoType === DecorationStyleType.Strikethrough;
745
+ const rectSource = isLineDecoration
746
+ ? getTextClientRects(item.range, ["rt", "rp"])
747
+ : item.range;
748
+ // Line decorations (underline/strikethrough) don't expand in the block axis —
749
+ // expand extends endpoints along the inline axis only.
750
+ let clientRects = getClientRectsNoOverlap(
751
+ rectSource,
752
+ true, // doNotMergeHorizontallyAlignedRects
753
+ ctx.isVertical, // doNotMergeVerticallyAlignedRects
754
+ isLineDecoration ? 0 : expand
755
+ );
756
+
757
+ clientRects = clientRects.sort((r1, r2) => {
758
+ if (ctx.isVertical) {
759
+ // vertical-rl: rightmost column first; vertical-lr: leftmost first
760
+ const factor = ctx.isVertLR ? 1 : -1;
761
+ return factor * (r1.left - r2.left);
762
+ }
763
+ return r1.top - r2.top;
764
+ });
765
+
766
+ return clientRects.map(clientRect => {
767
+ let posRect: Rect = clientRect;
768
+ if (isStrikethrough) {
769
+ // Thin the rect to ~10% of block size, centred on the mid-line.
770
+ const thickness = ctx.blockSize(clientRect) * 0.1;
771
+ const blockMid = ctx.blockStart(clientRect) + ctx.blockSize(clientRect) / 2;
772
+ const bs = blockMid - thickness / 2;
773
+ posRect = ctx.isVertical
774
+ ? { left: bs, right: bs + thickness, top: clientRect.top, bottom: clientRect.bottom, width: thickness, height: clientRect.height }
775
+ : { top: bs, bottom: bs + thickness, left: clientRect.left, right: clientRect.right, height: thickness, width: clientRect.width };
776
+ }
777
+ if (expand && isLineDecoration) {
778
+ posRect = ctx.isVertical
779
+ ? { ...posRect, top: posRect.top - expand, bottom: posRect.bottom + expand, height: posRect.height + expand * 2 }
780
+ : { ...posRect, left: posRect.left - expand, right: posRect.right + expand, width: posRect.width + expand * 2 };
781
+ }
782
+ return posRect;
783
+ });
784
+ }
785
+
786
+ /**
787
+ * Cheaply repositions an already-rendered overlay item's existing elements in place,
788
+ * without destroying and rebuilding them. Returns false if the element count no
789
+ * longer matches the current geometry (a genuine reflow happened), signalling the
790
+ * caller to fall back to a full layout().
791
+ */
792
+ private repositionItem(item: DecorationItem, ctx: WritingContext): boolean {
793
+ if (!item.container) return true;
794
+ const decoStyle = item.decoration.style;
795
+ if (decoStyle.type !== DecorationStyleType.Template) {
796
+ const type = (decoStyle as BuiltinDecorationStyle).type ?? DecorationStyleType.Highlight;
797
+ if (type === DecorationStyleType.TextColor || type === DecorationStyleType.Mask) return true;
798
+ }
799
+
800
+ const iz = 1 / this.effectiveZoom();
801
+ const expand = (decoStyle as BuiltinDecorationStyle).expand ?? 0;
802
+ const boundingRect = item.range.getBoundingClientRect();
803
+ const outlineInset = (() => {
804
+ if ((decoStyle as BuiltinDecorationStyle).type !== DecorationStyleType.Outline) return 0;
805
+ const w = (decoStyle as BuiltinDecorationStyle).width;
806
+ return (w === DecorationWidth.Page || w === DecorationWidth.Viewport) ? 3 : 0;
807
+ })();
808
+
809
+ const rects = this.computeOverlayRects(item, ctx, boundingRect, expand);
810
+ const children = Array.from(item.container.children) as HTMLElement[];
811
+ if (children.length !== rects.length) return false;
812
+
813
+ children.forEach((el, i) => this.positionElement(item, ctx, iz, el, rects[i], boundingRect, outlineInset));
814
+ return true;
815
+ }
816
+
658
817
  /**
659
818
  * Layouts a single DecorationItem.
660
819
  * @param item
@@ -677,29 +836,6 @@ class DecorationGroup {
677
836
  const iz = 1 / this.effectiveZoom();
678
837
 
679
838
  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
839
  const boundingRect = item.range.getBoundingClientRect();
704
840
 
705
841
  const decoStyle = item.decoration.style;
@@ -817,6 +953,8 @@ class DecorationGroup {
817
953
  `mix-blend-mode: ${isDarkMode ? "exclusion" : "multiply"} !important`,
818
954
  "opacity: 1 !important",
819
955
  "box-sizing: border-box !important",
956
+ // Works around an iPadOS Safari bug where mix-blend-mode paints opaque instead of blending.
957
+ "transform: translateZ(0) !important",
820
958
  ].join("; ");
821
959
  }
822
960
  }
@@ -827,70 +965,12 @@ class DecorationGroup {
827
965
  elementTemplate = template.content.firstElementChild!;
828
966
  }
829
967
 
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
- }
968
+ const rects = this.computeOverlayRects(item, ctx, boundingRect, expand);
969
+ for (const posRect of rects) {
970
+ const el = elementTemplate.cloneNode(true) as HTMLDivElement;
971
+ el.style.setProperty("pointer-events", "none");
972
+ this.positionElement(item, ctx, iz, el, posRect, boundingRect, outlineInset);
973
+ itemContainer.append(el);
894
974
  }
895
975
 
896
976
  item.container = itemContainer;
@@ -975,8 +1055,11 @@ class DecorationGroup {
975
1055
  this.maskSvg.remove();
976
1056
  this.maskSvg = undefined;
977
1057
  }
978
- if (this.shadowRoot) {
979
- this.shadowRoot.innerHTML = '';
1058
+ // Only tear down the shared shadow host if no other decorations use it
1059
+ if (this.shadowHost && !this.container) {
1060
+ this.shadowHost.remove();
1061
+ this.shadowRoot = undefined;
1062
+ this.shadowHost = undefined;
980
1063
  }
981
1064
  return;
982
1065
  }
@@ -1142,6 +1225,7 @@ export class Decorator extends Module {
1142
1225
  private resizeObserver!: ResizeObserver;
1143
1226
  private styleObserver!: MutationObserver;
1144
1227
  private wnd!: Window;
1228
+ private observedResizeTargets = new Map<string, Element>();
1145
1229
  /*private readonly lastSize = {
1146
1230
  width: 0,
1147
1231
  height: 0
@@ -1154,6 +1238,7 @@ export class Decorator extends Module {
1154
1238
  private cleanup() {
1155
1239
  this.groups.forEach(g => g.destroy());
1156
1240
  this.groups.clear();
1241
+ this.observedResizeTargets.clear();
1157
1242
  }
1158
1243
 
1159
1244
  private updateHighlightStyles() {
@@ -1165,13 +1250,25 @@ export class Decorator extends Module {
1165
1250
  private handleResize() {
1166
1251
  this.wnd.clearTimeout(this.resizeFrame);
1167
1252
  this.resizeFrame = this.wnd.setTimeout(() => {
1168
- this.groups.forEach(g => {
1169
- if(!g.experimentalHighlights) g.requestLayout();
1170
- });
1253
+ this.groups.forEach(g => g.requestLayout());
1171
1254
  }, 50);
1172
1255
  }
1173
1256
  private readonly handleResizer = this.handleResize.bind(this);
1174
1257
 
1258
+ private scrollScheduled = false;
1259
+ private handleScroll() {
1260
+ // rAF-throttled, not debounced: overlays must track every frame, not just settle after scroll stops.
1261
+ if (this.scrollScheduled) return;
1262
+ this.scrollScheduled = true;
1263
+ this.wnd.requestAnimationFrame(() => {
1264
+ this.scrollScheduled = false;
1265
+ this.groups.forEach(g => {
1266
+ if(g.hasOverlayItems) g.repositionOverlays();
1267
+ });
1268
+ });
1269
+ }
1270
+ private readonly handleScroller = this.handleScroll.bind(this);
1271
+
1175
1272
  mount(wnd: Window, comms: IComms): boolean {
1176
1273
  this.wnd = wnd;
1177
1274
 
@@ -1209,6 +1306,13 @@ export class Decorator extends Module {
1209
1306
  ack(true);
1210
1307
  });
1211
1308
 
1309
+ comms.register("decoration_resize", Decorator.moduleName, (data, ack) => {
1310
+ const req = data as DecorationResizeRequest;
1311
+ if (req.action === "watch") this.addDecorationResizeTarget(req.selector);
1312
+ else this.removeDecorationResizeTarget(req.selector);
1313
+ ack(true);
1314
+ });
1315
+
1212
1316
  comms.register("decoration_activatable", Decorator.moduleName, (data, ack) => {
1213
1317
  const req = data as { group: string; activatable: boolean };
1214
1318
  const group = this.groups.get(req.group);
@@ -1231,6 +1335,10 @@ export class Decorator extends Module {
1231
1335
  this.resizeObserver.observe(wnd.document.documentElement);
1232
1336
  wnd.addEventListener("orientationchange", this.handleResizer);
1233
1337
  wnd.addEventListener("resize", this.handleResizer);
1338
+ // Capture phase: scroll events don't bubble, so a scrollable descendant
1339
+ // (e.g. an app shell's own reading-pane container) would never reach a
1340
+ // bubble-phase listener on wnd.
1341
+ wnd.addEventListener("scroll", this.handleScroller, { passive: true, capture: true });
1234
1342
 
1235
1343
  // Watch for any style change on <html> — covers appearance, background color,
1236
1344
  // font size, line height, margins, and anything else that reflows text.
@@ -1256,6 +1364,7 @@ export class Decorator extends Module {
1256
1364
  unmount(wnd: Window, comms: IComms): boolean {
1257
1365
  wnd.removeEventListener("orientationchange", this.handleResizer);
1258
1366
  wnd.removeEventListener("resize", this.handleResizer);
1367
+ wnd.removeEventListener("scroll", this.handleScroller, { capture: true });
1259
1368
 
1260
1369
  comms.unregisterAll(Decorator.moduleName);
1261
1370
  this.resizeObserver.disconnect();
@@ -1265,4 +1374,25 @@ export class Decorator extends Module {
1265
1374
  comms.log("Decorator Unmounted");
1266
1375
  return true;
1267
1376
  }
1377
+
1378
+ private addDecorationResizeTarget(selector: string): void {
1379
+ const existing = this.observedResizeTargets.get(selector);
1380
+ if (existing) this.resizeObserver.unobserve(existing);
1381
+
1382
+ const el = this.wnd.document.querySelector(selector);
1383
+ if (el) {
1384
+ this.resizeObserver.observe(el);
1385
+ this.observedResizeTargets.set(selector, el);
1386
+ } else {
1387
+ this.observedResizeTargets.delete(selector);
1388
+ }
1389
+ }
1390
+
1391
+ private removeDecorationResizeTarget(selector: string): void {
1392
+ const el = this.observedResizeTargets.get(selector);
1393
+ if (el) {
1394
+ this.resizeObserver.unobserve(el);
1395
+ this.observedResizeTargets.delete(selector);
1396
+ }
1397
+ }
1268
1398
  }
@@ -1,3 +1,3 @@
1
1
  export type CommsEventKey = "_pong" | "_unhandled" | "_ack" | "log" | "error" | "click" | "tap" | "tap_more" | "no_more" | "no_less" | "swipe" | "scroll" | "progress" | "first_visible_locator" | "text_selected" | "context_menu" | "media_play" | "media_pause" | "content_protection" | "keyboard_peripherals" | "decoration_activated" | "decoration_pointer_enter" | "decoration_pointer_leave";
2
- export type CommsCommandKey = "_ping" | "go_prev" | "go_next" | "go_id" | "go_text" | "go_end" | "go_start" | "go_progression" | "get_properties" | "update_properties" | "set_property" | "remove_property" | "first_visible_locator" | "decorate" | "decoration_activatable" | "decoration_hoverable" | "protect" | "unprotect" | "unfocus" | "focus" | "activate" | "shake" | "force_webkit_recalc" | "peripherals_protection" | "keyboard_peripherals" | "scroll_protection" | "print_protection" | "timeline_entries";
2
+ export type CommsCommandKey = "_ping" | "go_prev" | "go_next" | "go_id" | "go_text" | "go_end" | "go_start" | "go_progression" | "get_properties" | "update_properties" | "set_property" | "remove_property" | "first_visible_locator" | "decorate" | "decoration_resize" | "decoration_activatable" | "decoration_hoverable" | "protect" | "unprotect" | "unfocus" | "focus" | "activate" | "shake" | "force_webkit_recalc" | "peripherals_protection" | "keyboard_peripherals" | "scroll_protection" | "print_protection" | "timeline_entries";
3
3
  export type SuspiciousActivityType = "developer_tools" | "select_all" | "save" | "suspicious_selection" | "bulk_copy" | "suspicious_scrolling" | "suspicious_snapping" | "drag_detected" | "drop_detected" | "print";
@@ -103,11 +103,19 @@ export type DecoratorRequest = {
103
103
  group: string;
104
104
  action: "clear";
105
105
  };
106
+ export type DecorationResizeRequest = {
107
+ action: "watch";
108
+ selector: string;
109
+ } | {
110
+ action: "unwatch";
111
+ selector: string;
112
+ };
106
113
  export declare class Decorator extends Module {
107
114
  static readonly moduleName: ModuleName;
108
115
  private resizeObserver;
109
116
  private styleObserver;
110
117
  private wnd;
118
+ private observedResizeTargets;
111
119
  private resizeFrame;
112
120
  private lastGroupId;
113
121
  private groups;
@@ -115,7 +123,12 @@ export declare class Decorator extends Module {
115
123
  private updateHighlightStyles;
116
124
  private handleResize;
117
125
  private readonly handleResizer;
126
+ private scrollScheduled;
127
+ private handleScroll;
128
+ private readonly handleScroller;
118
129
  mount(wnd: Window, comms: IComms): boolean;
119
130
  unmount(wnd: Window, comms: IComms): boolean;
131
+ private addDecorationResizeTarget;
132
+ private removeDecorationResizeTarget;
120
133
  }
121
134
  export {};