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