@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.
package/dist/helpers/locator.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getCssSelector as
|
|
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
|
|
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: ${
|
|
3
|
+
text-decoration-color: ${b};
|
|
4
4
|
text-decoration-thickness: 0.1em;
|
|
5
|
-
}`;break;case c.Strikethrough:
|
|
5
|
+
}`;break;case c.Strikethrough:m=`::highlight(${a}) {
|
|
6
6
|
text-decoration: line-through;
|
|
7
|
-
text-decoration-color: ${
|
|
7
|
+
text-decoration-color: ${b};
|
|
8
8
|
text-decoration-thickness: 0.1em;
|
|
9
|
-
}`;break;case c.Outline:
|
|
10
|
-
outline: 2px solid ${
|
|
9
|
+
}`;break;case c.Outline:m=`::highlight(${a}) {
|
|
10
|
+
outline: 2px solid ${b};
|
|
11
11
|
outline-offset: 1px;
|
|
12
|
-
}`;break;case c.TextColor:
|
|
13
|
-
color: ${
|
|
14
|
-
}`;break;case c.HighlightUnderline:{const{r:
|
|
15
|
-
color: ${O(
|
|
16
|
-
background-color: ${
|
|
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: ${
|
|
18
|
+
text-decoration-color: ${b};
|
|
19
19
|
text-decoration-thickness: 0.1em;
|
|
20
|
-
}`;break}case c.Highlight:default:
|
|
21
|
-
color: ${O(
|
|
22
|
-
background-color: ${
|
|
23
|
-
}`}
|
|
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.
|
|
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.
|
|
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.
|
|
48
|
-
"@readium/shared": "2.
|
|
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",
|
package/src/helpers/locator.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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,
|
package/src/modules/Decorator.ts
CHANGED
|
@@ -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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
}
|