@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.
- 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 +48 -8
- package/src/modules/snapper/ColumnSnapper.ts +14 -1
- package/src/modules/snapper/ScrollSnapper.ts +39 -6
- package/src/modules/snapper/Snapper.ts +103 -61
- package/src/modules/snapper/WebPubSnapper.ts +39 -6
- 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 +46 -25
- 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
|
|
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
|
-
`,
|
|
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 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
|
-
`,
|
|
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(e
|
|
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",
|
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
21
|
-
protected setupTimelineObserver(): void;
|
|
20
|
+
protected updateTimelineEntries(ids: string[], wnd: ReadiumWindow): void;
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
24
|
-
* (re)
|
|
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
|
|
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
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
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;
|