@readium/navigator-html-injectables 2.6.2 → 2.6.3

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{Module as l}from"../Module.js";const i=class i extends l{constructor(){super(...arguments),this.mediaPlayingCount=0,this.allAnimations=new Set}wndOnErr(e){this.comms?.send("error",{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno})}unblock(e){for(e._readium_blockEvents=!1;e._readium_blockedEvents?.length>0;){const t=e._readium_blockedEvents.shift();switch(t[0]){case 0:Reflect.apply(t[1],t[2],t[3]);break;case 1:const a=t[1],n=t[2];e.removeEventListener(a.type,e._readium_eventBlocker,!0);const s=new Event(a.type,{bubbles:a.bubbles,cancelable:a.cancelable});n?n.dispatchEvent(s):e.dispatchEvent(s);break}}}onMediaPlayEvent(){this.mediaPlayingCount++,this.comms?.send("media_play",this.mediaPlayingCount)}onMediaPauseEvent(){this.mediaPlayingCount>0&&this.mediaPlayingCount--,this.comms?.send("media_pause",this.mediaPlayingCount)}pauseAllMedia(e){const t=e.document.querySelectorAll("audio,video");for(let a=0;a<t.length;a++)t[a].pause()}mount(e,t){this.comms=t,e.addEventListener("error",this.wndOnErr,!1),Reflect.defineProperty(e.navigator,"epubReadingSystem",{value:{name:"readium-ts-toolkit",version:"2.6.2",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.6.3",hasFeature:(n,s="")=>{switch(n){case"dom-manipulation":return!0;case"layout-changes":return!0;case"touch-events":return!0;case"mouse-events":return!0;case"keyboard-events":return!0;case"spine-scripting":return!0;case"embedded-web-content":return!0;default:return!1}}},writable:!1}),"getAnimations"in e.document&&e.document.getAnimations().forEach(n=>{n.cancel(),this.allAnimations.add(n)}),t.register("activate",i.moduleName,(n,s)=>{this.allAnimations.forEach(r=>{r.cancel(),r.play()}),s(!0)}),t.register("unfocus",i.moduleName,(n,s)=>{this.pauseAllMedia(e),this.allAnimations.forEach(r=>r.pause()),s(!0)});const a=e.document.querySelectorAll("audio,video");for(let n=0;n<a.length;n++){const s=a[n];s.addEventListener("play",this.onMediaPlayEvent,{passive:!0}),s.addEventListener("pause",this.onMediaPauseEvent,{passive:!0})}return t.log("Setup Mounted"),!0}unmount(e,t){return e.removeEventListener("error",this.wndOnErr),e.removeEventListener("play",this.onMediaPlayEvent),e.removeEventListener("pause",this.onMediaPauseEvent),this.allAnimations.forEach(a=>a.cancel()),this.allAnimations.clear(),t.log("Setup Unmounted"),!0}};i.moduleName="setup";let o=i;export{o as Setup};
@@ -1,4 +1,4 @@
1
- import{LocatorText as m,Locator as g,LocatorLocations as f}from"@readium/shared";import{deselect as a,findFirstVisibleLocator as p}from"../../helpers/dom.js";import{Snapper as h}from"./Snapper.js";import{rangeFromLocator as b}from"../../helpers/locator.js";import{isVerticalLR as L,forceWebkitRecalc as S}from"../../helpers/document.js";import{PatternAnalyzer as v}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as y}from"../../protection/config.js";const d="readium-cjk-vertical-snapper-style",l=class l extends h{constructor(){super(...arguments),this.patternAnalyzer=null,this.lastScrollTime=0,this.isScrollProtectionEnabled=!1,this.initialScrollHandled=!1,this.isScrolling=!1,this.lastScrollLeft=0,this.isResizing=!1,this.resizeDebounce=null,this.verticalLR=!1,this.handleScroll=i=>{if(this.comms.ready&&!this.isResizing){if(!this.initialScrollHandled){this.lastScrollLeft=Math.abs(this.doc().scrollLeft),this.initialScrollHandled=!0,this.reportProgress();return}this.isScrolling||(this.isScrolling=!0,this.wnd.requestAnimationFrame(()=>{this.reportProgress();const r=Math.abs(this.doc().scrollLeft),s=r-this.lastScrollLeft;if(this.lastScrollLeft=r,this.isScrollProtectionEnabled&&Math.abs(s)>5){const e=Date.now(),t=e-(this.lastScrollTime||e);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(s>0?"down":"up",Math.abs(s),t)){const n=i.target&&"tagName"in i.target?{tagName:i.target.tagName}:null;this.comms?.send("content_protection",{type:"suspicious_scrolling",timestamp:Date.now(),scrollDelta:s,scrollDirection:s>0?"left":"right",targetElement:n})}this.lastScrollTime=e}this.comms.send("scroll",s),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}scrollable(){return Math.max(0,this.doc().scrollWidth-this.wnd.innerWidth)}hasScrolledPast(i){const r=i.getBoundingClientRect(),s=this.wnd.innerWidth/2,e=this.wnd.innerWidth*h.CENTER_TOLERANCE;return this.verticalLR?r.right<=s+e:r.left>=s-e}reportProgress(i){if(!this.comms.ready)return;const r=this.doc().scrollWidth,s=this.wnd.innerWidth,e=Math.max(1,r-s),t=Math.abs(this.doc().scrollLeft),o=Math.max(0,Math.min(1,t/e)),n=Math.max(0,Math.min(1,(t+s)/r));this.comms.send("progress",{start:o,end:n,fragmentId:i!==void 0?i:this.currentTimelineFragment()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new v(y),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(i,r){this.wnd=i,this.comms=r,this.setupTimelineObserver("horizontal"),this.initialScrollHandled=!1,this.lastScrollLeft=0,this.isResizing=!1,this.verticalLR=L(i),this.resizeDebounce&&(this.wnd.clearTimeout(this.resizeDebounce),this.resizeDebounce=null),i.navigator.epubReadingSystem&&(i.navigator.epubReadingSystem.layoutStyle="scrolling");const s=i.document.createElement("style");return s.dataset.readium="true",s.id=d,s.textContent=`
1
+ import{LocatorText as m,Locator as g,LocatorLocations as f}from"@readium/shared";import{deselect as a,findFirstVisibleLocator as p}from"../../helpers/dom.js";import{Snapper as c}from"./Snapper.js";import{rangeFromLocator as b}from"../../helpers/locator.js";import{isVerticalLR as L,forceWebkitRecalc as S}from"../../helpers/document.js";import{PatternAnalyzer as v}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as y}from"../../protection/config.js";const d="readium-cjk-vertical-snapper-style",l=class l extends c{constructor(){super(...arguments),this.patternAnalyzer=null,this.lastScrollTime=0,this.isScrollProtectionEnabled=!1,this.initialScrollHandled=!1,this.isScrolling=!1,this.lastScrollLeft=0,this.isResizing=!1,this.resizeDebounce=null,this.verticalLR=!1,this.handleScroll=s=>{if(this.comms.ready&&!this.isResizing){if(!this.initialScrollHandled){this.lastScrollLeft=Math.abs(this.doc().scrollLeft),this.initialScrollHandled=!0,this.reportProgress();return}this.isScrolling||(this.isScrolling=!0,this.wnd.requestAnimationFrame(()=>{this.reportProgress();const e=Math.abs(this.doc().scrollLeft),i=e-this.lastScrollLeft;if(this.lastScrollLeft=e,this.isScrollProtectionEnabled&&Math.abs(i)>5){const t=Date.now(),r=t-(this.lastScrollTime||t);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(i>0?"down":"up",Math.abs(i),r)){const n=s.target&&"tagName"in s.target?{tagName:s.target.tagName}:null;this.comms?.send("content_protection",{type:"suspicious_scrolling",timestamp:Date.now(),scrollDelta:i,scrollDirection:i>0?"left":"right",targetElement:n})}this.lastScrollTime=t}this.comms.send("scroll",i),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}scrollable(){return Math.max(0,this.doc().scrollWidth-this.wnd.innerWidth)}hasScrolledPast(s){const e=s.getBoundingClientRect(),i=this.wnd.innerWidth/2,t=this.wnd.innerWidth*c.CENTER_TOLERANCE;return this.verticalLR?e.right<=i+t:e.left>=i-t}inCenterBand(s){const e=s.getBoundingClientRect(),i=this.wnd.innerWidth/2,t=this.wnd.innerWidth*c.CENTER_TOLERANCE,r=i-t,o=i+t;return e.width===0?e.left<=o&&e.left>=r:Math.max(0,Math.min(e.right,o)-Math.max(e.left,r))/e.width>=c.CENTER_TOLERANCE}fragmentStart(s){return s.getBoundingClientRect().left+Math.abs(this.doc().scrollLeft)}currentScrollExtent(){return{pos:Math.abs(this.doc().scrollLeft),size:this.wnd.innerWidth}}reportProgress(s){if(!this.comms.ready)return;const e=this.doc().scrollWidth,i=this.wnd.innerWidth,t=Math.max(1,e-i),r=Math.abs(this.doc().scrollLeft),o=Math.max(0,Math.min(1,r/t)),n=Math.max(0,Math.min(1,(r+i)/e));this.comms.send("progress",{start:o,end:n,fragmentId:s!==void 0?s:this.currentTimelineFragment(),visibleFragmentIds:this.sortedVisibleFragmentIds()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new v(y),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(s,e){this.wnd=s,this.comms=e,this.initialScrollHandled=!1,this.lastScrollLeft=0,this.isResizing=!1,this.verticalLR=L(s),this.resizeDebounce&&(this.wnd.clearTimeout(this.resizeDebounce),this.resizeDebounce=null),s.navigator.epubReadingSystem&&(s.navigator.epubReadingSystem.layoutStyle="scrolling");const i=s.document.createElement("style");return i.dataset.readium="true",i.id=d,i.textContent=`
2
2
  * {
3
3
  scrollbar-width: none;
4
4
  }
@@ -9,4 +9,4 @@ import{LocatorText as m,Locator as g,LocatorLocations as f}from"@readium/shared"
9
9
  overflow-x: auto !important;
10
10
  overflow-y: hidden !important;
11
11
  }
12
- `,i.document.head.appendChild(s),this.resizeObserver=new ResizeObserver(()=>{this.resizeDebounce&&this.wnd.clearTimeout(this.resizeDebounce),this.isResizing=!0,this.resizeDebounce=this.wnd.setTimeout(()=>{this.isResizing=!1,this.resizeDebounce=null,this.reportProgress()},50)}),this.resizeObserver.observe(i.document.body),i.addEventListener("scroll",this.handleScroll,{passive:!0}),r.register("force_webkit_recalc",l.moduleName,()=>{S(this.wnd);const e=this.doc().scrollLeft;this.verticalLR?this.doc().scrollLeft=e>1?e-1:e+1:this.doc().scrollLeft=e<-1?e+1:e-1,this.doc().scrollLeft=e}),r.register("go_progression",l.moduleName,(e,t)=>{const o=e;if(o<0||o>1){r.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),t(!1);return}this.wnd.requestAnimationFrame(()=>{const n=this.scrollable()*o;this.doc().scrollLeft=this.verticalLR?n:-n,this.reportProgress(this.fragmentFromGeometry()),a(this.wnd),t(!0)})}),r.register("go_id",l.moduleName,(e,t)=>{const o=i.document.getElementById(e);if(!o){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollLeft+=o.getBoundingClientRect().left-i.innerWidth/2;const n=e;this.reportProgress(this.timelineEntries.has(n)?n:this.nearestPrecedingTimelineEntry(o)),a(this.wnd),t(!0)})}),r.register("go_text",l.moduleName,(e,t)=>{let o;Array.isArray(e)&&(e.length>1&&(o=e[1]),e=e[0]);const n=m.deserialize(e),c=b(this.wnd.document,new g({href:i.location.href,type:"text/html",text:n,locations:o?new f({otherLocations:new Map([["cssSelector",o]])}):void 0}));if(!c){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollLeft+=c.getBoundingClientRect().left-i.innerWidth/2,this.reportProgress(this.nearestPrecedingTimelineEntry(c.startContainer)),a(this.wnd),t(!0)})}),r.register("go_start",l.moduleName,(e,t)=>{if(this.doc().scrollLeft===0)return t(!1);this.doc().scrollLeft=0,this.reportProgress(this.firstFragmentIfReached()),t(!0)}),r.register("go_end",l.moduleName,(e,t)=>{if(Math.abs(this.doc().scrollLeft)===this.scrollable())return t(!1);this.doc().scrollLeft=this.verticalLR?this.scrollable():-this.scrollable(),this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),t(!0)}),r.register(["go_next","go_prev"],l.moduleName,(e,t)=>t(!1)),r.register("unfocus",l.moduleName,(e,t)=>{a(this.wnd),t(!0)}),r.register("scroll_protection",l.moduleName,(e,t)=>{this.enableScrollProtection(),t(!0)}),r.register("focus",l.moduleName,(e,t)=>{this.reportProgress(),t(!0)}),r.register("first_visible_locator",l.moduleName,(e,t)=>{const o=p(i,!0);this.comms.send("first_visible_locator",o.serialize()),t(!0)}),r.register("timeline_entries",l.moduleName,(e,t)=>{this.updateTimelineEntries(Array.isArray(e)?e:[],i),t(!0)}),r.log("CJKVerticalSnapper Mounted"),!0}unmount(i,r){return r.unregisterAll(l.moduleName),this.resizeObserver.disconnect(),this.handleScroll&&i.removeEventListener("scroll",this.handleScroll),i.document.getElementById(d)?.remove(),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isScrollProtectionEnabled=!1),this.timelineObserver?.disconnect(),this.timelineObserver=null,this.visibleFragmentIds.clear(),this.timelineEntries.clear(),r.log("CJKVerticalSnapper Unmounted"),!0}};l.moduleName="cjk_vertical_snapper";let u=l;export{u as CJKVerticalSnapper};
12
+ `,s.document.head.appendChild(i),this.resizeObserver=new ResizeObserver(()=>{this.resizeDebounce&&this.wnd.clearTimeout(this.resizeDebounce),this.isResizing=!0,this.resizeDebounce=this.wnd.setTimeout(()=>{this.isResizing=!1,this.resizeDebounce=null,this.refreshFragmentStarts(),this.reportProgress()},50)}),this.resizeObserver.observe(s.document.body),s.addEventListener("scroll",this.handleScroll,{passive:!0}),e.register("force_webkit_recalc",l.moduleName,()=>{S(this.wnd);const t=this.doc().scrollLeft;this.verticalLR?this.doc().scrollLeft=t>1?t-1:t+1:this.doc().scrollLeft=t<-1?t+1:t-1,this.doc().scrollLeft=t}),e.register("go_progression",l.moduleName,(t,r)=>{const o=t;if(o<0||o>1){e.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),r(!1);return}this.wnd.requestAnimationFrame(()=>{const n=this.scrollable()*o;this.doc().scrollLeft=this.verticalLR?n:-n,this.reportProgress(this.fragmentFromGeometry()),a(this.wnd),r(!0)})}),e.register("go_id",l.moduleName,(t,r)=>{const o=s.document.getElementById(t);if(!o){r(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollLeft+=o.getBoundingClientRect().left-s.innerWidth/2;const n=t;this.reportProgress(this.timelineEntries.has(n)?n:this.nearestPrecedingTimelineEntry(o)),a(this.wnd),r(!0)})}),e.register("go_text",l.moduleName,(t,r)=>{let o;Array.isArray(t)&&(t.length>1&&(o=t[1]),t=t[0]);const n=m.deserialize(t),h=b(this.wnd.document,new g({href:s.location.href,type:"text/html",text:n,locations:o?new f({otherLocations:new Map([["cssSelector",o]])}):void 0}));if(!h){r(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollLeft+=h.getBoundingClientRect().left-s.innerWidth/2,this.reportProgress(this.nearestPrecedingTimelineEntry(h.startContainer)),a(this.wnd),r(!0)})}),e.register("go_start",l.moduleName,(t,r)=>{if(this.doc().scrollLeft===0)return r(!1);this.doc().scrollLeft=0,this.reportProgress(this.firstFragmentIfReached()),r(!0)}),e.register("go_end",l.moduleName,(t,r)=>{if(Math.abs(this.doc().scrollLeft)===this.scrollable())return r(!1);this.doc().scrollLeft=this.verticalLR?this.scrollable():-this.scrollable(),this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),r(!0)}),e.register(["go_next","go_prev"],l.moduleName,(t,r)=>r(!1)),e.register("unfocus",l.moduleName,(t,r)=>{a(this.wnd),r(!0)}),e.register("scroll_protection",l.moduleName,(t,r)=>{this.enableScrollProtection(),r(!0)}),e.register("focus",l.moduleName,(t,r)=>{this.reportProgress(),r(!0)}),e.register("first_visible_locator",l.moduleName,(t,r)=>{const o=p(s,!0);this.comms.send("first_visible_locator",o.serialize()),r(!0)}),e.register("timeline_entries",l.moduleName,(t,r)=>{this.updateTimelineEntries(Array.isArray(t)?t:[],s),r(!0)}),e.log("CJKVerticalSnapper Mounted"),!0}unmount(s,e){return e.unregisterAll(l.moduleName),this.resizeObserver.disconnect(),this.handleScroll&&s.removeEventListener("scroll",this.handleScroll),s.document.getElementById(d)?.remove(),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isScrollProtectionEnabled=!1),this.timelineEntries.clear(),this.cachedFragmentIds=[],this.sortedFragmentIds=[],this.cachedFragmentStarts.clear(),e.log("CJKVerticalSnapper Unmounted"),!0}};l.moduleName="cjk_vertical_snapper";let u=l;export{u as CJKVerticalSnapper};
@@ -1,4 +1,4 @@
1
- import{Snapper as C}from"./Snapper.js";import{getColumnCountPerScreen as x,isRTL as A,appendVirtualColumnIfNeeded as W}from"../../helpers/document.js";import{styleChangeAffectsLayout as P}from"../../helpers/css.js";import{easeInOutQuad as N}from"../../helpers/animation.js";import{LocatorText as F,Locator as M,LocatorLocations as z}from"@readium/shared";import{rangeFromLocator as _}from"../../helpers/locator.js";import{deselect as m,findFirstVisibleLocator as X}from"../../helpers/dom.js";import{PatternAnalyzer as q}from"../../protection/PatternAnalyzer.js";const T="readium-column-snapper-style",w=200,h=class h extends C{constructor(){super(...arguments),this.isSnapProtectionEnabled=!1,this.patternAnalyzer=null,this.lastTurnTime=0,this.rtl=!1,this.shakeTimeout=0,this.snappingCancelled=!1,this.alreadyScrollLeft=0,this.overscroll=0,this.cachedScrollWidth=0,this.touchState=0,this.startingX=void 0,this.endingX=void 0,this.onTouchStarter=this.onTouchStart.bind(this),this.onTouchEnder=this.onTouchEnd.bind(this),this.onWidthChanger=this.onWidthChange.bind(this),this.onTouchMover=this.onTouchMove.bind(this)}doc(){return this.wnd.document.scrollingElement}scrollOffset(){const t=this.doc().scrollLeft;return t!==0?t:this.alreadyScrollLeft}snapOffset(t){const e=t+(this.rtl?-1:1);return e-e%this.wnd.innerWidth}snapNormOffset(t){const e=t+1;return e-e%this.wnd.innerWidth}normScroll(){const t=this.doc().scrollLeft||this.alreadyScrollLeft;return this.rtl?Math.abs(t):Math.max(0,this.wnd.scrollX>0?this.wnd.scrollX:t)}hasScrolledPast(t){const e=t.getBoundingClientRect();return this.rtl?e.left>=this.wnd.innerWidth:e.right<=0}currentTimelineFragment(){if(this.timelineEntries.size===0)return;const t=this.wnd.innerWidth;let e;for(const o of this.sortedFragmentIds){const l=this.timelineEntries.get(o).getBoundingClientRect();if(this.rtl?l.right>0:l.left<t)e=o;else break}return e}reportProgress(t){const e=this.cachedScrollWidth,o=this.wnd.innerWidth,n=this.normScroll(),l=Math.max(1,e-o),r=Math.max(0,Math.min(1,n/l)),s=Math.max(0,Math.min(1,(n+o)/e));this.comms.send("progress",{start:r,end:s,fragmentId:t!==void 0?t:this.currentTimelineFragment()})}shake(){if(this.overscroll!==0||this.shakeTimeout!==0)return;const t=this.doc(),e=this.normScroll()<5,o=this.rtl?e?"readium-bounce-r":"readium-bounce-l":"readium-bounce-r";t.classList.add(o);const n=this.scrollOffset();this.shakeTimeout=this.wnd.setTimeout(()=>{t.classList.remove(o),this.shakeTimeout=0,this.doc().scrollLeft=n},150)}takeOverSnap(){this.snappingCancelled=!0,this.clearTouches();const t=this.doc();this.overscroll=t.style.transform?.length>12?parseFloat(t.style.transform.slice(12).split("px")[0]):0}snapCurrentOffset(t=!1,e=!1){const o=this.doc(),n=x(this.wnd),l=this.cachedScrollWidth-this.wnd.innerWidth,r=Math.min(Math.max(0,this.normScroll()),l),s=this.dragOffset(),i=this.rtl?-s:s,d=this.wnd.innerWidth/3*(i>0?2:1),a=Math.min(l,Math.max(0,this.snapNormOffset(r+d))),c=this.rtl?-a:a,u=this.rtl?-r:r,O=c>u?"right":"left";if(this.checkSuspiciousSnap(O,Math.abs(c-u)),t&&c!==u){this.snappingCancelled=!1;const L=(p,f,g,S)=>g>S?f:p+(f-p)*N(g/S),v=w*n;let b;const y=p=>{if(this.snappingCancelled)return;b||(b=p);const f=p-b,g=L(this.overscroll,0,f,v),S=L(u,c,f,v);o.scrollLeft=S,this.overscroll!==0&&(o.style.transform=`translate3d(${-g}px, 0px, 0px)`),f<v?this.wnd.requestAnimationFrame(y):(this.clearTouches(),o.style.removeProperty("transform"),o.scrollLeft=c,e||this.reportProgress())};this.wnd.requestAnimationFrame(y)}else o.style.removeProperty("transform"),this.wnd.requestAnimationFrame(()=>{o.scrollLeft=c,this.clearTouches(),e||this.reportProgress()})}dragOffset(){return(this.startingX??0)-(this.endingX??0)}clearTouches(){this.startingX=void 0,this.endingX=void 0,this.overscroll=0}onTouchStart(t){switch(t.stopPropagation(),this.takeOverSnap(),t.touches.length){case 1:break;case 2:this.onTouchEnd(t);return;default:{this.onTouchEnd(t),this.comms.send("tap_more",t.touches.length);return}}this.startingX=t.touches[0].clientX,this.alreadyScrollLeft=this.doc().scrollLeft,this.touchState=1}onTouchEnd(t){if(this.touchState===2){const e=this.dragOffset(),o=this.normScroll(),n=this.cachedScrollWidth-this.wnd.innerWidth,l=this.rtl?-e:e;this.cachedScrollWidth<=this.wnd.innerWidth?(this.reportProgress(),l>5&&this.comms.send("no_more",void 0),l<-5&&this.comms.send("no_less",void 0)):o<5&&l<5?(this.alreadyScrollLeft=0,this.comms.send("no_less",void 0)):n-o<5&&l>5&&(this.alreadyScrollLeft=this.rtl?-n:n,this.comms.send("no_more",void 0)),this.snapCurrentOffset(!0),this.comms.send("swipe",e)}this.touchState=0}onWidthChange(){this.cachedScrollWidth=this.doc().scrollWidth,this.comms.ready&&this.snapCurrentOffset()}onTouchMove(t){if(this.touchState===0)return;this.touchState===1&&(this.touchState=2,m(this.wnd)),this.endingX=t.touches[0].clientX;const e=this.dragOffset(),o=this.alreadyScrollLeft+e,n=this.rtl?-(this.cachedScrollWidth-this.wnd.innerWidth):0,l=this.rtl?0:this.cachedScrollWidth-this.wnd.innerWidth;o<n?(this.overscroll=o,this.doc().style.transform=`translate3d(${-this.overscroll}px, 0px, 0px)`):o>l?(this.overscroll=o,this.doc().style.transform=`translate3d(${-o}px, 0px, 0px)`):(this.overscroll=0,this.doc().style.removeProperty("transform"),this.doc().scrollLeft=o)}enableSnapProtection(){this.patternAnalyzer||(this.patternAnalyzer=new q({maxVelocity:this.wnd.innerWidth,minVariance:.1,historySize:5,maxConsistentScrolls:3,minDirectionChanges:.3}),this.isSnapProtectionEnabled=!0,this.comms?.log("Snap protection enabled"))}checkSuspiciousSnap(t,e){if(!this.isSnapProtectionEnabled||!this.patternAnalyzer)return;const o=Date.now(),n=o-(this.lastTurnTime||o);this.lastTurnTime=o,this.patternAnalyzer.analyze(t,e,n)&&this.comms?.send("content_protection",{type:"suspicious_snapping",timestamp:Date.now(),event:null})}mount(t,e){if(this.wnd=t,this.comms=e,this.rtl=A(t),!super.mount(t,e))return!1;t.navigator.epubReadingSystem&&(t.navigator.epubReadingSystem.layoutStyle="paginated");const o=t.document.createElement("style");o.dataset.readium="true",o.id=T,o.textContent=`
1
+ import{Snapper as C}from"./Snapper.js";import{getColumnCountPerScreen as x,isRTL as A,appendVirtualColumnIfNeeded as W}from"../../helpers/document.js";import{styleChangeAffectsLayout as F}from"../../helpers/css.js";import{easeInOutQuad as P}from"../../helpers/animation.js";import{LocatorText as N,Locator as M,LocatorLocations as z}from"@readium/shared";import{rangeFromLocator as _}from"../../helpers/locator.js";import{deselect as m,findFirstVisibleLocator as X}from"../../helpers/dom.js";import{PatternAnalyzer as w}from"../../protection/PatternAnalyzer.js";const T="readium-column-snapper-style",q=200,h=class h extends C{constructor(){super(...arguments),this.isSnapProtectionEnabled=!1,this.patternAnalyzer=null,this.lastTurnTime=0,this.rtl=!1,this.shakeTimeout=0,this.snappingCancelled=!1,this.alreadyScrollLeft=0,this.overscroll=0,this.cachedScrollWidth=0,this.touchState=0,this.startingX=void 0,this.endingX=void 0,this.onTouchStarter=this.onTouchStart.bind(this),this.onTouchEnder=this.onTouchEnd.bind(this),this.onWidthChanger=this.onWidthChange.bind(this),this.onTouchMover=this.onTouchMove.bind(this)}doc(){return this.wnd.document.scrollingElement}scrollOffset(){const t=this.doc().scrollLeft;return t!==0?t:this.alreadyScrollLeft}snapOffset(t){const e=t+(this.rtl?-1:1);return e-e%this.wnd.innerWidth}snapNormOffset(t){const e=t+1;return e-e%this.wnd.innerWidth}normScroll(){const t=this.doc().scrollLeft||this.alreadyScrollLeft;return this.rtl?Math.abs(t):Math.max(0,this.wnd.scrollX>0?this.wnd.scrollX:t)}hasScrolledPast(t){const e=t.getBoundingClientRect();return this.rtl?e.left>=this.wnd.innerWidth:e.right<=0}currentTimelineFragment(){if(this.timelineEntries.size===0)return;const t=this.wnd.innerWidth;let e;for(const r of this.sortedFragmentIds){const l=this.timelineEntries.get(r).getBoundingClientRect();if(this.rtl?l.right>0:l.left<t)e=r;else break}return e}sortedVisibleFragmentIds(){const t=this.wnd.innerWidth;return this.sortedFragmentIds.filter(e=>{const r=this.timelineEntries.get(e).getBoundingClientRect();return this.rtl?r.left<t&&r.right>0:r.right>0&&r.left<t})}reportProgress(t){const e=this.cachedScrollWidth,r=this.wnd.innerWidth,n=this.normScroll(),l=Math.max(1,e-r),i=Math.max(0,Math.min(1,n/l)),s=Math.max(0,Math.min(1,(n+r)/e));this.comms.send("progress",{start:i,end:s,fragmentId:t!==void 0?t:this.currentTimelineFragment(),visibleFragmentIds:this.sortedVisibleFragmentIds()})}shake(){if(this.overscroll!==0||this.shakeTimeout!==0)return;const t=this.doc(),e=this.normScroll()<5,r=this.rtl?e?"readium-bounce-r":"readium-bounce-l":"readium-bounce-r";t.classList.add(r);const n=this.scrollOffset();this.shakeTimeout=this.wnd.setTimeout(()=>{t.classList.remove(r),this.shakeTimeout=0,this.doc().scrollLeft=n},150)}takeOverSnap(){this.snappingCancelled=!0,this.clearTouches();const t=this.doc();this.overscroll=t.style.transform?.length>12?parseFloat(t.style.transform.slice(12).split("px")[0]):0}snapCurrentOffset(t=!1,e=!1){const r=this.doc(),n=x(this.wnd),l=this.cachedScrollWidth-this.wnd.innerWidth,i=Math.min(Math.max(0,this.normScroll()),l),s=this.dragOffset(),o=this.rtl?-s:s,d=this.wnd.innerWidth/3*(o>0?2:1),a=Math.min(l,Math.max(0,this.snapNormOffset(i+d))),c=this.rtl?-a:a,u=this.rtl?-i:i,O=c>u?"right":"left";if(this.checkSuspiciousSnap(O,Math.abs(c-u)),t&&c!==u){this.snappingCancelled=!1;const L=(p,f,g,S)=>g>S?f:p+(f-p)*P(g/S),v=q*n;let b;const y=p=>{if(this.snappingCancelled)return;b||(b=p);const f=p-b,g=L(this.overscroll,0,f,v),S=L(u,c,f,v);r.scrollLeft=S,this.overscroll!==0&&(r.style.transform=`translate3d(${-g}px, 0px, 0px)`),f<v?this.wnd.requestAnimationFrame(y):(this.clearTouches(),r.style.removeProperty("transform"),r.scrollLeft=c,e||this.reportProgress())};this.wnd.requestAnimationFrame(y)}else r.style.removeProperty("transform"),this.wnd.requestAnimationFrame(()=>{r.scrollLeft=c,this.clearTouches(),e||this.reportProgress()})}dragOffset(){return(this.startingX??0)-(this.endingX??0)}clearTouches(){this.startingX=void 0,this.endingX=void 0,this.overscroll=0}onTouchStart(t){switch(t.stopPropagation(),this.takeOverSnap(),t.touches.length){case 1:break;case 2:this.onTouchEnd(t);return;default:{this.onTouchEnd(t),this.comms.send("tap_more",t.touches.length);return}}this.startingX=t.touches[0].clientX,this.alreadyScrollLeft=this.doc().scrollLeft,this.touchState=1}onTouchEnd(t){if(this.touchState===2){const e=this.dragOffset(),r=this.normScroll(),n=this.cachedScrollWidth-this.wnd.innerWidth,l=this.rtl?-e:e;this.cachedScrollWidth<=this.wnd.innerWidth?(this.reportProgress(),l>5&&this.comms.send("no_more",void 0),l<-5&&this.comms.send("no_less",void 0)):r<5&&l<5?(this.alreadyScrollLeft=0,this.comms.send("no_less",void 0)):n-r<5&&l>5&&(this.alreadyScrollLeft=this.rtl?-n:n,this.comms.send("no_more",void 0)),this.snapCurrentOffset(!0),this.comms.send("swipe",e)}this.touchState=0}onWidthChange(){this.cachedScrollWidth=this.doc().scrollWidth,this.comms.ready&&this.snapCurrentOffset()}onTouchMove(t){if(this.touchState===0)return;this.touchState===1&&(this.touchState=2,m(this.wnd)),this.endingX=t.touches[0].clientX;const e=this.dragOffset(),r=this.alreadyScrollLeft+e,n=this.rtl?-(this.cachedScrollWidth-this.wnd.innerWidth):0,l=this.rtl?0:this.cachedScrollWidth-this.wnd.innerWidth;r<n?(this.overscroll=r,this.doc().style.transform=`translate3d(${-this.overscroll}px, 0px, 0px)`):r>l?(this.overscroll=r,this.doc().style.transform=`translate3d(${-r}px, 0px, 0px)`):(this.overscroll=0,this.doc().style.removeProperty("transform"),this.doc().scrollLeft=r)}enableSnapProtection(){this.patternAnalyzer||(this.patternAnalyzer=new w({maxVelocity:this.wnd.innerWidth,minVariance:.1,historySize:5,maxConsistentScrolls:3,minDirectionChanges:.3}),this.isSnapProtectionEnabled=!0,this.comms?.log("Snap protection enabled"))}checkSuspiciousSnap(t,e){if(!this.isSnapProtectionEnabled||!this.patternAnalyzer)return;const r=Date.now(),n=r-(this.lastTurnTime||r);this.lastTurnTime=r,this.patternAnalyzer.analyze(t,e,n)&&this.comms?.send("content_protection",{type:"suspicious_snapping",timestamp:Date.now(),event:null})}mount(t,e){if(this.wnd=t,this.comms=e,this.rtl=A(t),!super.mount(t,e))return!1;t.navigator.epubReadingSystem&&(t.navigator.epubReadingSystem.layoutStyle="paginated");const r=t.document.createElement("style");r.dataset.readium="true",r.id=T,r.textContent=`
2
2
  @keyframes readium-bounce-l-animation {
3
3
  0%, 100% {transform: translate3d(0, 0, 0);}
4
4
  50% {transform: translate3d(-50px, 0, 0);}
@@ -32,4 +32,4 @@ import{Snapper as C}from"./Snapper.js";import{getColumnCountPerScreen as x,isRTL
32
32
  body::-webkit-scrollbar {
33
33
  display: none; /* for Chrome, Safari, and Opera */
34
34
  }
35
- `,t.document.head.appendChild(o),this.resizeObserver=new ResizeObserver(()=>{t.requestAnimationFrame(()=>{t&&W(t)}),this.onWidthChange()}),this.resizeObserver.observe(t.document.body),this.mutationObserver=new MutationObserver(r=>{for(const s of r)if(s.target===this.wnd.document.documentElement){const i=s.oldValue,d=s.target.getAttribute("style"),a=/transform\s*:\s*([^;]+)/,c=i?.match(a),u=d?.match(a);(!c&&!u&&P(i,d)||c&&!u||c&&u&&c[1]!==u[1])&&(t.requestAnimationFrame(()=>{t&&W(t)}),this.onWidthChange())}else t.requestAnimationFrame(()=>this.cachedScrollWidth=this.doc().scrollWidth)}),t.frameElement&&this.mutationObserver.observe(t.frameElement,{attributes:!0,attributeFilter:["style"]}),this.mutationObserver.observe(t.document,{attributes:!0,attributeFilter:["style"]}),this.mutationObserver.observe(t.document.documentElement,{attributes:!0,attributeFilter:["style"]}),e.register("scroll_protection",h.moduleName,(r,s)=>{this.enableSnapProtection(),s(!0)});const n=r=>{const s=this.doc().scrollLeft;return this.doc().scrollLeft=this.snapOffset(r),s!==this.doc().scrollLeft},l=r=>{const s=this.doc().scrollLeft,i=this.snapNormOffset(Math.max(0,Math.min(this.cachedScrollWidth-t.innerWidth,r)));return this.doc().scrollLeft=-i,s!==this.doc().scrollLeft};return t.addEventListener("orientationchange",this.onWidthChanger),t.addEventListener("resize",this.onWidthChanger),t.requestAnimationFrame(()=>this.cachedScrollWidth=this.doc().scrollWidth),e.register("go_progression",h.moduleName,(r,s)=>{const i=r;if(i<0||i>1){e.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),s(!1);return}this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;const a=(this.cachedScrollWidth-t.innerWidth)*i;this.rtl?this.doc().scrollLeft=-this.snapNormOffset(a):this.doc().scrollLeft=this.snapOffset(a),this.reportProgress(),m(this.wnd),s(!0)})}),e.register("go_id",h.moduleName,(r,s)=>{const i=t.document.getElementById(r);if(!i){s(!1);return}this.wnd.requestAnimationFrame(()=>{this.rtl?this.doc().scrollLeft=-this.snapNormOffset(i.getBoundingClientRect().left+t.scrollX):this.doc().scrollLeft=this.snapOffset(i.getBoundingClientRect().left+t.scrollX);const d=r;this.reportProgress(this.timelineEntries.has(d)?d:this.nearestPrecedingTimelineEntry(i)),m(this.wnd),s(!0)})}),e.register("go_text",h.moduleName,(r,s)=>{let i;Array.isArray(r)&&(r.length>1&&(i=r[1]),r=r[0]);const d=F.deserialize(r),a=_(this.wnd.document,new M({href:t.location.href,type:"text/html",text:d,locations:i?new z({otherLocations:new Map([["cssSelector",i]])}):void 0}));if(!a){s(!1);return}this.wnd.requestAnimationFrame(()=>{this.rtl?this.doc().scrollLeft=-this.snapNormOffset(a.getBoundingClientRect().left+t.scrollX):this.doc().scrollLeft=this.snapOffset(a.getBoundingClientRect().left+t.scrollX),this.reportProgress(this.nearestPrecedingTimelineEntry(a.startContainer)),m(this.wnd),s(!0)})}),e.register("go_end",h.moduleName,(r,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;let i;if(this.rtl?i=-this.snapNormOffset(this.cachedScrollWidth-t.innerWidth):i=this.snapOffset(this.cachedScrollWidth),this.doc().scrollLeft===i)return s(!1);this.doc().scrollLeft=i,this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),m(this.wnd),s(!0)})}),e.register("go_start",h.moduleName,(r,s)=>{this.wnd.requestAnimationFrame(()=>{if(this.doc().scrollLeft===0)return s(!1);this.doc().scrollLeft=0,this.reportProgress(this.firstFragmentIfReached()),m(this.wnd),s(!0)})}),e.register("go_prev",h.moduleName,(r,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;let i;this.rtl?i=l(this.normScroll()-t.innerWidth):i=n(t.scrollX-t.innerWidth),this.reportProgress(),i&&(m(this.wnd),this.checkSuspiciousSnap("left",this.wnd.innerWidth)),s(i)})}),e.register("go_next",h.moduleName,(r,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;let i;this.rtl?i=l(this.normScroll()+t.innerWidth):i=n(t.scrollX+t.innerWidth),this.reportProgress(),i&&(m(this.wnd),this.checkSuspiciousSnap("right",this.wnd.innerWidth)),s(i)})}),e.register("unfocus",h.moduleName,(r,s)=>{this.snappingCancelled=!0,m(this.wnd),s(!0)}),e.register("shake",h.moduleName,(r,s)=>{this.shake(),s(!0)}),e.register("focus",h.moduleName,(r,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth,this.snapCurrentOffset(!1,!0),this.reportProgress(),s(!0)})}),e.register("first_visible_locator",h.moduleName,(r,s)=>{const i=X(t,!1);this.comms.send("first_visible_locator",i.serialize()),s(!0)}),e.register("timeline_entries",h.moduleName,(r,s)=>{this.updateTimelineEntries(Array.isArray(r)?r:[],t),t.requestAnimationFrame(()=>this.reportProgress()),s(!0)}),t.addEventListener("touchstart",this.onTouchStarter,{passive:!0}),t.addEventListener("touchend",this.onTouchEnder,{passive:!0}),t.addEventListener("touchmove",this.onTouchMover,{passive:!0}),t.document.addEventListener("touchstart",()=>{}),e.log("ColumnSnapper Mounted"),!0}unmount(t,e){return this.snappingCancelled=!0,e.unregisterAll(h.moduleName),this.resizeObserver.disconnect(),this.mutationObserver.disconnect(),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isSnapProtectionEnabled=!1),t.removeEventListener("touchstart",this.onTouchStarter),t.removeEventListener("touchend",this.onTouchEnder),t.removeEventListener("touchmove",this.onTouchMover),t.removeEventListener("orientationchange",this.onWidthChanger),t.removeEventListener("resize",this.onWidthChanger),t.document.getElementById(T)?.remove(),this.timelineEntries.clear(),e.log("ColumnSnapper Unmounted"),super.unmount(t,e)}};h.moduleName="column_snapper";let E=h;export{E as ColumnSnapper};
35
+ `,t.document.head.appendChild(r),this.resizeObserver=new ResizeObserver(()=>{t.requestAnimationFrame(()=>{t&&W(t)}),this.onWidthChange()}),this.resizeObserver.observe(t.document.body),this.mutationObserver=new MutationObserver(i=>{for(const s of i)if(s.target===this.wnd.document.documentElement){const o=s.oldValue,d=s.target.getAttribute("style"),a=/transform\s*:\s*([^;]+)/,c=o?.match(a),u=d?.match(a);(!c&&!u&&F(o,d)||c&&!u||c&&u&&c[1]!==u[1])&&(t.requestAnimationFrame(()=>{t&&W(t)}),this.onWidthChange())}else t.requestAnimationFrame(()=>this.cachedScrollWidth=this.doc().scrollWidth)}),t.frameElement&&this.mutationObserver.observe(t.frameElement,{attributes:!0,attributeFilter:["style"]}),this.mutationObserver.observe(t.document,{attributes:!0,attributeFilter:["style"]}),this.mutationObserver.observe(t.document.documentElement,{attributes:!0,attributeFilter:["style"]}),e.register("scroll_protection",h.moduleName,(i,s)=>{this.enableSnapProtection(),s(!0)});const n=i=>{const s=this.doc().scrollLeft;return this.doc().scrollLeft=this.snapOffset(i),s!==this.doc().scrollLeft},l=i=>{const s=this.doc().scrollLeft,o=this.snapNormOffset(Math.max(0,Math.min(this.cachedScrollWidth-t.innerWidth,i)));return this.doc().scrollLeft=-o,s!==this.doc().scrollLeft};return t.addEventListener("orientationchange",this.onWidthChanger),t.addEventListener("resize",this.onWidthChanger),t.requestAnimationFrame(()=>this.cachedScrollWidth=this.doc().scrollWidth),e.register("go_progression",h.moduleName,(i,s)=>{const o=i;if(o<0||o>1){e.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),s(!1);return}this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;const a=(this.cachedScrollWidth-t.innerWidth)*o;this.rtl?this.doc().scrollLeft=-this.snapNormOffset(a):this.doc().scrollLeft=this.snapOffset(a),this.reportProgress(),m(this.wnd),s(!0)})}),e.register("go_id",h.moduleName,(i,s)=>{const o=t.document.getElementById(i);if(!o){s(!1);return}this.wnd.requestAnimationFrame(()=>{this.rtl?this.doc().scrollLeft=-this.snapNormOffset(o.getBoundingClientRect().left+t.scrollX):this.doc().scrollLeft=this.snapOffset(o.getBoundingClientRect().left+t.scrollX);const d=i;this.reportProgress(this.timelineEntries.has(d)?d:this.nearestPrecedingTimelineEntry(o)),m(this.wnd),s(!0)})}),e.register("go_text",h.moduleName,(i,s)=>{let o;Array.isArray(i)&&(i.length>1&&(o=i[1]),i=i[0]);const d=N.deserialize(i),a=_(this.wnd.document,new M({href:t.location.href,type:"text/html",text:d,locations:o?new z({otherLocations:new Map([["cssSelector",o]])}):void 0}));if(!a){s(!1);return}this.wnd.requestAnimationFrame(()=>{this.rtl?this.doc().scrollLeft=-this.snapNormOffset(a.getBoundingClientRect().left+t.scrollX):this.doc().scrollLeft=this.snapOffset(a.getBoundingClientRect().left+t.scrollX),this.reportProgress(this.nearestPrecedingTimelineEntry(a.startContainer)),m(this.wnd),s(!0)})}),e.register("go_end",h.moduleName,(i,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;let o;if(this.rtl?o=-this.snapNormOffset(this.cachedScrollWidth-t.innerWidth):o=this.snapOffset(this.cachedScrollWidth),this.doc().scrollLeft===o)return s(!1);this.doc().scrollLeft=o,this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),m(this.wnd),s(!0)})}),e.register("go_start",h.moduleName,(i,s)=>{this.wnd.requestAnimationFrame(()=>{if(this.doc().scrollLeft===0)return s(!1);this.doc().scrollLeft=0,this.reportProgress(this.firstFragmentIfReached()),m(this.wnd),s(!0)})}),e.register("go_prev",h.moduleName,(i,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;let o;this.rtl?o=l(this.normScroll()-t.innerWidth):o=n(t.scrollX-t.innerWidth),this.reportProgress(),o&&(m(this.wnd),this.checkSuspiciousSnap("left",this.wnd.innerWidth)),s(o)})}),e.register("go_next",h.moduleName,(i,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth;let o;this.rtl?o=l(this.normScroll()+t.innerWidth):o=n(t.scrollX+t.innerWidth),this.reportProgress(),o&&(m(this.wnd),this.checkSuspiciousSnap("right",this.wnd.innerWidth)),s(o)})}),e.register("unfocus",h.moduleName,(i,s)=>{this.snappingCancelled=!0,m(this.wnd),s(!0)}),e.register("shake",h.moduleName,(i,s)=>{this.shake(),s(!0)}),e.register("focus",h.moduleName,(i,s)=>{this.wnd.requestAnimationFrame(()=>{this.cachedScrollWidth=this.doc().scrollWidth,this.snapCurrentOffset(!1,!0),this.reportProgress(),s(!0)})}),e.register("first_visible_locator",h.moduleName,(i,s)=>{const o=X(t,!1);this.comms.send("first_visible_locator",o.serialize()),s(!0)}),e.register("timeline_entries",h.moduleName,(i,s)=>{this.updateTimelineEntries(Array.isArray(i)?i:[],t),t.requestAnimationFrame(()=>this.reportProgress()),s(!0)}),t.addEventListener("touchstart",this.onTouchStarter,{passive:!0}),t.addEventListener("touchend",this.onTouchEnder,{passive:!0}),t.addEventListener("touchmove",this.onTouchMover,{passive:!0}),t.document.addEventListener("touchstart",()=>{}),e.log("ColumnSnapper Mounted"),!0}unmount(t,e){return this.snappingCancelled=!0,e.unregisterAll(h.moduleName),this.resizeObserver.disconnect(),this.mutationObserver.disconnect(),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isSnapProtectionEnabled=!1),t.removeEventListener("touchstart",this.onTouchStarter),t.removeEventListener("touchend",this.onTouchEnder),t.removeEventListener("touchmove",this.onTouchMover),t.removeEventListener("orientationchange",this.onWidthChanger),t.removeEventListener("resize",this.onWidthChanger),t.document.getElementById(T)?.remove(),this.timelineEntries.clear(),e.log("ColumnSnapper Unmounted"),super.unmount(t,e)}};h.moduleName="column_snapper";let E=h;export{E as ColumnSnapper};
@@ -1,4 +1,4 @@
1
- import{LocatorText as m,Locator as g,LocatorLocations as p}from"@readium/shared";import{deselect as c,findFirstVisibleLocator as f}from"../../helpers/dom.js";import{Snapper as h}from"./Snapper.js";import{rangeFromLocator as S}from"../../helpers/locator.js";import{forceWebkitRecalc as b}from"../../helpers/document.js";import{PatternAnalyzer as T}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as y}from"../../protection/config.js";const d="readium-scroll-snapper-style",o=class o extends h{constructor(){super(...arguments),this.patternAnalyzer=null,this.lastScrollTime=0,this.isScrollProtectionEnabled=!1,this.initialScrollHandled=!1,this.isScrolling=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce=null,this.handleScroll=r=>{if(this.comms.ready&&!this.isResizing){if(!this.initialScrollHandled){this.lastScrollTop=this.doc().scrollTop,this.initialScrollHandled=!0,this.reportProgress();return}this.isScrolling||(this.isScrolling=!0,this.wnd.requestAnimationFrame(()=>{this.reportProgress();const s=this.doc().scrollTop,l=s-this.lastScrollTop;if(this.lastScrollTop=s,this.isScrollProtectionEnabled&&Math.abs(l)>5){const e=Date.now(),t=e-(this.lastScrollTime||e);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(l>0?"down":"up",Math.abs(l),t)){const n=r.target&&"tagName"in r.target?{tagName:r.target.tagName}:null;this.comms?.send("content_protection",{type:"suspicious_scrolling",timestamp:Date.now(),scrollDelta:l,scrollDirection:l>0?"down":"up",targetElement:n})}this.lastScrollTime=e}this.comms.send("scroll",l),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}hasScrolledPast(r){const s=this.wnd.innerHeight/2;return r.getBoundingClientRect().top<=s+this.wnd.innerHeight*h.CENTER_TOLERANCE}reportProgress(r){if(!this.comms.ready)return;const s=Math.ceil(this.doc().scrollTop),l=this.doc().scrollHeight,e=this.wnd.innerHeight,t=Math.max(0,Math.min(1,s/l)),i=Math.max(0,Math.min(1,(s+e)/l));this.comms.send("progress",{start:t,end:i,fragmentId:r!==void 0?r:this.currentTimelineFragment()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new T(y),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(r,s){this.wnd=r,this.comms=s,this.setupTimelineObserver(),this.initialScrollHandled=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce&&(this.wnd.clearTimeout(this.resizeDebounce),this.resizeDebounce=null),r.navigator.epubReadingSystem&&(r.navigator.epubReadingSystem.layoutStyle="scrolling");const l=r.document.createElement("style");return l.dataset.readium="true",l.id=d,l.textContent=`
1
+ import{LocatorText as g,Locator as m,LocatorLocations as p}from"@readium/shared";import{deselect as a,findFirstVisibleLocator as f}from"../../helpers/dom.js";import{Snapper as c}from"./Snapper.js";import{rangeFromLocator as S}from"../../helpers/locator.js";import{forceWebkitRecalc as T}from"../../helpers/document.js";import{PatternAnalyzer as b}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as y}from"../../protection/config.js";const d="readium-scroll-snapper-style",n=class n extends c{constructor(){super(...arguments),this.patternAnalyzer=null,this.lastScrollTime=0,this.isScrollProtectionEnabled=!1,this.initialScrollHandled=!1,this.isScrolling=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce=null,this.handleScroll=s=>{if(this.comms.ready&&!this.isResizing){if(!this.initialScrollHandled){this.lastScrollTop=this.doc().scrollTop,this.initialScrollHandled=!0,this.reportProgress();return}this.isScrolling||(this.isScrolling=!0,this.wnd.requestAnimationFrame(()=>{this.reportProgress();const t=this.doc().scrollTop,o=t-this.lastScrollTop;if(this.lastScrollTop=t,this.isScrollProtectionEnabled&&Math.abs(o)>5){const e=Date.now(),r=e-(this.lastScrollTime||e);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(o>0?"down":"up",Math.abs(o),r)){const l=s.target&&"tagName"in s.target?{tagName:s.target.tagName}:null;this.comms?.send("content_protection",{type:"suspicious_scrolling",timestamp:Date.now(),scrollDelta:o,scrollDirection:o>0?"down":"up",targetElement:l})}this.lastScrollTime=e}this.comms.send("scroll",o),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}hasScrolledPast(s){const t=this.wnd.innerHeight/2;return s.getBoundingClientRect().top<=t+this.wnd.innerHeight*c.CENTER_TOLERANCE}inCenterBand(s){const t=s.getBoundingClientRect(),o=this.wnd.innerHeight/2,e=this.wnd.innerHeight*c.CENTER_TOLERANCE,r=o-e,i=o+e;return t.height===0?t.top<=i&&t.top>=r:Math.max(0,Math.min(t.bottom,i)-Math.max(t.top,r))/t.height>=c.CENTER_TOLERANCE}fragmentStart(s){return s.getBoundingClientRect().top+this.doc().scrollTop}currentScrollExtent(){return{pos:this.doc().scrollTop,size:this.wnd.innerHeight}}reportProgress(s){if(!this.comms.ready)return;const t=Math.ceil(this.doc().scrollTop),o=this.doc().scrollHeight,e=this.wnd.innerHeight,r=Math.max(0,Math.min(1,t/o)),i=Math.max(0,Math.min(1,(t+e)/o));this.comms.send("progress",{start:r,end:i,fragmentId:s!==void 0?s:this.currentTimelineFragment(),visibleFragmentIds:this.sortedVisibleFragmentIds()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new b(y),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(s,t){this.wnd=s,this.comms=t,this.initialScrollHandled=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce&&(this.wnd.clearTimeout(this.resizeDebounce),this.resizeDebounce=null),s.navigator.epubReadingSystem&&(s.navigator.epubReadingSystem.layoutStyle="scrolling");const o=s.document.createElement("style");return o.dataset.readium="true",o.id=d,o.textContent=`
2
2
  * {
3
3
  scrollbar-width: none; /* for Firefox */
4
4
  }
@@ -6,4 +6,4 @@ import{LocatorText as m,Locator as g,LocatorLocations as p}from"@readium/shared"
6
6
  body::-webkit-scrollbar {
7
7
  display: none; /* for Chrome, Safari, and Opera */
8
8
  }
9
- `,r.document.head.appendChild(l),this.resizeObserver=new ResizeObserver(()=>{this.resizeDebounce&&this.wnd.clearTimeout(this.resizeDebounce),this.isResizing=!0,this.resizeDebounce=this.wnd.setTimeout(()=>{this.isResizing=!1,this.resizeDebounce=null,this.reportProgress()},50)}),this.resizeObserver.observe(r.document.body),r.addEventListener("scroll",this.handleScroll,{passive:!0}),s.register("force_webkit_recalc",o.moduleName,()=>{b(this.wnd);const e=this.doc().scrollTop;e>1?this.doc().scrollTop=e-1:this.doc().scrollTop=e+1,this.doc().scrollTop=e}),s.register("go_progression",o.moduleName,(e,t)=>{const i=e;if(i<0||i>1){s.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=this.doc().offsetHeight*i,this.reportProgress(this.fragmentFromGeometry()),c(this.wnd),t(!0)})}),s.register("go_id",o.moduleName,(e,t)=>{const i=r.document.getElementById(e);if(!i){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=i.getBoundingClientRect().top+r.scrollY-r.innerHeight/2;const n=e;this.reportProgress(this.timelineEntries.has(n)?n:this.nearestPrecedingTimelineEntry(i)),c(this.wnd),t(!0)})}),s.register("go_text",o.moduleName,(e,t)=>{let i;Array.isArray(e)&&(e.length>1&&(i=e[1]),e=e[0]);const n=m.deserialize(e),a=S(this.wnd.document,new g({href:r.location.href,type:"text/html",text:n,locations:i?new p({otherLocations:new Map([["cssSelector",i]])}):void 0}));if(!a){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=a.getBoundingClientRect().top+r.scrollY-r.innerHeight/2,this.reportProgress(this.nearestPrecedingTimelineEntry(a.startContainer)),c(this.wnd),t(!0)})}),s.register("go_start",o.moduleName,(e,t)=>{if(this.doc().scrollTop===0)return t(!1);this.doc().scrollTop=0,this.reportProgress(this.firstFragmentIfReached()),t(!0)}),s.register("go_end",o.moduleName,(e,t)=>{if(this.doc().scrollTop===this.doc().scrollHeight-this.doc().offsetHeight)return t(!1);this.doc().scrollTop=this.doc().scrollHeight-this.doc().offsetHeight,this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),t(!0)}),s.register("unfocus",o.moduleName,(e,t)=>{c(this.wnd),t(!0)}),s.register("scroll_protection",o.moduleName,(e,t)=>{this.enableScrollProtection(),t(!0)}),s.register(["go_next","go_prev"],o.moduleName,(e,t)=>t(!1)),s.register("focus",o.moduleName,(e,t)=>{this.reportProgress(),t(!0)}),s.register("first_visible_locator",o.moduleName,(e,t)=>{const i=f(r,!0);this.comms.send("first_visible_locator",i.serialize()),t(!0)}),s.register("timeline_entries",o.moduleName,(e,t)=>{this.updateTimelineEntries(Array.isArray(e)?e:[],r),t(!0)}),s.log("ScrollSnapper Mounted"),!0}unmount(r,s){return s.unregisterAll(o.moduleName),this.resizeObserver.disconnect(),this.handleScroll&&r.removeEventListener("scroll",this.handleScroll),r.document.getElementById(d)?.remove(),this.timelineObserver?.disconnect(),this.timelineObserver=null,this.visibleFragmentIds.clear(),this.timelineEntries.clear(),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isScrollProtectionEnabled=!1),s.log("ScrollSnapper Unmounted"),!0}};o.moduleName="scroll_snapper";let u=o;export{u as ScrollSnapper};
9
+ `,s.document.head.appendChild(o),this.resizeObserver=new ResizeObserver(()=>{this.resizeDebounce&&this.wnd.clearTimeout(this.resizeDebounce),this.isResizing=!0,this.resizeDebounce=this.wnd.setTimeout(()=>{this.isResizing=!1,this.resizeDebounce=null,this.refreshFragmentStarts(),this.reportProgress()},50)}),this.resizeObserver.observe(s.document.body),s.addEventListener("scroll",this.handleScroll,{passive:!0}),t.register("force_webkit_recalc",n.moduleName,()=>{T(this.wnd);const e=this.doc().scrollTop;e>1?this.doc().scrollTop=e-1:this.doc().scrollTop=e+1,this.doc().scrollTop=e}),t.register("go_progression",n.moduleName,(e,r)=>{const i=e;if(i<0||i>1){t.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),r(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=this.doc().offsetHeight*i,this.reportProgress(this.fragmentFromGeometry()),a(this.wnd),r(!0)})}),t.register("go_id",n.moduleName,(e,r)=>{const i=s.document.getElementById(e);if(!i){r(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=i.getBoundingClientRect().top+s.scrollY-s.innerHeight/2;const l=e;this.reportProgress(this.timelineEntries.has(l)?l:this.nearestPrecedingTimelineEntry(i)),a(this.wnd),r(!0)})}),t.register("go_text",n.moduleName,(e,r)=>{let i;Array.isArray(e)&&(e.length>1&&(i=e[1]),e=e[0]);const l=g.deserialize(e),h=S(this.wnd.document,new m({href:s.location.href,type:"text/html",text:l,locations:i?new p({otherLocations:new Map([["cssSelector",i]])}):void 0}));if(!h){r(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=h.getBoundingClientRect().top+s.scrollY-s.innerHeight/2,this.reportProgress(this.nearestPrecedingTimelineEntry(h.startContainer)),a(this.wnd),r(!0)})}),t.register("go_start",n.moduleName,(e,r)=>{if(this.doc().scrollTop===0)return r(!1);this.doc().scrollTop=0,this.reportProgress(this.firstFragmentIfReached()),r(!0)}),t.register("go_end",n.moduleName,(e,r)=>{if(this.doc().scrollTop===this.doc().scrollHeight-this.doc().offsetHeight)return r(!1);this.doc().scrollTop=this.doc().scrollHeight-this.doc().offsetHeight,this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),r(!0)}),t.register("unfocus",n.moduleName,(e,r)=>{a(this.wnd),r(!0)}),t.register("scroll_protection",n.moduleName,(e,r)=>{this.enableScrollProtection(),r(!0)}),t.register(["go_next","go_prev"],n.moduleName,(e,r)=>r(!1)),t.register("focus",n.moduleName,(e,r)=>{this.reportProgress(),r(!0)}),t.register("first_visible_locator",n.moduleName,(e,r)=>{const i=f(s,!0);this.comms.send("first_visible_locator",i.serialize()),r(!0)}),t.register("timeline_entries",n.moduleName,(e,r)=>{this.updateTimelineEntries(Array.isArray(e)?e:[],s),r(!0)}),t.log("ScrollSnapper Mounted"),!0}unmount(s,t){return t.unregisterAll(n.moduleName),this.resizeObserver.disconnect(),this.handleScroll&&s.removeEventListener("scroll",this.handleScroll),s.document.getElementById(d)?.remove(),this.timelineEntries.clear(),this.cachedFragmentIds=[],this.sortedFragmentIds=[],this.cachedFragmentStarts.clear(),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isScrollProtectionEnabled=!1),t.log("ScrollSnapper Unmounted"),!0}};n.moduleName="scroll_snapper";let u=n;export{u as ScrollSnapper};
@@ -1,5 +1,5 @@
1
- import{Module as m}from"../Module.js";const o="readium-snapper-style",r=class r extends m{constructor(){super(...arguments),this.protected=!1,this.timelineObserver=null,this.timelineEntries=new Map,this.visibleFragmentIds=new Set,this.cachedFragmentIds=[],this.sortedFragmentIds=[]}static inDomOrder(t,e,i){const s=t.get(e),n=t.get(i);return!s||!n?0:s.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1}static isVisibleEntry(t){const e=t.boundingClientRect;if(e.width>0&&e.height>0)return t.isIntersecting;const i=t.rootBounds;return i?e.left<=i.right&&e.right>=i.left&&e.top<=i.bottom&&e.bottom>=i.top:t.isIntersecting}setupTimelineObserver(t="vertical"){this.timelineObserver&&this.timelineObserver.disconnect();const e=`-${50-r.CENTER_TOLERANCE*100}%`;this.timelineObserver=new IntersectionObserver(i=>{for(const s of i)r.isVisibleEntry(s)?this.visibleFragmentIds.add(s.target.id):this.visibleFragmentIds.delete(s.target.id)},{threshold:[.01],rootMargin:t==="vertical"?`${e} 0px ${e} 0px`:`0px ${e} 0px ${e}`})}updateTimelineEntries(t,e){this.cachedFragmentIds=t,this.timelineObserver?.disconnect(),this.visibleFragmentIds.clear(),this.timelineEntries.clear();for(const i of t){const s=e.document.getElementById(i);s&&(this.timelineEntries.set(i,s),this.timelineObserver?.observe(s))}this.sortedFragmentIds=Array.from(this.timelineEntries.keys()).sort((i,s)=>r.inDomOrder(this.timelineEntries,i,s))}nearestPrecedingTimelineEntry(t){let e;for(const i of this.sortedFragmentIds){const s=this.timelineEntries.get(i),n=s.compareDocumentPosition(t);if(s===t||s.contains(t)||(n&Node.DOCUMENT_POSITION_FOLLOWING)!==0)e=i;else break}return e}fragmentFromGeometry(){let t;for(const e of this.sortedFragmentIds){const i=this.timelineEntries.get(e);if(this.hasScrolledPast(i))t=e;else break}return t}firstFragmentIfReached(){const t=this.sortedFragmentIds[0];if(t===void 0)return;const e=this.timelineEntries.get(t);return this.hasScrolledPast(e)?t:void 0}currentTimelineFragment(){return this.visibleFragmentIds.size>0?Array.from(this.visibleFragmentIds).sort((t,e)=>r.inDomOrder(this.timelineEntries,t,e))[0]:this.fragmentFromGeometry()}buildStyles(){return`
1
+ import{Module as l}from"../Module.js";const d="readium-snapper-style",o=class o extends l{constructor(){super(...arguments),this.protected=!1,this.timelineEntries=new Map,this.cachedFragmentIds=[],this.sortedFragmentIds=[],this.cachedFragmentStarts=new Map}static inDomOrder(t,e,r){const s=t.get(e),n=t.get(r);return!s||!n?0:s.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1}updateTimelineEntries(t,e){this.cachedFragmentIds=t,this.timelineEntries.clear(),this.cachedFragmentStarts.clear();for(const r of t){const s=e.document.getElementById(r);s&&this.timelineEntries.set(r,s)}this.sortedFragmentIds=Array.from(this.timelineEntries.keys()).sort((r,s)=>o.inDomOrder(this.timelineEntries,r,s)),this.refreshFragmentStarts()}refreshFragmentStarts(){for(const[t,e]of this.timelineEntries)this.cachedFragmentStarts.set(t,this.fragmentStart(e))}fragmentStart(t){return 0}currentScrollExtent(){return{pos:0,size:0}}nearestPrecedingTimelineEntry(t){let e;for(const r of this.sortedFragmentIds){const s=this.timelineEntries.get(r),n=s.compareDocumentPosition(t);if(s===t||s.contains(t)||(n&Node.DOCUMENT_POSITION_FOLLOWING)!==0)e=r;else break}return e}fragmentFromGeometry(){for(const e of this.sortedFragmentIds){const r=this.timelineEntries.get(e);if(this.inCenterBand(r))return e}let t;for(const e of this.sortedFragmentIds){const r=this.timelineEntries.get(e);if(this.hasScrolledPast(r))t=e;else break}return t}firstFragmentIfReached(){const t=this.sortedFragmentIds[0];if(t===void 0)return;const e=this.timelineEntries.get(t);return this.hasScrolledPast(e)?t:void 0}currentTimelineFragment(){const{pos:t,size:e}=this.currentScrollExtent(),r=t+e/2;let s;for(const n of this.sortedFragmentIds){const i=this.cachedFragmentStarts.get(n);if(i!==void 0)if(i<=r)s=n;else break}return s}sortedVisibleFragmentIds(){const{pos:t,size:e}=this.currentScrollExtent(),r=t+e;let s=-1,n=-1;for(let i=0;i<this.sortedFragmentIds.length;i++){const a=this.cachedFragmentStarts.get(this.sortedFragmentIds[i]);a!==void 0&&(a<=t&&(s=i),a<r&&(n=i))}return s<0&&(s=0),n<s?[]:this.sortedFragmentIds.slice(s,n+1)}inCenterBand(t){return!1}buildStyles(){return`
2
2
  html, body {
3
3
  touch-action: manipulation;
4
4
  user-select: ${this.protected?"none":"auto"};
5
- }`}mount(t,e){const i=t.document.createElement("style");return i.dataset.readium="true",i.id=o,i.textContent=this.buildStyles(),t.document.head.appendChild(i),e.register("protect",r.moduleName,(s,n)=>{this.protected=!0,i.textContent=this.buildStyles(),n(!0)}),e.register("unprotect",r.moduleName,(s,n)=>{this.protected=!1,i.textContent=this.buildStyles(),n(!0)}),e.log("Snapper Mounted"),!0}unmount(t,e){return t.document.getElementById(o)?.remove(),e.log("Snapper Unmounted"),!0}};r.moduleName="snapper",r.CENTER_TOLERANCE=.01;let l=r;export{l as Snapper};
5
+ }`}mount(t,e){const r=t.document.createElement("style");return r.dataset.readium="true",r.id=d,r.textContent=this.buildStyles(),t.document.head.appendChild(r),e.register("protect",o.moduleName,(s,n)=>{this.protected=!0,r.textContent=this.buildStyles(),n(!0)}),e.register("unprotect",o.moduleName,(s,n)=>{this.protected=!1,r.textContent=this.buildStyles(),n(!0)}),e.log("Snapper Mounted"),!0}unmount(t,e){return t.document.getElementById(d)?.remove(),this.timelineEntries.clear(),this.cachedFragmentIds=[],this.sortedFragmentIds=[],this.cachedFragmentStarts.clear(),e.log("Snapper Unmounted"),!0}};o.moduleName="snapper",o.CENTER_TOLERANCE=.01;let c=o;export{c as Snapper};
@@ -1 +1 @@
1
- import{LocatorText as u,Locator as d,LocatorLocations as g}from"@readium/shared";import{deselect as c,findFirstVisibleLocator as m}from"../../helpers/dom.js";import{Snapper as h}from"./Snapper.js";import{rangeFromLocator as p}from"../../helpers/locator.js";import{forceWebkitRecalc as f}from"../../helpers/document.js";import{PatternAnalyzer as T}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as S}from"../../protection/config.js";const o=class o extends h{constructor(){super(...arguments),this.patternAnalyzer=null,this.lastScrollTime=0,this.isScrollProtectionEnabled=!1,this.initialScrollHandled=!1,this.isScrolling=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce=null,this.handleScroll=i=>{if(this.comms.ready&&!this.isResizing){if(!this.initialScrollHandled){this.lastScrollTop=this.doc().scrollTop,this.initialScrollHandled=!0,this.reportProgress();return}this.isScrolling||(this.isScrolling=!0,this.wnd.requestAnimationFrame(()=>{this.reportProgress();const r=this.doc().scrollTop,e=r-this.lastScrollTop;if(this.lastScrollTop=r,this.isScrollProtectionEnabled&&Math.abs(e)>5){const t=Date.now(),s=t-(this.lastScrollTime||t);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(e>0?"down":"up",Math.abs(e),s)){const n=i.target&&"tagName"in i.target?{tagName:i.target.tagName}:null;this.comms?.send("content_protection",{type:"suspicious_scrolling",timestamp:Date.now(),scrollDelta:e,scrollDirection:e>0?"down":"up",targetElement:n})}this.lastScrollTime=t}this.comms.send("scroll",e),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}hasScrolledPast(i){const r=this.wnd.innerHeight/2;return i.getBoundingClientRect().top<=r+this.wnd.innerHeight*h.CENTER_TOLERANCE}reportProgress(i){if(!this.comms.ready)return;const r=Math.ceil(this.doc().scrollTop),e=this.doc().scrollHeight,t=this.wnd.innerHeight,s=Math.max(0,Math.min(1,r/e)),l=Math.max(0,Math.min(1,(r+t)/e));this.comms.send("progress",{start:s,end:l,fragmentId:i!==void 0?i:this.currentTimelineFragment()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new T(S),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(i,r){return this.wnd=i,this.comms=r,this.setupTimelineObserver(),this.initialScrollHandled=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce&&(this.wnd.clearTimeout(this.resizeDebounce),this.resizeDebounce=null),this.resizeObserver=new ResizeObserver(()=>{this.resizeDebounce&&this.wnd.clearTimeout(this.resizeDebounce),this.isResizing=!0,this.resizeDebounce=this.wnd.setTimeout(()=>{this.isResizing=!1,this.resizeDebounce=null,this.reportProgress()},50)}),this.resizeObserver.observe(i.document.body),i.addEventListener("scroll",this.handleScroll,{passive:!0}),r.register("force_webkit_recalc",o.moduleName,()=>{f(this.wnd);const e=this.doc().scrollTop;e>1?this.doc().scrollTop=e-1:this.doc().scrollTop=e+1,this.doc().scrollTop=e}),r.register("go_progression",o.moduleName,(e,t)=>{const s=e;if(s<0||s>1){r.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=this.doc().offsetHeight*s,this.reportProgress(this.fragmentFromGeometry()),c(this.wnd),t(!0)})}),r.register("go_id",o.moduleName,(e,t)=>{const s=i.document.getElementById(e);if(!s){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=s.getBoundingClientRect().top+i.scrollY-i.innerHeight/2;const l=e;this.reportProgress(this.timelineEntries.has(l)?l:this.nearestPrecedingTimelineEntry(s)),c(this.wnd),t(!0)})}),r.register("go_text",o.moduleName,(e,t)=>{let s;Array.isArray(e)&&(e.length>1&&(s=e[1]),e=e[0]);const l=u.deserialize(e),n=p(this.wnd.document,new d({href:i.location.href,type:"text/html",text:l,locations:s?new g({otherLocations:new Map([["cssSelector",s]])}):void 0}));if(!n){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=n.getBoundingClientRect().top+i.scrollY-i.innerHeight/2,this.reportProgress(this.nearestPrecedingTimelineEntry(n.startContainer)),c(this.wnd),t(!0)})}),r.register("go_start",o.moduleName,(e,t)=>{if(this.doc().scrollTop===0)return t(!1);this.doc().scrollTop=0,this.reportProgress(this.firstFragmentIfReached()),t(!0)}),r.register("go_end",o.moduleName,(e,t)=>{if(this.doc().scrollTop===this.doc().scrollHeight-this.doc().offsetHeight)return t(!1);this.doc().scrollTop=this.doc().scrollHeight-this.doc().offsetHeight,this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),t(!0)}),r.register("unfocus",o.moduleName,(e,t)=>{c(this.wnd),t(!0)}),r.register("scroll_protection",o.moduleName,(e,t)=>{this.enableScrollProtection(),t(!0)}),r.register(["go_next","go_prev"],o.moduleName,(e,t)=>t(!1)),r.register("focus",o.moduleName,(e,t)=>{this.reportProgress(),t(!0)}),r.register("first_visible_locator",o.moduleName,(e,t)=>{const s=m(i,!0);r.send("first_visible_locator",s.serialize()),t(!0)}),r.register("timeline_entries",o.moduleName,(e,t)=>{this.updateTimelineEntries(Array.isArray(e)?e:[],i),t(!0)}),r.log("WebPubSnapper Mounted"),!0}unmount(i,r){return r.unregisterAll(o.moduleName),this.resizeObserver.disconnect(),this.handleScroll&&i.removeEventListener("scroll",this.handleScroll),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isScrollProtectionEnabled=!1),this.timelineObserver?.disconnect(),this.timelineObserver=null,this.visibleFragmentIds.clear(),this.timelineEntries.clear(),r.log("WebPubSnapper Unmounted"),!0}};o.moduleName="webpub_snapper";let a=o;export{a as WebPubSnapper};
1
+ import{LocatorText as d,Locator as g,LocatorLocations as u}from"@readium/shared";import{deselect as c,findFirstVisibleLocator as m}from"../../helpers/dom.js";import{Snapper as a}from"./Snapper.js";import{rangeFromLocator as p}from"../../helpers/locator.js";import{forceWebkitRecalc as f}from"../../helpers/document.js";import{PatternAnalyzer as T}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as S}from"../../protection/config.js";const o=class o extends a{constructor(){super(...arguments),this.patternAnalyzer=null,this.lastScrollTime=0,this.isScrollProtectionEnabled=!1,this.initialScrollHandled=!1,this.isScrolling=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce=null,this.handleScroll=s=>{if(this.comms.ready&&!this.isResizing){if(!this.initialScrollHandled){this.lastScrollTop=this.doc().scrollTop,this.initialScrollHandled=!0,this.reportProgress();return}this.isScrolling||(this.isScrolling=!0,this.wnd.requestAnimationFrame(()=>{this.reportProgress();const r=this.doc().scrollTop,e=r-this.lastScrollTop;if(this.lastScrollTop=r,this.isScrollProtectionEnabled&&Math.abs(e)>5){const t=Date.now(),i=t-(this.lastScrollTime||t);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(e>0?"down":"up",Math.abs(e),i)){const l=s.target&&"tagName"in s.target?{tagName:s.target.tagName}:null;this.comms?.send("content_protection",{type:"suspicious_scrolling",timestamp:Date.now(),scrollDelta:e,scrollDirection:e>0?"down":"up",targetElement:l})}this.lastScrollTime=t}this.comms.send("scroll",e),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}hasScrolledPast(s){const r=this.wnd.innerHeight/2;return s.getBoundingClientRect().top<=r+this.wnd.innerHeight*a.CENTER_TOLERANCE}inCenterBand(s){const r=s.getBoundingClientRect(),e=this.wnd.innerHeight/2,t=this.wnd.innerHeight*a.CENTER_TOLERANCE,i=e-t,n=e+t;return r.height===0?r.top<=n&&r.top>=i:Math.max(0,Math.min(r.bottom,n)-Math.max(r.top,i))/r.height>=a.CENTER_TOLERANCE}fragmentStart(s){return s.getBoundingClientRect().top+this.doc().scrollTop}currentScrollExtent(){return{pos:this.doc().scrollTop,size:this.wnd.innerHeight}}reportProgress(s){if(!this.comms.ready)return;const r=Math.ceil(this.doc().scrollTop),e=this.doc().scrollHeight,t=this.wnd.innerHeight,i=Math.max(0,Math.min(1,r/e)),n=Math.max(0,Math.min(1,(r+t)/e));this.comms.send("progress",{start:i,end:n,fragmentId:s!==void 0?s:this.currentTimelineFragment(),visibleFragmentIds:this.sortedVisibleFragmentIds()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new T(S),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(s,r){return this.wnd=s,this.comms=r,this.initialScrollHandled=!1,this.lastScrollTop=0,this.isResizing=!1,this.resizeDebounce&&(this.wnd.clearTimeout(this.resizeDebounce),this.resizeDebounce=null),this.resizeObserver=new ResizeObserver(()=>{this.resizeDebounce&&this.wnd.clearTimeout(this.resizeDebounce),this.isResizing=!0,this.resizeDebounce=this.wnd.setTimeout(()=>{this.isResizing=!1,this.resizeDebounce=null,this.refreshFragmentStarts(),this.reportProgress()},50)}),this.resizeObserver.observe(s.document.body),s.addEventListener("scroll",this.handleScroll,{passive:!0}),r.register("force_webkit_recalc",o.moduleName,()=>{f(this.wnd);const e=this.doc().scrollTop;e>1?this.doc().scrollTop=e-1:this.doc().scrollTop=e+1,this.doc().scrollTop=e}),r.register("go_progression",o.moduleName,(e,t)=>{const i=e;if(i<0||i>1){r.send("error",{message:"go_progression must be given a position from 0.0 to 1.0"}),t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=this.doc().offsetHeight*i,this.reportProgress(this.fragmentFromGeometry()),c(this.wnd),t(!0)})}),r.register("go_id",o.moduleName,(e,t)=>{const i=s.document.getElementById(e);if(!i){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=i.getBoundingClientRect().top+s.scrollY-s.innerHeight/2;const n=e;this.reportProgress(this.timelineEntries.has(n)?n:this.nearestPrecedingTimelineEntry(i)),c(this.wnd),t(!0)})}),r.register("go_text",o.moduleName,(e,t)=>{let i;Array.isArray(e)&&(e.length>1&&(i=e[1]),e=e[0]);const n=d.deserialize(e),l=p(this.wnd.document,new g({href:s.location.href,type:"text/html",text:n,locations:i?new u({otherLocations:new Map([["cssSelector",i]])}):void 0}));if(!l){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=l.getBoundingClientRect().top+s.scrollY-s.innerHeight/2,this.reportProgress(this.nearestPrecedingTimelineEntry(l.startContainer)),c(this.wnd),t(!0)})}),r.register("go_start",o.moduleName,(e,t)=>{if(this.doc().scrollTop===0)return t(!1);this.doc().scrollTop=0,this.reportProgress(this.firstFragmentIfReached()),t(!0)}),r.register("go_end",o.moduleName,(e,t)=>{if(this.doc().scrollTop===this.doc().scrollHeight-this.doc().offsetHeight)return t(!1);this.doc().scrollTop=this.doc().scrollHeight-this.doc().offsetHeight,this.reportProgress(this.sortedFragmentIds[this.sortedFragmentIds.length-1]),t(!0)}),r.register("unfocus",o.moduleName,(e,t)=>{c(this.wnd),t(!0)}),r.register("scroll_protection",o.moduleName,(e,t)=>{this.enableScrollProtection(),t(!0)}),r.register(["go_next","go_prev"],o.moduleName,(e,t)=>t(!1)),r.register("focus",o.moduleName,(e,t)=>{this.reportProgress(),t(!0)}),r.register("first_visible_locator",o.moduleName,(e,t)=>{const i=m(s,!0);r.send("first_visible_locator",i.serialize()),t(!0)}),r.register("timeline_entries",o.moduleName,(e,t)=>{this.updateTimelineEntries(Array.isArray(e)?e:[],s),t(!0)}),r.log("WebPubSnapper Mounted"),!0}unmount(s,r){return r.unregisterAll(o.moduleName),this.resizeObserver.disconnect(),this.handleScroll&&s.removeEventListener("scroll",this.handleScroll),this.patternAnalyzer&&(this.patternAnalyzer.clear(),this.patternAnalyzer=null,this.isScrollProtectionEnabled=!1),this.timelineEntries.clear(),this.cachedFragmentIds=[],this.sortedFragmentIds=[],this.cachedFragmentStarts.clear(),r.log("WebPubSnapper Unmounted"),!0}};o.moduleName="webpub_snapper";let h=o;export{h as WebPubSnapper};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator-html-injectables",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "type": "module",
5
5
  "description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator",
6
6
  "author": "readium",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@readium/helpers": "1.0.0",
48
- "@readium/shared": "2.3.2"
48
+ "@readium/shared": "2.4.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "css-selector-generator": "^3.8.0",
@@ -71,8 +71,7 @@ export class CJKVerticalSnapper extends Snapper {
71
71
  protected hasScrolledPast(el: Element): boolean {
72
72
  // go_id/go_text center the target in the viewport, not scroll it to the edge —
73
73
  // so "reached" has to mean "crossed the viewport's horizontal center", the same
74
- // line navigation scrolls to, not the leading edge. The tolerance matches
75
- // setupTimelineObserver's rootMargin so both agree even with scrollLeft rounding.
74
+ // line navigation scrolls to, not the leading edge.
76
75
  const rect = el.getBoundingClientRect();
77
76
  const center = this.wnd.innerWidth / 2;
78
77
  const tolerance = this.wnd.innerWidth * Snapper.CENTER_TOLERANCE;
@@ -81,6 +80,40 @@ export class CJKVerticalSnapper extends Snapper {
81
80
  return this.verticalLR ? rect.right <= center + tolerance : rect.left >= center - tolerance;
82
81
  }
83
82
 
83
+ /**
84
+ * Overlap as a fraction of the element's own width, matching IntersectionObserver's
85
+ * intersectionRatio — a plain overlap test would keep a large wrapping element "in
86
+ * band" for its entire on-screen span, masking smaller fragments nested inside it.
87
+ * Direction-agnostic (unlike `hasScrolledPast`): overlap ratio is the same question
88
+ * regardless of vertical-lr/vertical-rl.
89
+ */
90
+ protected inCenterBand(el: Element): boolean {
91
+ const rect = el.getBoundingClientRect();
92
+ const center = this.wnd.innerWidth / 2;
93
+ const tolerance = this.wnd.innerWidth * Snapper.CENTER_TOLERANCE;
94
+ const bandLeft = center - tolerance;
95
+ const bandRight = center + tolerance;
96
+ if (rect.width === 0) {
97
+ // Zero-area landmark: ratio is undefined (0/0) by spec — fall back to a point check.
98
+ return rect.left <= bandRight && rect.left >= bandLeft;
99
+ }
100
+ const overlap = Math.max(0, Math.min(rect.right, bandRight) - Math.max(rect.left, bandLeft));
101
+ return overlap / rect.width >= Snapper.CENTER_TOLERANCE;
102
+ }
103
+
104
+ /**
105
+ * Document-absolute (scrollLeft-independent) leading-edge position. Uses the same
106
+ * `Math.abs(scrollLeft)` normalization as `reportProgress` so this stays in the same
107
+ * coordinate space as the live scroll position regardless of vertical-lr/rl sign.
108
+ */
109
+ protected fragmentStart(el: Element): number {
110
+ return el.getBoundingClientRect().left + Math.abs(this.doc().scrollLeft);
111
+ }
112
+
113
+ protected currentScrollExtent(): { pos: number; size: number } {
114
+ return { pos: Math.abs(this.doc().scrollLeft), size: this.wnd.innerWidth };
115
+ }
116
+
84
117
  private reportProgress(forcedFragmentId?: string) {
85
118
  if (!this.comms.ready) return;
86
119
  const scrollWidth = this.doc().scrollWidth;
@@ -96,7 +129,8 @@ export class CJKVerticalSnapper extends Snapper {
96
129
  this.comms.send("progress", {
97
130
  start: progress,
98
131
  end: viewportEnd,
99
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
132
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
133
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
100
134
  });
101
135
  }
102
136
 
@@ -164,7 +198,6 @@ export class CJKVerticalSnapper extends Snapper {
164
198
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
165
199
  this.wnd = wnd;
166
200
  this.comms = comms;
167
- this.setupTimelineObserver("horizontal");
168
201
 
169
202
  this.initialScrollHandled = false;
170
203
  this.lastScrollLeft = 0;
@@ -203,6 +236,7 @@ export class CJKVerticalSnapper extends Snapper {
203
236
  this.resizeDebounce = this.wnd.setTimeout(() => {
204
237
  this.isResizing = false;
205
238
  this.resizeDebounce = null;
239
+ this.refreshFragmentStarts();
206
240
  this.reportProgress();
207
241
  }, 50);
208
242
  });
@@ -355,10 +389,10 @@ export class CJKVerticalSnapper extends Snapper {
355
389
  this.isScrollProtectionEnabled = false;
356
390
  }
357
391
 
358
- this.timelineObserver?.disconnect();
359
- this.timelineObserver = null;
360
- this.visibleFragmentIds.clear();
361
392
  this.timelineEntries.clear();
393
+ this.cachedFragmentIds = [];
394
+ this.sortedFragmentIds = [];
395
+ this.cachedFragmentStarts.clear();
362
396
 
363
397
  comms.log("CJKVerticalSnapper Unmounted");
364
398
  return true;
@@ -105,6 +105,18 @@ export class ColumnSnapper extends Snapper {
105
105
  return result;
106
106
  }
107
107
 
108
+ /**
109
+ * Tests each fragment's rect directly against the current column viewport, returning
110
+ * every one actually on screen rather than only the last one to have started.
111
+ */
112
+ protected sortedVisibleFragmentIds(): string[] {
113
+ const vw = this.wnd.innerWidth;
114
+ return this.sortedFragmentIds.filter(id => {
115
+ const rect = this.timelineEntries.get(id)!.getBoundingClientRect();
116
+ return this.rtl ? (rect.left < vw && rect.right > 0) : (rect.right > 0 && rect.left < vw);
117
+ });
118
+ }
119
+
108
120
  reportProgress(forcedFragmentId?: string) {
109
121
  const scrollWidth = this.cachedScrollWidth;
110
122
  const viewportWidth = this.wnd.innerWidth;
@@ -117,7 +129,8 @@ export class ColumnSnapper extends Snapper {
117
129
  this.comms.send("progress", {
118
130
  start: progress,
119
131
  end: viewportEnd,
120
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
132
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
133
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
121
134
  });
122
135
  }
123
136
 
@@ -40,12 +40,39 @@ export class ScrollSnapper extends Snapper {
40
40
  protected hasScrolledPast(el: Element): boolean {
41
41
  // go_id/go_text center the target in the viewport, not scroll it to the top —
42
42
  // so "reached" has to mean "crossed the viewport's vertical center", the same
43
- // line navigation scrolls to, not the top/bottom edge. The tolerance matches
44
- // setupTimelineObserver's rootMargin so both agree even with scrollTop rounding.
43
+ // line navigation scrolls to, not the top/bottom edge.
45
44
  const center = this.wnd.innerHeight / 2;
46
45
  return el.getBoundingClientRect().top <= center + this.wnd.innerHeight * Snapper.CENTER_TOLERANCE;
47
46
  }
48
47
 
48
+ /**
49
+ * Overlap as a fraction of the element's own height, matching IntersectionObserver's
50
+ * intersectionRatio — a plain overlap test would keep a large wrapping element "in
51
+ * band" for its entire on-screen span, masking smaller fragments nested inside it.
52
+ */
53
+ protected inCenterBand(el: Element): boolean {
54
+ const rect = el.getBoundingClientRect();
55
+ const center = this.wnd.innerHeight / 2;
56
+ const tolerance = this.wnd.innerHeight * Snapper.CENTER_TOLERANCE;
57
+ const bandTop = center - tolerance;
58
+ const bandBottom = center + tolerance;
59
+ if (rect.height === 0) {
60
+ // Zero-area landmark: ratio is undefined (0/0) by spec — fall back to a point check.
61
+ return rect.top <= bandBottom && rect.top >= bandTop;
62
+ }
63
+ const overlap = Math.max(0, Math.min(rect.bottom, bandBottom) - Math.max(rect.top, bandTop));
64
+ return overlap / rect.height >= Snapper.CENTER_TOLERANCE;
65
+ }
66
+
67
+ /** Document-absolute (scrollTop-independent) leading-edge position. */
68
+ protected fragmentStart(el: Element): number {
69
+ return el.getBoundingClientRect().top + this.doc().scrollTop;
70
+ }
71
+
72
+ protected currentScrollExtent(): { pos: number; size: number } {
73
+ return { pos: this.doc().scrollTop, size: this.wnd.innerHeight };
74
+ }
75
+
49
76
  private reportProgress(forcedFragmentId?: string) {
50
77
  if (!this.comms.ready) return;
51
78
  // We have to round up the scroll position because
@@ -60,7 +87,8 @@ export class ScrollSnapper extends Snapper {
60
87
  this.comms.send("progress", {
61
88
  start: progress,
62
89
  end: viewportEnd,
63
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
90
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
91
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
64
92
  });
65
93
  }
66
94
 
@@ -134,7 +162,6 @@ export class ScrollSnapper extends Snapper {
134
162
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
135
163
  this.wnd = wnd;
136
164
  this.comms = comms;
137
- this.setupTimelineObserver();
138
165
 
139
166
  this.initialScrollHandled = false;
140
167
  this.lastScrollTop = 0;
@@ -173,6 +200,7 @@ export class ScrollSnapper extends Snapper {
173
200
  this.resizeDebounce = this.wnd.setTimeout(() => {
174
201
  this.isResizing = false;
175
202
  this.resizeDebounce = null;
203
+ this.refreshFragmentStarts();
176
204
  this.reportProgress();
177
205
  }, 50);
178
206
  });
@@ -322,10 +350,10 @@ export class ScrollSnapper extends Snapper {
322
350
  this.resizeObserver.disconnect();
323
351
  if (this.handleScroll) wnd.removeEventListener("scroll", this.handleScroll);
324
352
  wnd.document.getElementById(SCROLL_SNAPPER_STYLE_ID)?.remove();
325
- this.timelineObserver?.disconnect();
326
- this.timelineObserver = null;
327
- this.visibleFragmentIds.clear();
328
353
  this.timelineEntries.clear();
354
+ this.cachedFragmentIds = [];
355
+ this.sortedFragmentIds = [];
356
+ this.cachedFragmentStarts.clear();
329
357
 
330
358
  if (this.patternAnalyzer) {
331
359
  this.patternAnalyzer.clear();
@@ -8,21 +8,24 @@ const SNAPPER_STYLE_ID = "readium-snapper-style";
8
8
  export abstract class Snapper extends Module {
9
9
  static readonly moduleName: ModuleName = "snapper";
10
10
 
11
- // Shared by the observer's rootMargin and every hasScrolledPast implementation so both
12
- // stay pinned to the exact same boundary. Sized to comfortably exceed realistic
13
- // scrollTop-rounding error (~1px) while still reading as "the center", not a wide band.
11
+ // Shared by every hasScrolledPast/inCenterBand implementation so they stay pinned to the
12
+ // exact same boundary. Sized to comfortably exceed realistic scrollTop-rounding error
13
+ // (~1px) while still reading as "the center", not a wide band.
14
14
  protected static readonly CENTER_TOLERANCE = 0.01;
15
15
 
16
16
  private protected = false;
17
17
 
18
18
  // Timeline fragment tracking
19
- protected timelineObserver: IntersectionObserver | null = null;
20
19
  protected timelineEntries: Map<string, Element> = new Map();
21
- protected visibleFragmentIds: Set<string> = new Set();
22
20
  protected cachedFragmentIds: string[] = [];
23
21
  // DOM-order-sorted fragment ids, recomputed only when entries are (re)populated —
24
22
  // sorting on every progress report (i.e. every scroll frame) is wasted work.
25
23
  protected sortedFragmentIds: string[] = [];
24
+ // Document-absolute leading-edge position per fragment, refreshed on (re)population and
25
+ // resize only, never per scroll frame. A single point, not a start/end range: some TOC
26
+ // ids sit on wrapping elements that contain everything after them (a whole chapter's
27
+ // <section>), so an element's own height isn't a trustworthy measure of where it ends.
28
+ protected cachedFragmentStarts: Map<string, number> = new Map();
26
29
 
27
30
  private static inDomOrder(entries: Map<string, Element>, a: string, b: string): number {
28
31
  const ea = entries.get(a);
@@ -33,76 +36,56 @@ export abstract class Snapper extends Module {
33
36
  }
34
37
 
35
38
  /**
36
- * A zero-area target (e.g. an empty `<a id="…">` landmark) always has
37
- * intersectionRatio 0 by spec, so `isIntersecting` can never become true for it —
38
- * no threshold value changes that. For those entries, fall back to a geometric
39
- * containment check against `rootBounds` instead of trusting `isIntersecting`.
40
- */
41
- private static isVisibleEntry(entry: IntersectionObserverEntry): boolean {
42
- const rect = entry.boundingClientRect;
43
- // Element has real width and height: isIntersecting is spec-correct, trust it as-is.
44
- if (rect.width > 0 && rect.height > 0) return entry.isIntersecting;
45
- // Zero-width or zero-height element: isIntersecting is stuck at false (ratio is
46
- // area / 0, defined as 0), so it can't be trusted. Do our own overlap check instead.
47
- const root = entry.rootBounds;
48
- if (!root) return entry.isIntersecting;
49
- return rect.left <= root.right && rect.right >= root.left &&
50
- rect.top <= root.bottom && rect.bottom >= root.top;
51
- }
52
-
53
- /**
54
- * Collapses the observer's root to a thin band around the same center line
55
- * `hasScrolledPast` tests against (go_id/go_text navigate by centering the target, not by
56
- * aligning it to an edge), so "intersecting" and "scrolled past" can't disagree about
57
- * where "current" is. A literal zero-height root would make every intersection area zero
58
- * — isIntersecting would never fire — so CENTER_TOLERANCE leaves it a real, if thin, band.
59
- * Percentage-based margins track viewport resizes automatically, no re-setup needed.
60
- */
61
- protected setupTimelineObserver(axis: "vertical" | "horizontal" = "vertical"): void {
62
- if (this.timelineObserver) this.timelineObserver.disconnect();
63
- const inset = `-${50 - Snapper.CENTER_TOLERANCE * 100}%`;
64
- this.timelineObserver = new IntersectionObserver(
65
- (entries) => {
66
- for (const entry of entries) {
67
- if (Snapper.isVisibleEntry(entry))
68
- this.visibleFragmentIds.add((entry.target as HTMLElement).id);
69
- else
70
- this.visibleFragmentIds.delete((entry.target as HTMLElement).id);
71
- }
72
- },
73
- {
74
- threshold: [0.01],
75
- rootMargin: axis === "vertical" ? `${inset} 0px ${inset} 0px` : `0px ${inset} 0px ${inset}`
76
- }
77
- );
78
- }
79
-
80
- /**
81
- * Populates `timelineEntries`/`sortedFragmentIds` from a fresh id list, and
82
- * (re)starts IntersectionObserver-based visibility tracking for subclasses that use it.
83
- * Shared by all snappers' `timeline_entries` comms handler so DOM-order sorting and
84
- * observer bookkeeping only happen once per (re)population, not per report.
39
+ * Populates `timelineEntries`/`sortedFragmentIds` from a fresh id list, and refreshes
40
+ * `cachedFragmentStarts` for the new elements. Shared by all snappers' `timeline_entries`
41
+ * comms handler so DOM-order sorting and position caching only happen once per
42
+ * (re)population, not per report.
85
43
  */
86
44
  protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void {
87
45
  this.cachedFragmentIds = ids;
88
- this.timelineObserver?.disconnect();
89
- this.visibleFragmentIds.clear();
90
46
  this.timelineEntries.clear();
47
+ this.cachedFragmentStarts.clear();
91
48
  for (const id of ids) {
92
49
  const el = wnd.document.getElementById(id);
93
- if (el) {
94
- this.timelineEntries.set(id, el);
95
- this.timelineObserver?.observe(el);
96
- }
50
+ if (el) this.timelineEntries.set(id, el);
97
51
  }
98
52
  this.sortedFragmentIds = Array.from(this.timelineEntries.keys())
99
53
  .sort((a, b) => Snapper.inDomOrder(this.timelineEntries, a, b));
54
+ this.refreshFragmentStarts();
55
+ }
56
+
57
+ /**
58
+ * Recomputes `cachedFragmentStarts` for every tracked fragment. Call whenever layout may
59
+ * have changed — on (re)population here, and from each snapper's own resize handler.
60
+ */
61
+ protected refreshFragmentStarts(): void {
62
+ for (const [id, el] of this.timelineEntries) {
63
+ this.cachedFragmentStarts.set(id, this.fragmentStart(el));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Axis-appropriate, document-absolute leading-edge position of `el`. Subclasses with a
69
+ * scroll axis override this; `ColumnSnapper` doesn't use the cache (it has its own
70
+ * rect-based visibility test) so keeps this no-op default.
71
+ */
72
+ protected fragmentStart(_el: Element): number {
73
+ return 0;
74
+ }
75
+
76
+ /**
77
+ * The current viewport's position and size along the scroll axis, in the same
78
+ * document-absolute coordinate space as `fragmentStart`. Paired with it to test
79
+ * on-screen-ness without any per-frame geometry read.
80
+ */
81
+ protected currentScrollExtent(): { pos: number; size: number } {
82
+ return { pos: 0, size: 0 };
100
83
  }
101
84
 
102
85
  /**
103
86
  * Returns the nearest fragment at or before `node` in DOM (reading) order, with no
104
87
  * dependency on rendered geometry — safe to call for programmatic navigation where the
105
- * target is already known, regardless of whether layout/IntersectionObserver has settled.
88
+ * target is already known, regardless of whether layout has settled.
106
89
  */
107
90
  protected nearestPrecedingTimelineEntry(node: Node): string | undefined {
108
91
  let nearestId: string | undefined;
@@ -118,12 +101,17 @@ export abstract class Snapper extends Module {
118
101
  }
119
102
 
120
103
  /**
121
- * Rect-based scan: last element in DOM order whose leading edge has already been
122
- * scrolled past. Subclasses implement `hasScrolledPast` for their layout/axis.
123
- * Exposed so callers can force an accurate one-off geometry check (e.g. right after
124
- * a `go_progression` jump) instead of trusting a possibly-stale visibility cache.
104
+ * Rect-based scan for an accurate one-off read right after a programmatic jump
105
+ * (go_id/go_text), before the next scroll event would otherwise refresh things: first
106
+ * checks for a fragment currently in the center band, and only if none is currently in
107
+ * the band falls back to the last fragment whose leading edge has been scrolled past.
125
108
  */
126
109
  protected fragmentFromGeometry(): string | undefined {
110
+ for (const id of this.sortedFragmentIds) {
111
+ const el = this.timelineEntries.get(id)!;
112
+ if (this.inCenterBand(el)) return id;
113
+ }
114
+
127
115
  let nearestId: string | undefined;
128
116
  for (const id of this.sortedFragmentIds) {
129
117
  const el = this.timelineEntries.get(id)!;
@@ -148,20 +136,43 @@ export abstract class Snapper extends Module {
148
136
  }
149
137
 
150
138
  /**
151
- * Returns the ID of the currently active timeline fragment:
152
- * 1. Primary — IntersectionObserver: returns the first visible element in DOM (reading) order.
153
- * This is direction-agnostic; works for LTR, RTL, and vertical writing modes. Reliable for
154
- * continuous, gradual scrolling.
155
- * 2. Fallback — rect scan (`fragmentFromGeometry`): used when IntersectionObserver hasn't
156
- * fired yet. Not reliable right after an instantaneous programmatic jump — callers that
157
- * know the target (goTo-style navigation) should bypass this method entirely.
139
+ * Returns the ID of the currently active timeline fragment: the last fragment in DOM
140
+ * order whose cached leading edge is at or before the viewport's center point — the same
141
+ * center line `hasScrolledPast`/`go_id`/`go_text` navigate to.
158
142
  */
159
143
  protected currentTimelineFragment(): string | undefined {
160
- if (this.visibleFragmentIds.size > 0) {
161
- return Array.from(this.visibleFragmentIds)
162
- .sort((a, b) => Snapper.inDomOrder(this.timelineEntries, a, b))[0];
144
+ const { pos, size } = this.currentScrollExtent();
145
+ const center = pos + size / 2;
146
+ let nearestId: string | undefined;
147
+ for (const id of this.sortedFragmentIds) {
148
+ const start = this.cachedFragmentStarts.get(id);
149
+ if (start === undefined) continue;
150
+ if (start <= center) nearestId = id;
151
+ else break;
152
+ }
153
+ return nearestId;
154
+ }
155
+
156
+ /**
157
+ * Every timeline fragment currently on screen, in DOM order — not just the single one
158
+ * `currentTimelineFragment` picks. Since fragments are ordered points, not ranges, "on
159
+ * screen" means the run of fragments whose leading edges fall between whichever one
160
+ * governs the viewport's top edge and whichever falls before its bottom edge.
161
+ */
162
+ protected sortedVisibleFragmentIds(): string[] {
163
+ const { pos, size } = this.currentScrollExtent();
164
+ const bottom = pos + size;
165
+ let from = -1;
166
+ let to = -1;
167
+ for (let i = 0; i < this.sortedFragmentIds.length; i++) {
168
+ const start = this.cachedFragmentStarts.get(this.sortedFragmentIds[i]);
169
+ if (start === undefined) continue;
170
+ if (start <= pos) from = i;
171
+ if (start < bottom) to = i;
163
172
  }
164
- return this.fragmentFromGeometry();
173
+ if (from < 0) from = 0;
174
+ if (to < from) return [];
175
+ return this.sortedFragmentIds.slice(from, to + 1);
165
176
  }
166
177
 
167
178
  /**
@@ -171,6 +182,15 @@ export abstract class Snapper extends Module {
171
182
  */
172
183
  protected abstract hasScrolledPast(el: Element): boolean;
173
184
 
185
+ /**
186
+ * Whether the element currently overlaps the viewport's center band, not merely
187
+ * "already scrolled past" it. Defaults to `false` for subclasses with no center-line
188
+ * concept (e.g. paginated `ColumnSnapper`).
189
+ */
190
+ protected inCenterBand(_el: Element): boolean {
191
+ return false;
192
+ }
193
+
174
194
  buildStyles() {
175
195
  return `
176
196
  html, body {
@@ -204,6 +224,11 @@ export abstract class Snapper extends Module {
204
224
  unmount(wnd: ReadiumWindow, comms: Comms): boolean {
205
225
  wnd.document.getElementById(SNAPPER_STYLE_ID)?.remove();
206
226
 
227
+ this.timelineEntries.clear();
228
+ this.cachedFragmentIds = [];
229
+ this.sortedFragmentIds = [];
230
+ this.cachedFragmentStarts.clear();
231
+
207
232
  comms.log("Snapper Unmounted");
208
233
  return true;
209
234
  }
@@ -34,12 +34,39 @@ export class WebPubSnapper extends Snapper {
34
34
  protected hasScrolledPast(el: Element): boolean {
35
35
  // go_id/go_text center the target in the viewport, not scroll it to the top —
36
36
  // so "reached" has to mean "crossed the viewport's vertical center", the same
37
- // line navigation scrolls to, not the top/bottom edge. The tolerance matches
38
- // setupTimelineObserver's rootMargin so both agree even with scrollTop rounding.
37
+ // line navigation scrolls to, not the top/bottom edge.
39
38
  const center = this.wnd.innerHeight / 2;
40
39
  return el.getBoundingClientRect().top <= center + this.wnd.innerHeight * Snapper.CENTER_TOLERANCE;
41
40
  }
42
41
 
42
+ /**
43
+ * Overlap as a fraction of the element's own height, matching IntersectionObserver's
44
+ * intersectionRatio — a plain overlap test would keep a large wrapping element "in
45
+ * band" for its entire on-screen span, masking smaller fragments nested inside it.
46
+ */
47
+ protected inCenterBand(el: Element): boolean {
48
+ const rect = el.getBoundingClientRect();
49
+ const center = this.wnd.innerHeight / 2;
50
+ const tolerance = this.wnd.innerHeight * Snapper.CENTER_TOLERANCE;
51
+ const bandTop = center - tolerance;
52
+ const bandBottom = center + tolerance;
53
+ if (rect.height === 0) {
54
+ // Zero-area landmark: ratio is undefined (0/0) by spec — fall back to a point check.
55
+ return rect.top <= bandBottom && rect.top >= bandTop;
56
+ }
57
+ const overlap = Math.max(0, Math.min(rect.bottom, bandBottom) - Math.max(rect.top, bandTop));
58
+ return overlap / rect.height >= Snapper.CENTER_TOLERANCE;
59
+ }
60
+
61
+ /** Document-absolute (scrollTop-independent) leading-edge position. */
62
+ protected fragmentStart(el: Element): number {
63
+ return el.getBoundingClientRect().top + this.doc().scrollTop;
64
+ }
65
+
66
+ protected currentScrollExtent(): { pos: number; size: number } {
67
+ return { pos: this.doc().scrollTop, size: this.wnd.innerHeight };
68
+ }
69
+
43
70
  private reportProgress(forcedFragmentId?: string) {
44
71
  if (!this.comms.ready) return;
45
72
 
@@ -52,7 +79,8 @@ export class WebPubSnapper extends Snapper {
52
79
  this.comms.send("progress", {
53
80
  start: progress,
54
81
  end: viewportEnd,
55
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
82
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
83
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
56
84
  });
57
85
  }
58
86
 
@@ -125,7 +153,6 @@ export class WebPubSnapper extends Snapper {
125
153
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
126
154
  this.wnd = wnd;
127
155
  this.comms = comms;
128
- this.setupTimelineObserver();
129
156
 
130
157
  this.initialScrollHandled = false;
131
158
  this.lastScrollTop = 0;
@@ -145,6 +172,7 @@ export class WebPubSnapper extends Snapper {
145
172
  this.resizeDebounce = this.wnd.setTimeout(() => {
146
173
  this.isResizing = false;
147
174
  this.resizeDebounce = null;
175
+ this.refreshFragmentStarts();
148
176
  this.reportProgress();
149
177
  }, 50);
150
178
  });
@@ -298,10 +326,10 @@ export class WebPubSnapper extends Snapper {
298
326
  this.isScrollProtectionEnabled = false;
299
327
  }
300
328
 
301
- this.timelineObserver?.disconnect();
302
- this.timelineObserver = null;
303
- this.visibleFragmentIds.clear();
304
329
  this.timelineEntries.clear();
330
+ this.cachedFragmentIds = [];
331
+ this.sortedFragmentIds = [];
332
+ this.cachedFragmentStarts.clear();
305
333
 
306
334
  comms.log("WebPubSnapper Unmounted");
307
335
  return true;
@@ -51,6 +51,24 @@ export declare class CJKVerticalSnapper extends Snapper {
51
51
  /** Total horizontally scrollable distance (magnitude). */
52
52
  private scrollable;
53
53
  protected hasScrolledPast(el: Element): boolean;
54
+ /**
55
+ * Overlap as a fraction of the element's own width, matching IntersectionObserver's
56
+ * intersectionRatio — a plain overlap test would keep a large wrapping element "in
57
+ * band" for its entire on-screen span, masking smaller fragments nested inside it.
58
+ * Direction-agnostic (unlike `hasScrolledPast`): overlap ratio is the same question
59
+ * regardless of vertical-lr/vertical-rl.
60
+ */
61
+ protected inCenterBand(el: Element): boolean;
62
+ /**
63
+ * Document-absolute (scrollLeft-independent) leading-edge position. Uses the same
64
+ * `Math.abs(scrollLeft)` normalization as `reportProgress` so this stays in the same
65
+ * coordinate space as the live scroll position regardless of vertical-lr/rl sign.
66
+ */
67
+ protected fragmentStart(el: Element): number;
68
+ protected currentScrollExtent(): {
69
+ pos: number;
70
+ size: number;
71
+ };
54
72
  private reportProgress;
55
73
  private handleScroll;
56
74
  private enableScrollProtection;
@@ -47,6 +47,11 @@ export declare class ColumnSnapper extends Snapper {
47
47
  * whose leading edge has entered or passed the viewport — that is the current section.
48
48
  */
49
49
  protected currentTimelineFragment(): string | undefined;
50
+ /**
51
+ * Tests each fragment's rect directly against the current column viewport, returning
52
+ * every one actually on screen rather than only the last one to have started.
53
+ */
54
+ protected sortedVisibleFragmentIds(): string[];
50
55
  reportProgress(forcedFragmentId?: string): void;
51
56
  private shakeTimeout;
52
57
  shake(): void;
@@ -26,6 +26,18 @@ export declare class ScrollSnapper extends Snapper {
26
26
  private resizeDebounce;
27
27
  private doc;
28
28
  protected hasScrolledPast(el: Element): boolean;
29
+ /**
30
+ * Overlap as a fraction of the element's own height, matching IntersectionObserver's
31
+ * intersectionRatio — a plain overlap test would keep a large wrapping element "in
32
+ * band" for its entire on-screen span, masking smaller fragments nested inside it.
33
+ */
34
+ protected inCenterBand(el: Element): boolean;
35
+ /** Document-absolute (scrollTop-independent) leading-edge position. */
36
+ protected fragmentStart(el: Element): number;
37
+ protected currentScrollExtent(): {
38
+ pos: number;
39
+ size: number;
40
+ };
29
41
  private reportProgress;
30
42
  private handleScroll;
31
43
  private enableScrollProtection;
@@ -6,46 +6,49 @@ export declare abstract class Snapper extends Module {
6
6
  static readonly moduleName: ModuleName;
7
7
  protected static readonly CENTER_TOLERANCE = 0.01;
8
8
  private protected;
9
- protected timelineObserver: IntersectionObserver | null;
10
9
  protected timelineEntries: Map<string, Element>;
11
- protected visibleFragmentIds: Set<string>;
12
10
  protected cachedFragmentIds: string[];
13
11
  protected sortedFragmentIds: string[];
12
+ protected cachedFragmentStarts: Map<string, number>;
14
13
  private static inDomOrder;
15
14
  /**
16
- * A zero-area target (e.g. an empty `<a id="…">` landmark) always has
17
- * intersectionRatio 0 by spec, so `isIntersecting` can never become true for it —
18
- * no threshold value changes that. For those entries, fall back to a geometric
19
- * containment check against `rootBounds` instead of trusting `isIntersecting`.
15
+ * Populates `timelineEntries`/`sortedFragmentIds` from a fresh id list, and refreshes
16
+ * `cachedFragmentStarts` for the new elements. Shared by all snappers' `timeline_entries`
17
+ * comms handler so DOM-order sorting and position caching only happen once per
18
+ * (re)population, not per report.
20
19
  */
21
- private static isVisibleEntry;
20
+ protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void;
22
21
  /**
23
- * Collapses the observer's root to a thin band around the same center line
24
- * `hasScrolledPast` tests against (go_id/go_text navigate by centering the target, not by
25
- * aligning it to an edge), so "intersecting" and "scrolled past" can't disagree about
26
- * where "current" is. A literal zero-height root would make every intersection area zero
27
- * — isIntersecting would never fire — so CENTER_TOLERANCE leaves it a real, if thin, band.
28
- * Percentage-based margins track viewport resizes automatically, no re-setup needed.
22
+ * Recomputes `cachedFragmentStarts` for every tracked fragment. Call whenever layout may
23
+ * have changed — on (re)population here, and from each snapper's own resize handler.
29
24
  */
30
- protected setupTimelineObserver(axis?: "vertical" | "horizontal"): void;
25
+ protected refreshFragmentStarts(): void;
31
26
  /**
32
- * Populates `timelineEntries`/`sortedFragmentIds` from a fresh id list, and
33
- * (re)starts IntersectionObserver-based visibility tracking for subclasses that use it.
34
- * Shared by all snappers' `timeline_entries` comms handler so DOM-order sorting and
35
- * observer bookkeeping only happen once per (re)population, not per report.
27
+ * Axis-appropriate, document-absolute leading-edge position of `el`. Subclasses with a
28
+ * scroll axis override this; `ColumnSnapper` doesn't use the cache (it has its own
29
+ * rect-based visibility test) so keeps this no-op default.
36
30
  */
37
- protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void;
31
+ protected fragmentStart(_el: Element): number;
32
+ /**
33
+ * The current viewport's position and size along the scroll axis, in the same
34
+ * document-absolute coordinate space as `fragmentStart`. Paired with it to test
35
+ * on-screen-ness without any per-frame geometry read.
36
+ */
37
+ protected currentScrollExtent(): {
38
+ pos: number;
39
+ size: number;
40
+ };
38
41
  /**
39
42
  * Returns the nearest fragment at or before `node` in DOM (reading) order, with no
40
43
  * dependency on rendered geometry — safe to call for programmatic navigation where the
41
- * target is already known, regardless of whether layout/IntersectionObserver has settled.
44
+ * target is already known, regardless of whether layout has settled.
42
45
  */
43
46
  protected nearestPrecedingTimelineEntry(node: Node): string | undefined;
44
47
  /**
45
- * Rect-based scan: last element in DOM order whose leading edge has already been
46
- * scrolled past. Subclasses implement `hasScrolledPast` for their layout/axis.
47
- * Exposed so callers can force an accurate one-off geometry check (e.g. right after
48
- * a `go_progression` jump) instead of trusting a possibly-stale visibility cache.
48
+ * Rect-based scan for an accurate one-off read right after a programmatic jump
49
+ * (go_id/go_text), before the next scroll event would otherwise refresh things: first
50
+ * checks for a fragment currently in the center band, and only if none is currently in
51
+ * the band falls back to the last fragment whose leading edge has been scrolled past.
49
52
  */
50
53
  protected fragmentFromGeometry(): string | undefined;
51
54
  /**
@@ -57,21 +60,30 @@ export declare abstract class Snapper extends Module {
57
60
  */
58
61
  protected firstFragmentIfReached(): string | undefined;
59
62
  /**
60
- * Returns the ID of the currently active timeline fragment:
61
- * 1. Primary — IntersectionObserver: returns the first visible element in DOM (reading) order.
62
- * This is direction-agnostic; works for LTR, RTL, and vertical writing modes. Reliable for
63
- * continuous, gradual scrolling.
64
- * 2. Fallback — rect scan (`fragmentFromGeometry`): used when IntersectionObserver hasn't
65
- * fired yet. Not reliable right after an instantaneous programmatic jump — callers that
66
- * know the target (goTo-style navigation) should bypass this method entirely.
63
+ * Returns the ID of the currently active timeline fragment: the last fragment in DOM
64
+ * order whose cached leading edge is at or before the viewport's center point — the same
65
+ * center line `hasScrolledPast`/`go_id`/`go_text` navigate to.
67
66
  */
68
67
  protected currentTimelineFragment(): string | undefined;
68
+ /**
69
+ * Every timeline fragment currently on screen, in DOM order — not just the single one
70
+ * `currentTimelineFragment` picks. Since fragments are ordered points, not ranges, "on
71
+ * screen" means the run of fragments whose leading edges fall between whichever one
72
+ * governs the viewport's top edge and whichever falls before its bottom edge.
73
+ */
74
+ protected sortedVisibleFragmentIds(): string[];
69
75
  /**
70
76
  * Returns true if the element's leading edge (in reading direction) has been scrolled past
71
77
  * — i.e. the element is before the current viewport position.
72
78
  * Subclasses implement this for their specific scroll axis and reading direction.
73
79
  */
74
80
  protected abstract hasScrolledPast(el: Element): boolean;
81
+ /**
82
+ * Whether the element currently overlaps the viewport's center band, not merely
83
+ * "already scrolled past" it. Defaults to `false` for subclasses with no center-line
84
+ * concept (e.g. paginated `ColumnSnapper`).
85
+ */
86
+ protected inCenterBand(_el: Element): boolean;
75
87
  buildStyles(): string;
76
88
  mount(wnd: ReadiumWindow, comms: Comms): boolean;
77
89
  unmount(wnd: ReadiumWindow, comms: Comms): boolean;
@@ -19,6 +19,18 @@ export declare class WebPubSnapper extends Snapper {
19
19
  private resizeDebounce;
20
20
  private doc;
21
21
  protected hasScrolledPast(el: Element): boolean;
22
+ /**
23
+ * Overlap as a fraction of the element's own height, matching IntersectionObserver's
24
+ * intersectionRatio — a plain overlap test would keep a large wrapping element "in
25
+ * band" for its entire on-screen span, masking smaller fragments nested inside it.
26
+ */
27
+ protected inCenterBand(el: Element): boolean;
28
+ /** Document-absolute (scrollTop-independent) leading-edge position. */
29
+ protected fragmentStart(el: Element): number;
30
+ protected currentScrollExtent(): {
31
+ pos: number;
32
+ size: number;
33
+ };
22
34
  private reportProgress;
23
35
  private handleScroll;
24
36
  private enableScrollProtection;