@readium/navigator-html-injectables 2.6.1 → 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.1",hasFeature:(n,s="")=>{switch(n){case"dom-manipulation":return!0;case"layout-changes":return!0;case"touch-events":return!0;case"mouse-events":return!0;case"keyboard-events":return!0;case"spine-scripting":return!0;case"embedded-web-content":return!0;default:return!1}}},writable:!1}),"getAnimations"in e.document&&e.document.getAnimations().forEach(n=>{n.cancel(),this.allAnimations.add(n)}),t.register("activate",i.moduleName,(n,s)=>{this.allAnimations.forEach(r=>{r.cancel(),r.play()}),s(!0)}),t.register("unfocus",i.moduleName,(n,s)=>{this.pauseAllMedia(e),this.allAnimations.forEach(r=>r.pause()),s(!0)});const a=e.document.querySelectorAll("audio,video");for(let n=0;n<a.length;n++){const s=a[n];s.addEventListener("play",this.onMediaPlayEvent,{passive:!0}),s.addEventListener("pause",this.onMediaPauseEvent,{passive:!0})}return t.log("Setup Mounted"),!0}unmount(e,t){return e.removeEventListener("error",this.wndOnErr),e.removeEventListener("play",this.onMediaPlayEvent),e.removeEventListener("pause",this.onMediaPauseEvent),this.allAnimations.forEach(a=>a.cancel()),this.allAnimations.clear(),t.log("Setup Unmounted"),!0}};i.moduleName="setup";let o=i;export{o as Setup};
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 u,Locator as m,LocatorLocations as g}from"@readium/shared";import{deselect as a,findFirstVisibleLocator as f}from"../../helpers/dom.js";import{Snapper as p}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 h="readium-cjk-vertical-snapper-style",l=class l extends p{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),o=r-this.lastScrollLeft;if(this.lastScrollLeft=r,this.isScrollProtectionEnabled&&Math.abs(o)>5){const e=Date.now(),t=e-(this.lastScrollTime||e);if(this.patternAnalyzer&&this.patternAnalyzer.analyze(o>0?"down":"up",Math.abs(o),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:o,scrollDirection:o>0?"left":"right",targetElement:n})}this.lastScrollTime=e}this.comms.send("scroll",o),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();return this.verticalLR?r.right<=0:r.left>=this.wnd.innerWidth}reportProgress(i){if(!this.comms.ready)return;const r=this.doc().scrollWidth,o=this.wnd.innerWidth,e=Math.max(1,r-o),t=Math.abs(this.doc().scrollLeft),s=Math.max(0,Math.min(1,t/e)),n=Math.max(0,Math.min(1,(t+o)/r));this.comms.send("progress",{start:s,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(),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 o=i.document.createElement("style");return o.dataset.readium="true",o.id=h,o.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 u,Locator as m,LocatorLocations as g}from"@readium/shared"
9
9
  overflow-x: auto !important;
10
10
  overflow-y: hidden !important;
11
11
  }
12
- `,i.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.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 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(()=>{const n=this.scrollable()*s;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 s=i.document.getElementById(e);if(!s){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollLeft+=s.getBoundingClientRect().left-i.innerWidth/2;const n=e;this.reportProgress(this.timelineEntries.has(n)?n:this.nearestPrecedingTimelineEntry(s)),a(this.wnd),t(!0)})}),r.register("go_text",l.moduleName,(e,t)=>{let s;Array.isArray(e)&&(e.length>1&&(s=e[1]),e=e[0]);const n=u.deserialize(e),c=b(this.wnd.document,new m({href:i.location.href,type:"text/html",text:n,locations:s?new g({otherLocations:new Map([["cssSelector",s]])}):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 s=f(i,!0);this.comms.send("first_visible_locator",s.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(h)?.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 d=l;export{d 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 u,Locator as m,LocatorLocations as g}from"@readium/shared";import{deselect as a,findFirstVisibleLocator as p}from"../../helpers/dom.js";import{Snapper as f}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 h="readium-scroll-snapper-style",o=class o extends f{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){return r.getBoundingClientRect().bottom<=0}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=h,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 u,Locator as m,LocatorLocations as g}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()),a(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)),a(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=u.deserialize(e),c=S(this.wnd.document,new m({href:r.location.href,type:"text/html",text:n,locations:i?new g({otherLocations:new Map([["cssSelector",i]])}):void 0}));if(!c){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=c.getBoundingClientRect().top+r.scrollY-r.innerHeight/2,this.reportProgress(this.nearestPrecedingTimelineEntry(c.startContainer)),a(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)=>{a(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=p(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(h)?.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 d=o;export{d 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",n=class n 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(e,t,i){const r=e.get(t),s=e.get(i);return!r||!s?0:r.compareDocumentPosition(s)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1}static isVisibleEntry(e){const t=e.boundingClientRect;if(t.width>0&&t.height>0)return e.isIntersecting;const i=e.rootBounds;return i?t.left<=i.right&&t.right>=i.left&&t.top<=i.bottom&&t.bottom>=i.top:e.isIntersecting}setupTimelineObserver(){this.timelineObserver&&this.timelineObserver.disconnect(),this.timelineObserver=new IntersectionObserver(e=>{for(const t of e)n.isVisibleEntry(t)?this.visibleFragmentIds.add(t.target.id):this.visibleFragmentIds.delete(t.target.id)},{threshold:[.01]})}updateTimelineEntries(e,t){this.cachedFragmentIds=e,this.timelineObserver?.disconnect(),this.visibleFragmentIds.clear(),this.timelineEntries.clear();for(const i of e){const r=t.document.getElementById(i);r&&(this.timelineEntries.set(i,r),this.timelineObserver?.observe(r))}this.sortedFragmentIds=Array.from(this.timelineEntries.keys()).sort((i,r)=>n.inDomOrder(this.timelineEntries,i,r))}nearestPrecedingTimelineEntry(e){let t;for(const i of this.sortedFragmentIds){const r=this.timelineEntries.get(i),s=r.compareDocumentPosition(e);if(r===e||r.contains(e)||(s&Node.DOCUMENT_POSITION_FOLLOWING)!==0)t=i;else break}return t}fragmentFromGeometry(){let e;for(const t of this.sortedFragmentIds){const i=this.timelineEntries.get(t);if(this.hasScrolledPast(i))e=t;else break}return e}firstFragmentIfReached(){const e=this.sortedFragmentIds[0];if(e===void 0)return;const t=this.timelineEntries.get(e);return this.hasScrolledPast(t)?e:void 0}currentTimelineFragment(){return this.visibleFragmentIds.size>0?Array.from(this.visibleFragmentIds).sort((e,t)=>n.inDomOrder(this.timelineEntries,e,t))[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(e,t){const i=e.document.createElement("style");return i.dataset.readium="true",i.id=o,i.textContent=this.buildStyles(),e.document.head.appendChild(i),t.register("protect",n.moduleName,(r,s)=>{this.protected=!0,i.textContent=this.buildStyles(),s(!0)}),t.register("unprotect",n.moduleName,(r,s)=>{this.protected=!1,i.textContent=this.buildStyles(),s(!0)}),t.log("Snapper Mounted"),!0}unmount(e,t){return e.document.getElementById(o)?.remove(),t.log("Snapper Unmounted"),!0}};n.moduleName="snapper";let l=n;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 h,Locator as u,LocatorLocations as d}from"@readium/shared";import{deselect as c,findFirstVisibleLocator as g}from"../../helpers/dom.js";import{Snapper as m}from"./Snapper.js";import{rangeFromLocator as p}from"../../helpers/locator.js";import{forceWebkitRecalc as f}from"../../helpers/document.js";import{PatternAnalyzer as b}from"../../protection/PatternAnalyzer.js";import{SCROLL_PROTECTION_CONFIG as S}from"../../protection/config.js";const o=class o extends m{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 n=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:n})}this.lastScrollTime=t}this.comms.send("scroll",e),this.isScrolling=!1}))}}}doc(){return this.wnd.document.scrollingElement}hasScrolledPast(s){return s.getBoundingClientRect().bottom<=0}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)),l=Math.max(0,Math.min(1,(r+t)/e));this.comms.send("progress",{start:i,end:l,fragmentId:s!==void 0?s:this.currentTimelineFragment()})}enableScrollProtection(){this.patternAnalyzer||(this.patternAnalyzer=new b(S),this.isScrollProtectionEnabled=!0,this.comms?.log("Scroll protection enabled"))}mount(s,r){return this.wnd=s,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(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 l=e;this.reportProgress(this.timelineEntries.has(l)?l: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 l=h.deserialize(e),n=p(this.wnd.document,new u({href:s.location.href,type:"text/html",text:l,locations:i?new d({otherLocations:new Map([["cssSelector",i]])}):void 0}));if(!n){t(!1);return}this.wnd.requestAnimationFrame(()=>{this.doc().scrollTop=n.getBoundingClientRect().top+s.scrollY-s.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 i=g(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.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.1",
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.0"
48
+ "@readium/shared": "2.4.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "css-selector-generator": "^3.8.0",
@@ -69,10 +69,49 @@ export class CJKVerticalSnapper extends Snapper {
69
69
  }
70
70
 
71
71
  protected hasScrolledPast(el: Element): boolean {
72
+ // go_id/go_text center the target in the viewport, not scroll it to the edge —
73
+ // so "reached" has to mean "crossed the viewport's horizontal center", the same
74
+ // line navigation scrolls to, not the leading edge.
72
75
  const rect = el.getBoundingClientRect();
73
- // vertical-rl: content flows right→left; leading edge is right, scrolled past when off-screen right.
74
- // vertical-lr: content flows left→right; leading edge is left, scrolled past when off-screen left.
75
- return this.verticalLR ? rect.right <= 0 : rect.left >= this.wnd.innerWidth;
76
+ const center = this.wnd.innerWidth / 2;
77
+ const tolerance = this.wnd.innerWidth * Snapper.CENTER_TOLERANCE;
78
+ // vertical-rl: content flows right→left; leading edge is right, scrolled past when past center.
79
+ // vertical-lr: content flows left→right; leading edge is left, scrolled past when past center.
80
+ return this.verticalLR ? rect.right <= center + tolerance : rect.left >= center - tolerance;
81
+ }
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 };
76
115
  }
77
116
 
78
117
  private reportProgress(forcedFragmentId?: string) {
@@ -90,7 +129,8 @@ export class CJKVerticalSnapper extends Snapper {
90
129
  this.comms.send("progress", {
91
130
  start: progress,
92
131
  end: viewportEnd,
93
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
132
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
133
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
94
134
  });
95
135
  }
96
136
 
@@ -158,7 +198,6 @@ export class CJKVerticalSnapper extends Snapper {
158
198
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
159
199
  this.wnd = wnd;
160
200
  this.comms = comms;
161
- this.setupTimelineObserver();
162
201
 
163
202
  this.initialScrollHandled = false;
164
203
  this.lastScrollLeft = 0;
@@ -197,6 +236,7 @@ export class CJKVerticalSnapper extends Snapper {
197
236
  this.resizeDebounce = this.wnd.setTimeout(() => {
198
237
  this.isResizing = false;
199
238
  this.resizeDebounce = null;
239
+ this.refreshFragmentStarts();
200
240
  this.reportProgress();
201
241
  }, 50);
202
242
  });
@@ -349,10 +389,10 @@ export class CJKVerticalSnapper extends Snapper {
349
389
  this.isScrollProtectionEnabled = false;
350
390
  }
351
391
 
352
- this.timelineObserver?.disconnect();
353
- this.timelineObserver = null;
354
- this.visibleFragmentIds.clear();
355
392
  this.timelineEntries.clear();
393
+ this.cachedFragmentIds = [];
394
+ this.sortedFragmentIds = [];
395
+ this.cachedFragmentStarts.clear();
356
396
 
357
397
  comms.log("CJKVerticalSnapper Unmounted");
358
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
 
@@ -38,7 +38,39 @@ export class ScrollSnapper extends Snapper {
38
38
  }
39
39
 
40
40
  protected hasScrolledPast(el: Element): boolean {
41
- return el.getBoundingClientRect().bottom <= 0;
41
+ // go_id/go_text center the target in the viewport, not scroll it to the top —
42
+ // so "reached" has to mean "crossed the viewport's vertical center", the same
43
+ // line navigation scrolls to, not the top/bottom edge.
44
+ const center = this.wnd.innerHeight / 2;
45
+ return el.getBoundingClientRect().top <= center + this.wnd.innerHeight * Snapper.CENTER_TOLERANCE;
46
+ }
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 };
42
74
  }
43
75
 
44
76
  private reportProgress(forcedFragmentId?: string) {
@@ -55,7 +87,8 @@ export class ScrollSnapper extends Snapper {
55
87
  this.comms.send("progress", {
56
88
  start: progress,
57
89
  end: viewportEnd,
58
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
90
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
91
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
59
92
  });
60
93
  }
61
94
 
@@ -129,7 +162,6 @@ export class ScrollSnapper extends Snapper {
129
162
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
130
163
  this.wnd = wnd;
131
164
  this.comms = comms;
132
- this.setupTimelineObserver();
133
165
 
134
166
  this.initialScrollHandled = false;
135
167
  this.lastScrollTop = 0;
@@ -168,6 +200,7 @@ export class ScrollSnapper extends Snapper {
168
200
  this.resizeDebounce = this.wnd.setTimeout(() => {
169
201
  this.isResizing = false;
170
202
  this.resizeDebounce = null;
203
+ this.refreshFragmentStarts();
171
204
  this.reportProgress();
172
205
  }, 50);
173
206
  });
@@ -317,10 +350,10 @@ export class ScrollSnapper extends Snapper {
317
350
  this.resizeObserver.disconnect();
318
351
  if (this.handleScroll) wnd.removeEventListener("scroll", this.handleScroll);
319
352
  wnd.document.getElementById(SCROLL_SNAPPER_STYLE_ID)?.remove();
320
- this.timelineObserver?.disconnect();
321
- this.timelineObserver = null;
322
- this.visibleFragmentIds.clear();
323
353
  this.timelineEntries.clear();
354
+ this.cachedFragmentIds = [];
355
+ this.sortedFragmentIds = [];
356
+ this.cachedFragmentStarts.clear();
324
357
 
325
358
  if (this.patternAnalyzer) {
326
359
  this.patternAnalyzer.clear();
@@ -8,16 +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 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
+ protected static readonly CENTER_TOLERANCE = 0.01;
15
+
11
16
  private protected = false;
12
17
 
13
18
  // Timeline fragment tracking
14
- protected timelineObserver: IntersectionObserver | null = null;
15
19
  protected timelineEntries: Map<string, Element> = new Map();
16
- protected visibleFragmentIds: Set<string> = new Set();
17
20
  protected cachedFragmentIds: string[] = [];
18
21
  // DOM-order-sorted fragment ids, recomputed only when entries are (re)populated —
19
22
  // sorting on every progress report (i.e. every scroll frame) is wasted work.
20
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();
21
29
 
22
30
  private static inDomOrder(entries: Map<string, Element>, a: string, b: string): number {
23
31
  const ea = entries.get(a);
@@ -28,64 +36,56 @@ export abstract class Snapper extends Module {
28
36
  }
29
37
 
30
38
  /**
31
- * A zero-area target (e.g. an empty `<a id="…">` landmark) always has
32
- * intersectionRatio 0 by spec, so `isIntersecting` can never become true for it —
33
- * no threshold value changes that. For those entries, fall back to a geometric
34
- * containment check against `rootBounds` instead of trusting `isIntersecting`.
35
- */
36
- private static isVisibleEntry(entry: IntersectionObserverEntry): boolean {
37
- const rect = entry.boundingClientRect;
38
- // Element has real width and height: isIntersecting is spec-correct, trust it as-is.
39
- if (rect.width > 0 && rect.height > 0) return entry.isIntersecting;
40
- // Zero-width or zero-height element: isIntersecting is stuck at false (ratio is
41
- // area / 0, defined as 0), so it can't be trusted. Do our own overlap check instead.
42
- const root = entry.rootBounds;
43
- if (!root) return entry.isIntersecting;
44
- return rect.left <= root.right && rect.right >= root.left &&
45
- rect.top <= root.bottom && rect.bottom >= root.top;
46
- }
47
-
48
- protected setupTimelineObserver(): void {
49
- if (this.timelineObserver) this.timelineObserver.disconnect();
50
- this.timelineObserver = new IntersectionObserver(
51
- (entries) => {
52
- for (const entry of entries) {
53
- if (Snapper.isVisibleEntry(entry))
54
- this.visibleFragmentIds.add((entry.target as HTMLElement).id);
55
- else
56
- this.visibleFragmentIds.delete((entry.target as HTMLElement).id);
57
- }
58
- },
59
- { threshold: [0.01] }
60
- );
61
- }
62
-
63
- /**
64
- * Populates `timelineEntries`/`sortedFragmentIds` from a fresh id list, and
65
- * (re)starts IntersectionObserver-based visibility tracking for subclasses that use it.
66
- * Shared by all snappers' `timeline_entries` comms handler so DOM-order sorting and
67
- * 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.
68
43
  */
69
44
  protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void {
70
45
  this.cachedFragmentIds = ids;
71
- this.timelineObserver?.disconnect();
72
- this.visibleFragmentIds.clear();
73
46
  this.timelineEntries.clear();
47
+ this.cachedFragmentStarts.clear();
74
48
  for (const id of ids) {
75
49
  const el = wnd.document.getElementById(id);
76
- if (el) {
77
- this.timelineEntries.set(id, el);
78
- this.timelineObserver?.observe(el);
79
- }
50
+ if (el) this.timelineEntries.set(id, el);
80
51
  }
81
52
  this.sortedFragmentIds = Array.from(this.timelineEntries.keys())
82
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 };
83
83
  }
84
84
 
85
85
  /**
86
86
  * Returns the nearest fragment at or before `node` in DOM (reading) order, with no
87
87
  * dependency on rendered geometry — safe to call for programmatic navigation where the
88
- * target is already known, regardless of whether layout/IntersectionObserver has settled.
88
+ * target is already known, regardless of whether layout has settled.
89
89
  */
90
90
  protected nearestPrecedingTimelineEntry(node: Node): string | undefined {
91
91
  let nearestId: string | undefined;
@@ -101,12 +101,17 @@ export abstract class Snapper extends Module {
101
101
  }
102
102
 
103
103
  /**
104
- * Rect-based scan: last element in DOM order whose leading edge has already been
105
- * scrolled past. Subclasses implement `hasScrolledPast` for their layout/axis.
106
- * Exposed so callers can force an accurate one-off geometry check (e.g. right after
107
- * 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.
108
108
  */
109
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
+
110
115
  let nearestId: string | undefined;
111
116
  for (const id of this.sortedFragmentIds) {
112
117
  const el = this.timelineEntries.get(id)!;
@@ -131,20 +136,43 @@ export abstract class Snapper extends Module {
131
136
  }
132
137
 
133
138
  /**
134
- * Returns the ID of the currently active timeline fragment:
135
- * 1. Primary — IntersectionObserver: returns the first visible element in DOM (reading) order.
136
- * This is direction-agnostic; works for LTR, RTL, and vertical writing modes. Reliable for
137
- * continuous, gradual scrolling.
138
- * 2. Fallback — rect scan (`fragmentFromGeometry`): used when IntersectionObserver hasn't
139
- * fired yet. Not reliable right after an instantaneous programmatic jump — callers that
140
- * 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.
141
142
  */
142
143
  protected currentTimelineFragment(): string | undefined {
143
- if (this.visibleFragmentIds.size > 0) {
144
- return Array.from(this.visibleFragmentIds)
145
- .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;
146
152
  }
147
- return this.fragmentFromGeometry();
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;
172
+ }
173
+ if (from < 0) from = 0;
174
+ if (to < from) return [];
175
+ return this.sortedFragmentIds.slice(from, to + 1);
148
176
  }
149
177
 
150
178
  /**
@@ -154,6 +182,15 @@ export abstract class Snapper extends Module {
154
182
  */
155
183
  protected abstract hasScrolledPast(el: Element): boolean;
156
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
+
157
194
  buildStyles() {
158
195
  return `
159
196
  html, body {
@@ -187,6 +224,11 @@ export abstract class Snapper extends Module {
187
224
  unmount(wnd: ReadiumWindow, comms: Comms): boolean {
188
225
  wnd.document.getElementById(SNAPPER_STYLE_ID)?.remove();
189
226
 
227
+ this.timelineEntries.clear();
228
+ this.cachedFragmentIds = [];
229
+ this.sortedFragmentIds = [];
230
+ this.cachedFragmentStarts.clear();
231
+
190
232
  comms.log("Snapper Unmounted");
191
233
  return true;
192
234
  }
@@ -32,7 +32,39 @@ export class WebPubSnapper extends Snapper {
32
32
  }
33
33
 
34
34
  protected hasScrolledPast(el: Element): boolean {
35
- return el.getBoundingClientRect().bottom <= 0;
35
+ // go_id/go_text center the target in the viewport, not scroll it to the top —
36
+ // so "reached" has to mean "crossed the viewport's vertical center", the same
37
+ // line navigation scrolls to, not the top/bottom edge.
38
+ const center = this.wnd.innerHeight / 2;
39
+ return el.getBoundingClientRect().top <= center + this.wnd.innerHeight * Snapper.CENTER_TOLERANCE;
40
+ }
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 };
36
68
  }
37
69
 
38
70
  private reportProgress(forcedFragmentId?: string) {
@@ -47,7 +79,8 @@ export class WebPubSnapper extends Snapper {
47
79
  this.comms.send("progress", {
48
80
  start: progress,
49
81
  end: viewportEnd,
50
- fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment()
82
+ fragmentId: forcedFragmentId !== undefined ? forcedFragmentId : this.currentTimelineFragment(),
83
+ visibleFragmentIds: this.sortedVisibleFragmentIds()
51
84
  });
52
85
  }
53
86
 
@@ -120,7 +153,6 @@ export class WebPubSnapper extends Snapper {
120
153
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
121
154
  this.wnd = wnd;
122
155
  this.comms = comms;
123
- this.setupTimelineObserver();
124
156
 
125
157
  this.initialScrollHandled = false;
126
158
  this.lastScrollTop = 0;
@@ -140,6 +172,7 @@ export class WebPubSnapper extends Snapper {
140
172
  this.resizeDebounce = this.wnd.setTimeout(() => {
141
173
  this.isResizing = false;
142
174
  this.resizeDebounce = null;
175
+ this.refreshFragmentStarts();
143
176
  this.reportProgress();
144
177
  }, 50);
145
178
  });
@@ -293,10 +326,10 @@ export class WebPubSnapper extends Snapper {
293
326
  this.isScrollProtectionEnabled = false;
294
327
  }
295
328
 
296
- this.timelineObserver?.disconnect();
297
- this.timelineObserver = null;
298
- this.visibleFragmentIds.clear();
299
329
  this.timelineEntries.clear();
330
+ this.cachedFragmentIds = [];
331
+ this.sortedFragmentIds = [];
332
+ this.cachedFragmentStarts.clear();
300
333
 
301
334
  comms.log("WebPubSnapper Unmounted");
302
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;
@@ -4,39 +4,51 @@ import { Module } from "../Module.ts";
4
4
  import { ModuleName } from "../ModuleLibrary.ts";
5
5
  export declare abstract class Snapper extends Module {
6
6
  static readonly moduleName: ModuleName;
7
+ protected static readonly CENTER_TOLERANCE = 0.01;
7
8
  private protected;
8
- protected timelineObserver: IntersectionObserver | null;
9
9
  protected timelineEntries: Map<string, Element>;
10
- protected visibleFragmentIds: Set<string>;
11
10
  protected cachedFragmentIds: string[];
12
11
  protected sortedFragmentIds: string[];
12
+ protected cachedFragmentStarts: Map<string, number>;
13
13
  private static inDomOrder;
14
14
  /**
15
- * A zero-area target (e.g. an empty `<a id="…">` landmark) always has
16
- * intersectionRatio 0 by spec, so `isIntersecting` can never become true for it —
17
- * no threshold value changes that. For those entries, fall back to a geometric
18
- * 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.
19
19
  */
20
- private static isVisibleEntry;
21
- protected setupTimelineObserver(): void;
20
+ protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void;
22
21
  /**
23
- * Populates `timelineEntries`/`sortedFragmentIds` from a fresh id list, and
24
- * (re)starts IntersectionObserver-based visibility tracking for subclasses that use it.
25
- * Shared by all snappers' `timeline_entries` comms handler so DOM-order sorting and
26
- * observer bookkeeping only happen once per (re)population, not per report.
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.
27
24
  */
28
- protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void;
25
+ protected refreshFragmentStarts(): void;
26
+ /**
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.
30
+ */
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
+ };
29
41
  /**
30
42
  * Returns the nearest fragment at or before `node` in DOM (reading) order, with no
31
43
  * dependency on rendered geometry — safe to call for programmatic navigation where the
32
- * target is already known, regardless of whether layout/IntersectionObserver has settled.
44
+ * target is already known, regardless of whether layout has settled.
33
45
  */
34
46
  protected nearestPrecedingTimelineEntry(node: Node): string | undefined;
35
47
  /**
36
- * Rect-based scan: last element in DOM order whose leading edge has already been
37
- * scrolled past. Subclasses implement `hasScrolledPast` for their layout/axis.
38
- * Exposed so callers can force an accurate one-off geometry check (e.g. right after
39
- * 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.
40
52
  */
41
53
  protected fragmentFromGeometry(): string | undefined;
42
54
  /**
@@ -48,21 +60,30 @@ export declare abstract class Snapper extends Module {
48
60
  */
49
61
  protected firstFragmentIfReached(): string | undefined;
50
62
  /**
51
- * Returns the ID of the currently active timeline fragment:
52
- * 1. Primary — IntersectionObserver: returns the first visible element in DOM (reading) order.
53
- * This is direction-agnostic; works for LTR, RTL, and vertical writing modes. Reliable for
54
- * continuous, gradual scrolling.
55
- * 2. Fallback — rect scan (`fragmentFromGeometry`): used when IntersectionObserver hasn't
56
- * fired yet. Not reliable right after an instantaneous programmatic jump — callers that
57
- * 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.
58
66
  */
59
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[];
60
75
  /**
61
76
  * Returns true if the element's leading edge (in reading direction) has been scrolled past
62
77
  * — i.e. the element is before the current viewport position.
63
78
  * Subclasses implement this for their specific scroll axis and reading direction.
64
79
  */
65
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;
66
87
  buildStyles(): string;
67
88
  mount(wnd: ReadiumWindow, comms: Comms): boolean;
68
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;