@readium/navigator 2.11.0 → 2.12.0
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/divina/DivinaNavigator.js +1 -1
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FrameComms.js +1 -1
- package/dist/epub/frame/FrameManager.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLFrameManager.js +1 -1
- package/dist/helpers/serialize.js +1 -0
- package/dist/index.js +1 -1
- package/dist/preferences/guards.js +1 -1
- package/dist/readaloud/GuidedNavigationProvider.js +1 -0
- package/dist/readaloud/ReadAloudNavigator.js +1 -0
- package/dist/readaloud/ReadingUnit.js +1 -0
- package/dist/readaloud/SpeechProgress.js +1 -0
- package/dist/readaloud/preferences/ReadAloudDefaults.js +1 -0
- package/dist/readaloud/preferences/ReadAloudPreferences.js +1 -0
- package/dist/readaloud/preferences/ReadAloudPreferencesEditor.js +1 -0
- package/dist/readaloud/preferences/ReadAloudSettings.js +1 -0
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +8 -6
- package/src/divina/DivinaNavigator.ts +1 -0
- package/src/epub/EpubNavigator.ts +127 -21
- package/src/epub/frame/FrameComms.ts +42 -7
- package/src/epub/frame/FrameManager.ts +20 -7
- package/src/epub/frame/FramePoolManager.ts +5 -0
- package/src/epub/fxl/FXLFrameManager.ts +30 -8
- package/src/helpers/serialize.ts +9 -0
- package/src/index.ts +1 -0
- package/src/preferences/guards.ts +11 -0
- package/src/readaloud/GuidedNavigationProvider.ts +87 -0
- package/src/readaloud/ReadAloudNavigator.ts +949 -0
- package/src/readaloud/ReadingUnit.ts +154 -0
- package/src/readaloud/SpeechProgress.ts +60 -0
- package/src/readaloud/index.ts +17 -0
- package/src/readaloud/preferences/ReadAloudDefaults.ts +24 -0
- package/src/readaloud/preferences/ReadAloudPreferences.ts +104 -0
- package/src/readaloud/preferences/ReadAloudPreferencesEditor.ts +115 -0
- package/src/readaloud/preferences/ReadAloudSettings.ts +44 -0
- package/src/readaloud/preferences/index.ts +4 -0
- package/src/webpub/WebPubFrameManager.ts +24 -7
- package/src/webpub/WebPubFramePoolManager.ts +5 -0
- package/src/webpub/WebPubNavigator.ts +114 -10
- package/types/src/epub/EpubNavigator.d.ts +26 -1
- package/types/src/epub/frame/FrameComms.d.ts +8 -1
- package/types/src/epub/frame/FrameManager.d.ts +3 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +2 -0
- package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -0
- package/types/src/helpers/serialize.d.ts +1 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/preferences/guards.d.ts +1 -0
- package/types/src/readaloud/GuidedNavigationProvider.d.ts +28 -0
- package/types/src/readaloud/ReadAloudNavigator.d.ts +158 -0
- package/types/src/readaloud/ReadingUnit.d.ts +58 -0
- package/types/src/readaloud/SpeechProgress.d.ts +19 -0
- package/types/src/readaloud/index.d.ts +6 -0
- package/types/src/readaloud/preferences/ReadAloudDefaults.d.ts +11 -0
- package/types/src/readaloud/preferences/ReadAloudPreferences.d.ts +58 -0
- package/types/src/readaloud/preferences/ReadAloudPreferencesEditor.d.ts +37 -0
- package/types/src/readaloud/preferences/ReadAloudSettings.d.ts +26 -0
- package/types/src/readaloud/preferences/index.d.ts +4 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +4 -0
- package/types/src/webpub/WebPubFramePoolManager.d.ts +2 -0
- package/types/src/webpub/WebPubNavigator.d.ts +25 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Feature as w,Link as u,Locator as f,LocatorText as P,getCssSelector as y,getHtmlId as S,LocatorLocations as k}from"@readium/shared";import{VisualNavigator as x}from"../Navigator.js";import{WebPubFramePoolManager as D}from"./WebPubFramePoolManager.js";import{WebPubModules as C}from"@readium/navigator-html-injectables";import{p as g}from"../_virtual/index.js";import{getScriptMode as I}from"../helpers/scriptMode.js";import{WebPubCSS as O}from"./css/WebPubCSS.js";import{WebUserProperties as E,WebRSProperties as F}from"./css/Properties.js";import{WebPubPreferences as A}from"./preferences/WebPubPreferences.js";import{WebPubDefaults as T}from"./preferences/WebPubDefaults.js";import{WebPubSettings as _}from"./preferences/WebPubSettings.js";import{WebPubPreferencesEditor as v}from"./preferences/WebPubPreferencesEditor.js";import{Injector as W}from"../injection/Injector.js";import{createReadiumWebPubRules as M}from"../injection/webpubInjectables.js";import{NavigatorProtector as R,NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT as b}from"../protection/NavigatorProtector.js";import{KeyboardPeripherals as H,NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT as L}from"../peripherals/KeyboardPeripherals.js";import{supportsDecorationStyle as N,decorationsEqual as z,resolveDecorationForWire as m}from"@readium/decorator";const V=h=>({frameLoaded:h.frameLoaded||(()=>{}),positionChanged:h.positionChanged||(()=>{}),timelineItemChanged:h.timelineItemChanged||(()=>{}),tap:h.tap||(()=>!1),click:h.click||(()=>!1),zoom:h.zoom||(()=>{}),scroll:h.scroll||(()=>{}),customEvent:h.customEvent||(()=>{}),handleLocator:h.handleLocator||(()=>!1),textSelected:h.textSelected||(()=>{}),contentProtection:h.contentProtection||(()=>{}),contextMenu:h.contextMenu||(()=>{}),peripheral:h.peripheral||(()=>{})});function j(h,e){return h.length===e.length&&h.every((t,i)=>t===e[i])}class B extends x{constructor(e,t,i,r=void 0,o={preferences:{},defaults:{}}){super(),this.currentIndex=0,this._destroyed=!1,this._visibleFragmentIds=[],this._notifiedVisibleFragmentIds=[],this._preferencesEditor=null,this._injector=null,this._isNavigating=!1,this._navigatorProtector=null,this._keyboardPeripheralsManager=null,this._suspiciousActivityListener=null,this._keyboardPeripheralListener=null,this._decorations=new Map,this._decorationObservers=new Map,this._decorationHoveredDecorations=new Map,this._decorationActivationState=new Map,this._decorationHoverState=new Map,this._decorationActivationConsumed=!1,this.webViewport={readingOrder:[],progressions:new Map,positions:null},this.pub=t,this.container=e,this.listeners=V(i),this._preferences=new A(o.preferences),this._defaults=new T(o.defaults),this._settings=new _(this._preferences,this._defaults,this.hasDisplayTransformability),this._css=new O({rsProperties:new F({experiments:this._settings.experiments||null}),userProperties:new E({zoom:this._settings.zoom})});const s=M(t.readingOrder.items),l=o.injectables||{rules:[],allowedDomains:[]};if(this._injector=new W({rules:[...s,...l.rules],allowedDomains:l.allowedDomains}),this._contentProtection=o.contentProtection||{},this._decoratorConfig=o.decoratorConfig||{},this._decorationResizeSelectors=new Set(this._decoratorConfig.resizeWatchSelectors??[]),this._keyboardPeripherals=this.mergeKeyboardPeripherals(this._contentProtection,o.keyboardPeripherals||[]),(this._contentProtection.disableContextMenu||this._contentProtection.checkAutomation||this._contentProtection.checkIFrameEmbedding||this._contentProtection.monitorDevTools||this._contentProtection.protectPrinting?.disable)&&(this._navigatorProtector=new R(this._contentProtection),this._suspiciousActivityListener=a=>{const{type:n,...c}=a.detail;n==="context_menu"?this.listeners.contextMenu(c):this.listeners.contentProtection(n,c)},window.addEventListener(b,this._suspiciousActivityListener)),this._keyboardPeripherals.length>0&&(this._keyboardPeripheralsManager=new H({keyboardPeripherals:this._keyboardPeripherals}),this._keyboardPeripheralListener=a=>{const n=a.detail;this.listeners.peripheral(n)},window.addEventListener(L,this._keyboardPeripheralListener)),r&&typeof r.copyWithLocations=="function"){this.currentLocation=r;const a=this.pub.readingOrder.findIndexWithHref(r.href);a>=0&&(this.currentIndex=a)}else this.currentLocation=this.createCurrentLocator()}async load(){if(await this.updateCSS(!1),this._destroyed)return;const e=this.compileCSSProperties(this._css);this.framePool=new D(this.container,e,this._injector,this._contentProtection,this._keyboardPeripherals,t=>this.pub.timeline.segmentsForHref(t).flatMap(i=>i.references).map(i=>{const r=i.indexOf("#");return r>=0?i.slice(r+1):""}).filter(Boolean),[...this._decorationResizeSelectors]),!this._destroyed&&(this.currentLocation=this.completeLocator(this.currentLocation),await this.apply())}get settings(){return Object.freeze({...this._settings})}get preferencesEditor(){return this._preferencesEditor===null&&(this._preferencesEditor=new v(this._preferences,this.settings,this.pub.metadata)),this._preferencesEditor}async submitPreferences(e){this._preferences=this._preferences.merging(e),await this.applyPreferences()}async applyPreferences(){this._settings=new _(this._preferences,this._defaults,this.hasDisplayTransformability),this._preferencesEditor!==null&&(this._preferencesEditor=new v(this._preferences,this.settings,this.pub.metadata)),await this.updateCSS(!0)}async updateCSS(e){this._css.update(this._settings),e&&await this.commitCSS(this._css)}compileCSSProperties(e){const t={};for(const[i,r]of Object.entries(e.rsProperties.toCSSProperties()))t[i]=r;for(const[i,r]of Object.entries(e.userProperties.toCSSProperties()))t[i]=r;return t}async commitCSS(e){const t=this.compileCSSProperties(e);this.framePool.setCSSProperties(t)}get _cframes(){return this.framePool.currentFrames}get hasDisplayTransformability(){return this.pub.metadata?.accessibility?.feature?.some(e=>e.value===w.DISPLAY_TRANSFORMABILITY.value)??!1}eventListener(e,t){switch(e){case"_pong":this.listeners.frameLoaded(this.framePool.currentFrames[0].iframe.contentWindow),this.listeners.positionChanged(this.currentLocation),this._notifyTimelineChange(this.currentLocation),this._reapplyDecorationsToCurrentFrame();break;case"first_visible_locator":const i=f.deserialize(t);if(!i)break;this.currentLocation=new f({href:this.currentLocation.href,type:this.currentLocation.type,title:this.currentLocation.title,locations:i?.locations,text:i?.text}),this.listeners.positionChanged(this.currentLocation),this._notifyTimelineChange(this.currentLocation);break;case"text_selected":{const a=t;a.locator=new f({href:this.currentLocation.href,type:this.currentLocation.type,text:new P({highlight:a.text})}),this.listeners.textSelected(a);break}case"decoration_activated":{this._handleDecorationActivated(t)&&(this._decorationActivationConsumed=!0);break}case"decoration_pointer_enter":this._handleDecorationPointerEnter(t);break;case"decoration_pointer_leave":this._handleDecorationPointerLeave(t);break;case"click":case"tap":if(this._decorationActivationConsumed){this._decorationActivationConsumed=!1;break}const r=t;if(r.interactiveElement){const a=new DOMParser().parseFromString(r.interactiveElement,"text/html").body.children[0];if(a.nodeType===a.ELEMENT_NODE&&a.nodeName==="A"&&a.hasAttribute("href")){const n=a.attributes.getNamedItem("href")?.value;if(n.startsWith("#"))this.go(this.currentLocation.copyWithLocations({fragments:[n.substring(1)]}),!1,()=>{});else if(n.startsWith("mailto:")||n.startsWith("tel:"))this.listeners.handleLocator(new u({href:n}).locator);else try{let c;if(n.startsWith("http://")||n.startsWith("https://"))c=n;else if(this.currentLocation.href.startsWith("http://")||this.currentLocation.href.startsWith("https://")){const p=new URL(this.currentLocation.href);c=new URL(n,p).href}else c=g.join(g.dirname(this.currentLocation.href),n);const d=this.pub.readingOrder.findWithHref(c);d?this.goLink(d,!1,()=>{}):(console.warn(`Internal link not found in readingOrder: ${c}`),this.listeners.handleLocator(new u({href:n}).locator))}catch(c){console.warn(`Couldn't resolve internal link for ${n}: ${c}`),this.listeners.handleLocator(new u({href:n}).locator)}}else console.log("Clicked on",a)}else if(e==="click"?this.listeners.click(r):this.listeners.tap(r))break;break;case"scroll":this.listeners.scroll(t);break;case"zoom":this.listeners.zoom(t);break;case"progress":this.syncLocation(t);break;case"content_protection":const o=t;this.listeners.contentProtection(o.type,o);break;case"context_menu":this.listeners.contextMenu(t);break;case"keyboard_peripherals":const s=t,l={...s,interactiveElement:void 0};s.interactiveElement&&(l.interactiveElement=new DOMParser().parseFromString(s.interactiveElement,"text/html").body.children[0]),this.listeners.peripheral(l);break;case"log":console.log(this.framePool.currentFrames[0]?.source?.split("/")[3],...t);break;default:this.listeners.customEvent(e,t);break}}determineModules(){const e=C.slice(),t=I(this.pub.metadata);return t==="cjk-vertical"||t==="mongolian-vertical"?e.map(i=>i==="webpub_snapper"?"cjk_vertical_snapper":i):e}attachListener(){this.framePool.currentFrames[0]?.msg&&(this.framePool.currentFrames[0].msg.listener=(e,t)=>{this.eventListener(e,t)}),this._reapplyDecorationsToCurrentFrame()}async apply(){if(await this.framePool.update(this.pub,this.currentLocation,this.determineModules()),this.attachListener(),this.pub.readingOrder.findIndexWithHref(this.currentLocation.href)<0)throw Error("Link for "+this.currentLocation.href+" not found!")}async destroy(){this._destroyed=!0,this._suspiciousActivityListener&&window.removeEventListener(b,this._suspiciousActivityListener),this._keyboardPeripheralListener&&window.removeEventListener(L,this._keyboardPeripheralListener),this._navigatorProtector?.destroy(),this._keyboardPeripheralsManager?.destroy(),await this.framePool?.destroy(),this._decorations.clear(),this._decorationObservers.clear(),this._decorationHoveredDecorations.clear(),this._decorationActivationState.clear(),this._decorationHoverState.clear(),this._decorationResizeSelectors.clear()}supportsDecorationStyle(e){return N(e,this._decoratorConfig.decorationTemplates)}registerDecorationObserver(e,t){this._decorationObservers.has(e)||this._decorationObservers.set(e,new Set),this._decorationObservers.get(e).add(t),t.onDecorationActivated&&(this._decorationActivationState.set(e,!0),this._sendDecorationActivatable(e,!0)),(t.onDecorationPointerEnter||t.onDecorationPointerLeave)&&(this._decorationHoverState.set(e,!0),this._sendDecorationHoverable(e,!0))}unregisterDecorationObserver(e){this._decorationObservers.forEach((t,i)=>{if(!t.has(e))return;t.delete(e);const r=[...t].some(s=>s.onDecorationActivated);this._decorationActivationState.has(i)&&!r&&(this._decorationActivationState.delete(i),this._sendDecorationActivatable(i,!1));const o=[...t].some(s=>s.onDecorationPointerEnter||s.onDecorationPointerLeave);this._decorationHoverState.has(i)&&!o&&(this._decorationHoverState.delete(i),this._sendDecorationHoverable(i,!1))})}_sendDecorationActivatable(e,t){const i=this.framePool?.currentFrames[0];i?.msg&&i.msg.send("decoration_activatable",{group:e,activatable:t})}_sendDecorationHoverable(e,t){const i=this.framePool?.currentFrames[0];i?.msg&&i.msg.send("decoration_hoverable",{group:e,hoverable:t})}addDecorationResizeTarget(e){this._decorationResizeSelectors.add(e),this.framePool?.addResizeTarget(e)}removeDecorationResizeTarget(e){this._decorationResizeSelectors.delete(e),this.framePool?.removeResizeTarget(e)}applyDecorations(e,t){const i=this._decorations.get(t)??[],r=new Map(i.map(d=>[d.id,d])),o=new Map(e.map(d=>[d.id,d])),s=[],l=[],a=[];for(const[d,p]of r)o.has(d)?z(p,o.get(d))||a.push(o.get(d)):s.push(d);for(const[d,p]of o)r.has(d)||l.push(p);this._decorations.set(t,e),this._sendDecorationOps(t,s,l,a,i);const n=this._decorationActivationState.get(t);n!==void 0&&this._sendDecorationActivatable(t,n);const c=this._decorationHoverState.get(t);c!==void 0&&this._sendDecorationHoverable(t,c)}_sendDecorationOps(e,t,i,r,o){const s=this.framePool?.currentFrames[0];if(!s?.msg)return;const l=this.currentLocation.href,a=new Map(o.map(n=>[n.id,n]));for(const n of t){const c=a.get(n);!c||c.locator.href!==l||s.msg.send("decorate",{group:e,action:"remove",decoration:{id:n}})}for(const n of i)n.locator.href===l&&s.msg.send("decorate",{group:e,action:"add",decoration:m(n,this._decoratorConfig.decorationTemplates)});for(const n of r)n.locator.href===l&&s.msg.send("decorate",{group:e,action:"update",decoration:m(n,this._decoratorConfig.decorationTemplates)})}_reapplyDecorationsToCurrentFrame(){const e=this.framePool?.currentFrames[0];if(!e?.msg)return;const t=this.currentLocation.href;for(const[i,r]of this._decorations){const o=r.filter(s=>s.locator.href===t);if(o.length!==0){e.msg.send("decorate",{group:i,action:"clear"});for(const s of o)e.msg.send("decorate",{group:i,action:"add",decoration:m(s,this._decoratorConfig.decorationTemplates)})}}for(const[i,r]of this._decorationActivationState)e.msg.send("decoration_activatable",{group:i,activatable:r});for(const[i,r]of this._decorationHoverState)e.msg.send("decoration_hoverable",{group:i,hoverable:r})}_handleDecorationActivated(e){const t=this._decorationObservers.get(e.group);if(!t||t.size===0)return!1;const i=(this._decorations.get(e.group)??[]).find(s=>s.id===e.decorationId);if(!i)return!1;const r={decoration:i,group:e.group,rect:e.rect,point:e.point};let o=!1;for(const s of t)s.onDecorationActivated?.(r)&&(o=!0);return o}_handleDecorationPointerEnter(e){const t=this._decorationObservers.get(e.group);if(!t||t.size===0)return;const i=(this._decorations.get(e.group)??[]).find(o=>o.id===e.decorationId);if(!i)return;this._decorationHoveredDecorations.set(e.group,i);const r={decoration:i,group:e.group,rect:e.rect,point:e.point};for(const o of t)o.onDecorationPointerEnter?.(r)}_handleDecorationPointerLeave(e){const t=this._decorationObservers.get(e.group);if(!t||t.size===0)return;const i=(this._decorations.get(e.group)??[]).find(o=>o.id===e.decorationId)??this._decorationHoveredDecorations.get(e.group);if(this._decorationHoveredDecorations.delete(e.group),!i)return;const r={decoration:i,group:e.group,rect:e.rect,point:e.point};for(const o of t)o.onDecorationPointerLeave?.(r)}async changeResource(e){if(e===0)return!1;const t=this.pub.readingOrder.findIndexWithHref(this.currentLocation.href),i=Math.max(0,Math.min(this.pub.readingOrder.items.length-1,t+e));return i===t?!1:(this.currentIndex=i,this.currentLocation=this.createCurrentLocator(),await this.apply(),!0)}updateViewport(e){this.webViewport.readingOrder=[],this.webViewport.progressions.clear(),this.webViewport.positions=null,this.currentLocation&&(this.webViewport.readingOrder.push(this.currentLocation.href),this.webViewport.progressions.set(this.currentLocation.href,e),this.currentLocation.locations?.position!==void 0&&(this.webViewport.positions=[this.currentLocation.locations.position]))}async syncLocation(e){const t=e;this.currentLocation&&(this.currentLocation=this.currentLocation.copyWithLocations({progression:t.start})),this.updateViewport(t),this.listeners.positionChanged(this.currentLocation);const i=t.fragmentId?this.currentLocation.copyWithLocations({fragments:[`#${t.fragmentId}`]}):this.currentLocation;this._visibleFragmentIds=t.visibleFragmentIds??[],this._notifyTimelineChange(i),await this.framePool.update(this.pub,this.currentLocation,this.determineModules())}goBackward(e,t){if(this._isNavigating){t(!1);return}this._isNavigating=!0,this.changeResource(-1).then(i=>{this._isNavigating=!1,t(i)})}goForward(e,t){if(this._isNavigating){t(!1);return}this._isNavigating=!0,this.changeResource(1).then(i=>{this._isNavigating=!1,t(i)})}get currentLocator(){return this.currentLocation}get viewport(){return this.webViewport}get isScrollStart(){const e=this.viewport.readingOrder[0];return this.viewport.progressions.get(e)?.start===0}get isScrollEnd(){const e=this.viewport.readingOrder[this.viewport.readingOrder.length-1];return this.viewport.progressions.get(e)?.end===1}get canGoBackward(){const e=this.pub.readingOrder.items[0]?.href;return!(this.viewport.progressions.has(e)&&this.viewport.progressions.get(e)?.start===0)}get canGoForward(){const e=this.pub.readingOrder.items[this.pub.readingOrder.items.length-1]?.href;return!(this.viewport.progressions.has(e)&&this.viewport.progressions.get(e)?.end===1)}get readingProgression(){return this.pub.metadata.effectiveReadingProgression}get publication(){return this.pub}get timeline(){const e=this.pub.timeline;return this._wrappedTimeline||(this._wrappedTimeline=new Proxy(e,{get:(t,i)=>{if(i!=="navigableFrom"){const r=Reflect.get(t,i,t);return typeof r=="function"?r.bind(t):r}return r=>{const o=this._visibleFragmentIds;let s=(o.length?t.locate(this.currentLocation.copyWithLocations({fragments:[`#${o[0]}`]})):void 0)??r;const l=(o.length?t.locate(this.currentLocation.copyWithLocations({fragments:[`#${o[o.length-1]}`]})):void 0)??r;if(this.isScrollStart){const c=t.ancestors(s).find(d=>d.references.includes(this.currentLocation.href));c&&(s=c)}const a=t.navigableFrom(s).previous,n=t.navigableFrom(l).next;return{previous:a,next:n}}}})),this._wrappedTimeline}async loadLocator(e,t){let i=!1,r=y(e.locations);if(e.text?.highlight?i=await new Promise((a,n)=>{this.framePool.currentFrames[0].msg.send("go_text",r?[e.text?.serialize(),r]:e.text?.serialize(),c=>a(c))}):r&&(i=await new Promise((a,n)=>{this.framePool.currentFrames[0].msg.send("go_text",["",r],c=>a(c))})),i){t(i);return}const o=S(e.locations);if(o&&(i=await new Promise((a,n)=>{this.framePool.currentFrames[0].msg.send("go_id",o,c=>a(c))})),i){t(i);return}const s=e?.locations?.progression;s&&s>0?i=await new Promise((a,n)=>{this.framePool.currentFrames[0].msg.send("go_progression",s,c=>a(c))}):i=!0,t(i)}completeLocator(e){if(!e.href){const t=this.pub.readingOrder.items;let i=!1;if(typeof e.locations.position=="number"){const r=t[e.locations.position-1];r&&(e=(this.pub.manifest.locatorFromLink(r)||new f({href:r.href,type:r.type||"text/html"})).copyWithLocations(e.locations),i=!0)}if(!i&&typeof e.locations?.totalProgression=="number"){const r=e.locations.totalProgression,o=Math.min(t.length-1,Math.max(0,Math.round(r*(t.length-1)))),s=t[o];s&&(e=(this.pub.manifest.locatorFromLink(s)||new f({href:s.href,type:s.type||"text/html"})).copyWithLocations(e.locations))}}return e}go(e,t,i){e=this.completeLocator(e);const r=e.href.split("#")[0];if(!this.pub.readingOrder.findWithHref(r))return i(this.listeners.handleLocator(e));const s=this.pub.readingOrder.findIndexWithHref(r);if(s>=0&&(this.currentIndex=s),this._isNavigating){i(!1);return}this._isNavigating=!0,this.currentLocation=this.createCurrentLocator(),this.apply().then(()=>this.loadLocator(e,l=>{this._isNavigating=!1,i(l)})).then(()=>{this.attachListener()})}goLink(e,t,i){return this.go(e.locator,t,i)}createCurrentLocator(){const t=this.pub.readingOrder.items[this.currentIndex];if(!t)throw new Error("No current resource available");const r=this.currentLocation&&this.currentLocation.href===t.href&&this.currentLocation.locations.progression?this.currentLocation.locations.progression:0;return this.pub.manifest.locatorFromLink(t)||new f({href:t.href,type:t.type||"text/html",locations:new k({fragments:[],progression:r,position:this.currentIndex+1})})}_notifyTimelineChange(e){const t=this.timeline.locate(e),i=!j(this._visibleFragmentIds,this._notifiedVisibleFragmentIds);(t!==this._currentTimelineItem||i)&&(this._currentTimelineItem=t,this._notifiedVisibleFragmentIds=this._visibleFragmentIds,this.listeners.timelineItemChanged(t))}}const ce=B;export{ce as ExperimentalWebPubNavigator,B as WebPubNavigator};
|
|
1
|
+
import{Feature as P,Link as m,Locator as u,LocatorText as x,getCssSelector as g,getHtmlId as k,LocatorLocations as S}from"@readium/shared";import{VisualNavigator as I}from"../Navigator.js";import{WebPubFramePoolManager as C}from"./WebPubFramePoolManager.js";import{WebPubModules as D}from"@readium/navigator-html-injectables";import{p as v}from"../_virtual/index.js";import{getScriptMode as E}from"../helpers/scriptMode.js";import{WebPubCSS as F}from"./css/WebPubCSS.js";import{WebUserProperties as O,WebRSProperties as T}from"./css/Properties.js";import{WebPubPreferences as W}from"./preferences/WebPubPreferences.js";import{WebPubDefaults as A}from"./preferences/WebPubDefaults.js";import{WebPubSettings as b}from"./preferences/WebPubSettings.js";import{WebPubPreferencesEditor as L}from"./preferences/WebPubPreferencesEditor.js";import{Injector as R}from"../injection/Injector.js";import{createReadiumWebPubRules as M}from"../injection/webpubInjectables.js";import{NavigatorProtector as H,NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT as w}from"../protection/NavigatorProtector.js";import{KeyboardPeripherals as N,NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT as y}from"../peripherals/KeyboardPeripherals.js";import{supportsDecorationStyle as z,decorationsEqual as V,resolveDecorationForWire as _}from"@readium/decorator";const j=1e3,U=d=>({frameLoaded:d.frameLoaded||(()=>{}),positionChanged:d.positionChanged||(()=>{}),timelineItemChanged:d.timelineItemChanged||(()=>{}),tap:d.tap||(()=>!1),click:d.click||(()=>!1),zoom:d.zoom||(()=>{}),scroll:d.scroll||(()=>{}),customEvent:d.customEvent||(()=>{}),handleLocator:d.handleLocator||(()=>!1),textSelected:d.textSelected||(()=>{}),contentProtection:d.contentProtection||(()=>{}),contextMenu:d.contextMenu||(()=>{}),peripheral:d.peripheral||(()=>{})});function B(d,e){return d.length===e.length&&d.every((t,i)=>t===e[i])}class q extends I{constructor(e,t,i,r=void 0,o={preferences:{},defaults:{}}){super(),this.currentIndex=0,this._destroyed=!1,this._visibleFragmentIds=[],this._notifiedVisibleFragmentIds=[],this._preferencesEditor=null,this._injector=null,this._isNavigating=!1,this._navigatorProtector=null,this._keyboardPeripheralsManager=null,this._suspiciousActivityListener=null,this._keyboardPeripheralListener=null,this._decorations=new Map,this._decorationObservers=new Map,this._decorationHoveredDecorations=new Map,this._decorationActivationState=new Map,this._decorationHoverState=new Map,this._decorationActivationConsumed=!1,this._navigationLocked=!1,this._pointedPieceRequests=new Map,this._pointedPieceRequestId=0,this.webViewport={readingOrder:[],progressions:new Map,positions:null},this.pub=t,this.container=e,this.listeners=U(i),this._preferences=new W(o.preferences),this._defaults=new A(o.defaults),this._settings=new b(this._preferences,this._defaults,this.hasDisplayTransformability),this._css=new F({rsProperties:new T({experiments:this._settings.experiments||null}),userProperties:new O({zoom:this._settings.zoom})});const n=M(t.readingOrder.items),l=o.injectables||{rules:[],allowedDomains:[]};if(this._injector=new R({rules:[...n,...l.rules],allowedDomains:l.allowedDomains}),this._contentProtection=o.contentProtection||{},this._decoratorConfig=o.decoratorConfig||{},this._decorationResizeSelectors=new Set(this._decoratorConfig.resizeWatchSelectors??[]),this._keyboardPeripherals=this.mergeKeyboardPeripherals(this._contentProtection,o.keyboardPeripherals||[]),(this._contentProtection.disableContextMenu||this._contentProtection.checkAutomation||this._contentProtection.checkIFrameEmbedding||this._contentProtection.monitorDevTools||this._contentProtection.protectPrinting?.disable)&&(this._navigatorProtector=new H(this._contentProtection),this._suspiciousActivityListener=c=>{const{type:a,...s}=c.detail;a==="context_menu"?this.listeners.contextMenu(s):this.listeners.contentProtection(a,s)},window.addEventListener(w,this._suspiciousActivityListener)),this._keyboardPeripherals.length>0&&(this._keyboardPeripheralsManager=new N({keyboardPeripherals:this._keyboardPeripherals}),this._keyboardPeripheralListener=c=>{const a=c.detail;this.listeners.peripheral(a)},window.addEventListener(y,this._keyboardPeripheralListener)),r&&typeof r.copyWithLocations=="function"){this.currentLocation=r;const c=this.pub.readingOrder.findIndexWithHref(r.href);c>=0&&(this.currentIndex=c)}else this.currentLocation=this.createCurrentLocator()}async load(){if(await this.updateCSS(!1),this._destroyed)return;const e=this.compileCSSProperties(this._css);this.framePool=new C(this.container,e,this._injector,this._contentProtection,this._keyboardPeripherals,t=>this.pub.timeline.segmentsForHref(t).flatMap(i=>i.references).map(i=>{const r=i.indexOf("#");return r>=0?i.slice(r+1):""}).filter(Boolean),[...this._decorationResizeSelectors]),!this._destroyed&&(this.currentLocation=this.completeLocator(this.currentLocation),await this.apply())}get settings(){return Object.freeze({...this._settings})}get preferencesEditor(){return this._preferencesEditor===null&&(this._preferencesEditor=new L(this._preferences,this.settings,this.pub.metadata)),this._preferencesEditor}async submitPreferences(e){this._preferences=this._preferences.merging(e),await this.applyPreferences()}async applyPreferences(){this._settings=new b(this._preferences,this._defaults,this.hasDisplayTransformability),this._preferencesEditor!==null&&(this._preferencesEditor=new L(this._preferences,this.settings,this.pub.metadata)),await this.updateCSS(!0)}async updateCSS(e){this._css.update(this._settings),e&&await this.commitCSS(this._css)}compileCSSProperties(e){const t={};for(const[i,r]of Object.entries(e.rsProperties.toCSSProperties()))t[i]=r;for(const[i,r]of Object.entries(e.userProperties.toCSSProperties()))t[i]=r;return t}async commitCSS(e){const t=this.compileCSSProperties(e);this.framePool.setCSSProperties(t)}get _cframes(){return this.framePool.currentFrames}get hasDisplayTransformability(){return this.pub.metadata?.accessibility?.feature?.some(e=>e.value===P.DISPLAY_TRANSFORMABILITY.value)??!1}eventListener(e,t,i){switch(e){case"_pong":this.listeners.frameLoaded(this.framePool.currentFrames[0].iframe.contentWindow),this.listeners.positionChanged(this.currentLocation),this._notifyTimelineChange(this.currentLocation),this._reapplyDecorationsToCurrentFrame(),this._watchTextLayoutInCurrentFrame(!0),this._sendNavigationLock();break;case"text_layout":i&&i===this._textLayoutFrame&&this.currentLocation.href.split("#")[0]===this._textLayoutWatch?.href&&this._textLayoutWatch.cb(t);break;case"pointed_piece":{const{id:a,index:s}=t;this._pointedPieceRequests.get(a)?.(s);break}case"first_visible_locator":const r=u.deserialize(t);if(!r)break;this.currentLocation=new u({href:this.currentLocation.href,type:this.currentLocation.type,title:this.currentLocation.title,locations:r?.locations,text:r?.text}),this.listeners.positionChanged(this.currentLocation),this._notifyTimelineChange(this.currentLocation);break;case"text_selected":{const a=t;a.locator=new u({href:this.currentLocation.href,type:this.currentLocation.type,text:new x({highlight:a.text})}),this.listeners.textSelected(a);break}case"decoration_activated":{this._handleDecorationActivated(t)&&(this._decorationActivationConsumed=!0);break}case"decoration_pointer_enter":this._handleDecorationPointerEnter(t);break;case"decoration_pointer_leave":this._handleDecorationPointerLeave(t);break;case"click":case"tap":if(this._decorationActivationConsumed){this._decorationActivationConsumed=!1;break}const o=t;if(o.interactiveElement){const a=new DOMParser().parseFromString(o.interactiveElement,"text/html").body.children[0];if(a.nodeType===a.ELEMENT_NODE&&a.nodeName==="A"&&a.hasAttribute("href")){const s=a.attributes.getNamedItem("href")?.value;if(s.startsWith("#"))this._navigationLocked||this.go(this.currentLocation.copyWithLocations({fragments:[s.substring(1)]}),!1,()=>{});else if(s.startsWith("mailto:")||s.startsWith("tel:"))this.listeners.handleLocator(new m({href:s}).locator);else try{let h;if(s.startsWith("http://")||s.startsWith("https://"))h=s;else if(this.currentLocation.href.startsWith("http://")||this.currentLocation.href.startsWith("https://")){const p=new URL(this.currentLocation.href);h=new URL(s,p).href}else h=v.join(v.dirname(this.currentLocation.href),s);const f=this.pub.readingOrder.findWithHref(h);f?this._navigationLocked||this.goLink(f,!1,()=>{}):(console.warn(`Internal link not found in readingOrder: ${h}`),this.listeners.handleLocator(new m({href:s}).locator))}catch(h){console.warn(`Couldn't resolve internal link for ${s}: ${h}`),this.listeners.handleLocator(new m({href:s}).locator)}}else console.log("Clicked on",a)}else if(e==="click"?this.listeners.click(o):this.listeners.tap(o))break;break;case"scroll":this.listeners.scroll(t);break;case"zoom":this.listeners.zoom(t);break;case"progress":{const a=i&&this.framePool._frameFor(this.currentLocation.href.split("#")[0]);if(i&&i!==a)break;this.syncLocation(t);break}case"content_protection":const n=t;this.listeners.contentProtection(n.type,n);break;case"context_menu":this.listeners.contextMenu(t);break;case"keyboard_peripherals":const l=t,c={...l,interactiveElement:void 0};l.interactiveElement&&(c.interactiveElement=new DOMParser().parseFromString(l.interactiveElement,"text/html").body.children[0]),this.listeners.peripheral(c);break;case"log":console.log(this.framePool.currentFrames[0]?.source?.split("/")[3],...t);break;default:this.listeners.customEvent(e,t);break}}determineModules(){const e=D.slice(),t=E(this.pub.metadata);return t==="cjk-vertical"||t==="mongolian-vertical"?e.map(i=>i==="webpub_snapper"?"cjk_vertical_snapper":i):e}attachListener(){this.framePool.currentFrames.filter(t=>!!t).forEach(t=>{t.msg&&(t.msg.listener=(i,r)=>{this.eventListener(i,r,t)})}),this._reapplyDecorationsToCurrentFrame(),this._watchTextLayoutInCurrentFrame(),this._sendNavigationLock()}async apply(){if(await this.framePool.update(this.pub,this.currentLocation,this.determineModules()),this.attachListener(),this.pub.readingOrder.findIndexWithHref(this.currentLocation.href)<0)throw Error("Link for "+this.currentLocation.href+" not found!")}async destroy(){this._destroyed=!0,this._suspiciousActivityListener&&window.removeEventListener(w,this._suspiciousActivityListener),this._keyboardPeripheralListener&&window.removeEventListener(y,this._keyboardPeripheralListener),this._navigatorProtector?.destroy(),this._keyboardPeripheralsManager?.destroy(),await this.framePool?.destroy(),this._decorations.clear(),this._decorationObservers.clear(),this._decorationHoveredDecorations.clear(),this._decorationActivationState.clear(),this._decorationHoverState.clear(),this._decorationResizeSelectors.clear()}supportsDecorationStyle(e){return z(e,this._decoratorConfig.decorationTemplates)}registerDecorationObserver(e,t){this._decorationObservers.has(e)||this._decorationObservers.set(e,new Set),this._decorationObservers.get(e).add(t),t.onDecorationActivated&&(this._decorationActivationState.set(e,!0),this._sendDecorationActivatable(e,!0)),(t.onDecorationPointerEnter||t.onDecorationPointerLeave)&&(this._decorationHoverState.set(e,!0),this._sendDecorationHoverable(e,!0))}unregisterDecorationObserver(e){this._decorationObservers.forEach((t,i)=>{if(!t.has(e))return;t.delete(e);const r=[...t].some(n=>n.onDecorationActivated);this._decorationActivationState.has(i)&&!r&&(this._decorationActivationState.delete(i),this._sendDecorationActivatable(i,!1));const o=[...t].some(n=>n.onDecorationPointerEnter||n.onDecorationPointerLeave);this._decorationHoverState.has(i)&&!o&&(this._decorationHoverState.delete(i),this._sendDecorationHoverable(i,!1))})}_sendDecorationActivatable(e,t){const i=this.framePool?.currentFrames[0];i?.msg&&i.msg.send("decoration_activatable",{group:e,activatable:t})}_sendDecorationHoverable(e,t){const i=this.framePool?.currentFrames[0];i?.msg&&i.msg.send("decoration_hoverable",{group:e,hoverable:t})}addDecorationResizeTarget(e){this._decorationResizeSelectors.add(e),this.framePool?.addResizeTarget(e)}removeDecorationResizeTarget(e){this._decorationResizeSelectors.delete(e),this.framePool?.removeResizeTarget(e)}applyDecorations(e,t){const i=this._decorations.get(t)??[],r=new Map(i.map(h=>[h.id,h])),o=new Map(e.map(h=>[h.id,h])),n=[],l=[],c=[];for(const[h,f]of r)o.has(h)?V(f,o.get(h))||c.push(o.get(h)):n.push(h);for(const[h,f]of o)r.has(h)||l.push(f);this._decorations.set(t,e),this._sendDecorationOps(t,n,l,c,i);const a=this._decorationActivationState.get(t);a!==void 0&&this._sendDecorationActivatable(t,a);const s=this._decorationHoverState.get(t);s!==void 0&&this._sendDecorationHoverable(t,s)}_sendDecorationOps(e,t,i,r,o){const n=this.framePool?.currentFrames[0];if(!n?.msg)return;const l=this.currentLocation.href,c=new Map(o.map(a=>[a.id,a]));for(const a of t){const s=c.get(a);!s||s.locator.href!==l||n.msg.send("decorate",{group:e,action:"remove",decoration:{id:a}})}for(const a of i)a.locator.href===l&&n.msg.send("decorate",{group:e,action:"add",decoration:_(a,this._decoratorConfig.decorationTemplates)});for(const a of r)a.locator.href===l&&n.msg.send("decorate",{group:e,action:"update",decoration:_(a,this._decoratorConfig.decorationTemplates)})}_reapplyDecorationsToCurrentFrame(){const e=this.framePool?.currentFrames[0];if(!e?.msg)return;const t=this.currentLocation.href;for(const[i,r]of this._decorations){const o=r.filter(n=>n.locator.href===t);if(o.length!==0){e.msg.send("decorate",{group:i,action:"clear"});for(const n of o)e.msg.send("decorate",{group:i,action:"add",decoration:_(n,this._decoratorConfig.decorationTemplates)})}}for(const[i,r]of this._decorationActivationState)e.msg.send("decoration_activatable",{group:i,activatable:r});for(const[i,r]of this._decorationHoverState)e.msg.send("decoration_hoverable",{group:i,hoverable:r})}watchTextLayout(e,t){const i={href:e[0]?.href.split("#")[0]??"",pieces:e.map(r=>[r.text?.serialize(),g(r.locations)]),cb:t};return this._textLayoutWatch=i,this._watchTextLayoutInCurrentFrame(!0),()=>{this._textLayoutWatch===i&&(this._textLayoutWatch=void 0,this._watchTextLayoutInCurrentFrame())}}lockNavigation(e){this._navigationLocked=e,this._sendNavigationLock()}_sendNavigationLock(){this.framePool?.currentFrames[0]?.msg?.send("lock_navigation",this._navigationLocked)}async findPointedPiece(e,t){const i=this.framePool.currentFrames.filter(s=>!!s),r=i.findIndex(s=>s.source===e.targetFrameSrc),o=r>=0?this.viewport.readingOrder[r]:void 0;if(!o||!e.cssSelector)return;const n=t.filter(s=>s.href.split("#")[0]===o);if(n.length===0)return;const l=`${++this._pointedPieceRequestId}`;let c;const a=await new Promise(s=>{this._pointedPieceRequests.set(l,s),c=setTimeout(()=>s(-1),j);const h={id:l,cssSelector:e.cssSelector,x:e.x,y:e.y,pieces:n.map(p=>[p.text?.serialize(),g(p.locations)])};i[r].msg?.send("pointed_piece",h,()=>s(-1))||s(-1)});return clearTimeout(c),this._pointedPieceRequests.delete(l),n[a]}_watchTextLayoutInCurrentFrame(e=!1){const t=this.framePool?.currentFrames[0],i=this._textLayoutWatch,r=i&&t&&this.currentLocation?.href.split("#")[0]===i.href?t:void 0;if(r===this._textLayoutFrame&&!e)return;const o=this._textLayoutFrame;o&&o!==r&&!o.isDestroyed&&o.msg?.send("watch_text_layout",[]),this._textLayoutFrame=r,r&&i&&r.msg?.send("watch_text_layout",i.pieces)}_handleDecorationActivated(e){const t=this._decorationObservers.get(e.group);if(!t||t.size===0)return!1;const i=(this._decorations.get(e.group)??[]).find(n=>n.id===e.decorationId);if(!i)return!1;const r={decoration:i,group:e.group,rect:e.rect,point:e.point};let o=!1;for(const n of t)n.onDecorationActivated?.(r)&&(o=!0);return o}_handleDecorationPointerEnter(e){const t=this._decorationObservers.get(e.group);if(!t||t.size===0)return;const i=(this._decorations.get(e.group)??[]).find(o=>o.id===e.decorationId);if(!i)return;this._decorationHoveredDecorations.set(e.group,i);const r={decoration:i,group:e.group,rect:e.rect,point:e.point};for(const o of t)o.onDecorationPointerEnter?.(r)}_handleDecorationPointerLeave(e){const t=this._decorationObservers.get(e.group);if(!t||t.size===0)return;const i=(this._decorations.get(e.group)??[]).find(o=>o.id===e.decorationId)??this._decorationHoveredDecorations.get(e.group);if(this._decorationHoveredDecorations.delete(e.group),!i)return;const r={decoration:i,group:e.group,rect:e.rect,point:e.point};for(const o of t)o.onDecorationPointerLeave?.(r)}async changeResource(e){if(e===0)return!1;const t=this.pub.readingOrder.findIndexWithHref(this.currentLocation.href),i=Math.max(0,Math.min(this.pub.readingOrder.items.length-1,t+e));return i===t?!1:(this.currentIndex=i,this.currentLocation=this.createCurrentLocator(),await this.apply(),!0)}updateViewport(e){this.webViewport.readingOrder=[],this.webViewport.progressions.clear(),this.webViewport.positions=null,this.currentLocation&&(this.webViewport.readingOrder.push(this.currentLocation.href),this.webViewport.progressions.set(this.currentLocation.href,e),this.currentLocation.locations?.position!==void 0&&(this.webViewport.positions=[this.currentLocation.locations.position]))}async syncLocation(e){const t=e;this.currentLocation&&(this.currentLocation=this.currentLocation.copyWithLocations({progression:t.start})),this.updateViewport(t),this.listeners.positionChanged(this.currentLocation);const i=t.fragmentId?this.currentLocation.copyWithLocations({fragments:[`#${t.fragmentId}`]}):this.currentLocation;this._visibleFragmentIds=t.visibleFragmentIds??[],this._notifyTimelineChange(i),await this.framePool.update(this.pub,this.currentLocation,this.determineModules())}goBackward(e,t){if(this._isNavigating){t(!1);return}this._isNavigating=!0,this.changeResource(-1).then(i=>{this._isNavigating=!1,t(i)})}goForward(e,t){if(this._isNavigating){t(!1);return}this._isNavigating=!0,this.changeResource(1).then(i=>{this._isNavigating=!1,t(i)})}get currentLocator(){return this.currentLocation}get viewport(){return this.webViewport}get isScrollStart(){const e=this.viewport.readingOrder[0];return this.viewport.progressions.get(e)?.start===0}get isScrollEnd(){const e=this.viewport.readingOrder[this.viewport.readingOrder.length-1];return this.viewport.progressions.get(e)?.end===1}get canGoBackward(){const e=this.pub.readingOrder.items[0]?.href;return!(this.viewport.progressions.has(e)&&this.viewport.progressions.get(e)?.start===0)}get canGoForward(){const e=this.pub.readingOrder.items[this.pub.readingOrder.items.length-1]?.href;return!(this.viewport.progressions.has(e)&&this.viewport.progressions.get(e)?.end===1)}get readingProgression(){return this.pub.metadata.effectiveReadingProgression}get publication(){return this.pub}get timeline(){const e=this.pub.timeline;return this._wrappedTimeline||(this._wrappedTimeline=new Proxy(e,{get:(t,i)=>{if(i!=="navigableFrom"){const r=Reflect.get(t,i,t);return typeof r=="function"?r.bind(t):r}return r=>{const o=this._visibleFragmentIds;let n=(o.length?t.locate(this.currentLocation.copyWithLocations({fragments:[`#${o[0]}`]})):void 0)??r;const l=(o.length?t.locate(this.currentLocation.copyWithLocations({fragments:[`#${o[o.length-1]}`]})):void 0)??r;if(this.isScrollStart){const s=t.ancestors(n).find(h=>h.references.includes(this.currentLocation.href));s&&(n=s)}const c=t.navigableFrom(n).previous,a=t.navigableFrom(l).next;return{previous:c,next:a}}}})),this._wrappedTimeline}async loadLocator(e,t){let i=!1,r=g(e.locations);if(e.text?.highlight?i=await new Promise((c,a)=>{this.framePool.currentFrames[0].msg.send("go_text",r?[e.text?.serialize(),r]:e.text?.serialize(),s=>c(s))}):r&&(i=await new Promise((c,a)=>{this.framePool.currentFrames[0].msg.send("go_text",["",r],s=>c(s))})),i){t(i);return}const o=k(e.locations);if(o&&(i=await new Promise((c,a)=>{this.framePool.currentFrames[0].msg.send("go_id",o,s=>c(s))})),i){t(i);return}const n=e?.locations?.progression;n&&n>0?i=await new Promise((c,a)=>{this.framePool.currentFrames[0].msg.send("go_progression",n,s=>c(s))}):i=!0,t(i)}completeLocator(e){if(!e.href){const t=this.pub.readingOrder.items;let i=!1;if(typeof e.locations.position=="number"){const r=t[e.locations.position-1];r&&(e=(this.pub.manifest.locatorFromLink(r)||new u({href:r.href,type:r.type||"text/html"})).copyWithLocations(e.locations),i=!0)}if(!i&&typeof e.locations?.totalProgression=="number"){const r=e.locations.totalProgression,o=Math.min(t.length-1,Math.max(0,Math.round(r*(t.length-1)))),n=t[o];n&&(e=(this.pub.manifest.locatorFromLink(n)||new u({href:n.href,type:n.type||"text/html"})).copyWithLocations(e.locations))}}return e}go(e,t,i){e=this.completeLocator(e);const r=e.href.split("#")[0];if(!this.pub.readingOrder.findWithHref(r))return i(this.listeners.handleLocator(e));const n=this.pub.readingOrder.findIndexWithHref(r);if(n>=0&&(this.currentIndex=n),this._isNavigating){i(!1);return}this._isNavigating=!0,this.currentLocation=this.createCurrentLocator(),this.apply().then(()=>this.loadLocator(e,l=>{this._isNavigating=!1,i(l)})).then(()=>{this.attachListener()})}goLink(e,t,i){return this.go(e.locator,t,i)}createCurrentLocator(){const t=this.pub.readingOrder.items[this.currentIndex];if(!t)throw new Error("No current resource available");const r=this.currentLocation&&this.currentLocation.href===t.href&&this.currentLocation.locations.progression?this.currentLocation.locations.progression:0;return this.pub.manifest.locatorFromLink(t)||new u({href:t.href,type:t.type||"text/html",locations:new S({fragments:[],progression:r,position:this.currentIndex+1})})}_notifyTimelineChange(e){const t=this.timeline.locate(e),i=!B(this._visibleFragmentIds,this._notifiedVisibleFragmentIds);(t!==this._currentTimelineItem||i)&&(this._currentTimelineItem=t,this._notifiedVisibleFragmentIds=this._visibleFragmentIds,this.listeners.timelineItemChanged(t))}}const le=q;export{le as ExperimentalWebPubNavigator,q as WebPubNavigator};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@readium/navigator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Next generation SDK for publications in Web Apps",
|
|
6
6
|
"author": "readium",
|
|
@@ -41,13 +41,15 @@
|
|
|
41
41
|
"types"
|
|
42
42
|
],
|
|
43
43
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
44
|
+
"node": ">=24"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@readium/
|
|
48
|
-
"@readium/
|
|
49
|
-
"@readium/
|
|
50
|
-
"@readium/
|
|
47
|
+
"@readium/speech": "^0.14.1",
|
|
48
|
+
"@readium/decorator": "1.3.0",
|
|
49
|
+
"@readium/guided-navigation": "1.1.1",
|
|
50
|
+
"@readium/helpers": "1.2.1",
|
|
51
|
+
"@readium/shared": "2.8.0",
|
|
52
|
+
"@readium/navigator-html-injectables": "2.9.0"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@readium/css": "^2.0.5",
|
|
@@ -2,7 +2,7 @@ import { Layout, Link, Locator, LocatorText, Profile, Publication, ReadingProgre
|
|
|
2
2
|
import { Configurable, ConfigurableSettings, LineLengths, ProgressionRange, VisualNavigator, VisualNavigatorViewport } from "../index.ts";
|
|
3
3
|
import { FramePoolManager } from "./frame/FramePoolManager.ts";
|
|
4
4
|
import { FXLFramePoolManager } from "./fxl/FXLFramePoolManager.ts";
|
|
5
|
-
import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData, FXLModules, ModuleLibrary, ModuleName, ReflowableModules, BasicTextSelection, FrameClickEvent, SuspiciousActivityEvent, KeyboardPeripheralEvent } from "@readium/navigator-html-injectables";
|
|
5
|
+
import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData, FXLModules, ModuleLibrary, ModuleName, ReflowableModules, BasicTextSelection, FrameClickEvent, SuspiciousActivityEvent, KeyboardPeripheralEvent, TextLayout, PointedPieceRequest, PointedPieceResponse } from "@readium/navigator-html-injectables";
|
|
6
6
|
import { Decoration, OnDecorationActivatedEvent, OnDecorationPointerEnterEvent, OnDecorationPointerLeaveEvent, DecorationObserver, DecorableNavigator, DecoratorConfig, decorationsEqual, resolveDecorationForWire, supportsDecorationStyle as canRenderDecorationStyle, DecorationStyleType } from "../decorations/index.ts";
|
|
7
7
|
import * as path from "path-browserify";
|
|
8
8
|
import { FXLFrameManager } from "./fxl/FXLFrameManager.ts";
|
|
@@ -56,6 +56,9 @@ export interface EpubNavigatorListeners {
|
|
|
56
56
|
// showToc: () => void;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Expired comms callbacks are dropped, never called.
|
|
60
|
+
const POINTED_PIECE_TIMEOUT = 1000;
|
|
61
|
+
|
|
59
62
|
const defaultListeners = (listeners: EpubNavigatorListeners): EpubNavigatorListeners => ({
|
|
60
63
|
frameLoaded: listeners.frameLoaded || (() => {}),
|
|
61
64
|
positionChanged: listeners.positionChanged || (() => {}),
|
|
@@ -122,6 +125,11 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
122
125
|
private _decorationHoverState: Map<string, boolean> = new Map();
|
|
123
126
|
private _decorationActivationConsumed = false;
|
|
124
127
|
private _decorationResizeSelectors: Set<string>;
|
|
128
|
+
private _textLayoutWatch?: { href: string; pieces: unknown[]; cb: (layout: TextLayout) => void };
|
|
129
|
+
private _textLayoutFrame?: FrameManager;
|
|
130
|
+
private _navigationLocked = false;
|
|
131
|
+
private readonly _pointedPieceRequests = new Map<string, (index: number) => void>();
|
|
132
|
+
private _pointedPieceRequestId = 0;
|
|
125
133
|
|
|
126
134
|
private reflowViewport: VisualNavigatorViewport = {
|
|
127
135
|
readingOrder: [],
|
|
@@ -491,7 +499,18 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
491
499
|
} else {
|
|
492
500
|
this._reapplyDecorationsToCurrentFrames();
|
|
493
501
|
}
|
|
502
|
+
this._watchTextLayoutInCurrentFrame(true);
|
|
503
|
+
this._sendNavigationLock();
|
|
504
|
+
break;
|
|
505
|
+
case "text_layout":
|
|
506
|
+
// A frame being hidden after moving to another resource lays out again, collapsed.
|
|
507
|
+
if (sourceFrame && sourceFrame === this._textLayoutFrame && this.currentLocation.href.split("#")[0] === this._textLayoutWatch?.href) this._textLayoutWatch.cb(data as TextLayout);
|
|
494
508
|
break;
|
|
509
|
+
case "pointed_piece": {
|
|
510
|
+
const { id, index } = data as PointedPieceResponse;
|
|
511
|
+
this._pointedPieceRequests.get(id)?.(index);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
495
514
|
case "first_visible_locator":
|
|
496
515
|
const loc = Locator.deserialize(data as string);
|
|
497
516
|
if(!loc) break;
|
|
@@ -549,7 +568,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
549
568
|
) {
|
|
550
569
|
const origHref = element.attributes.getNamedItem("href")?.value!;
|
|
551
570
|
if (origHref.startsWith("#")) {
|
|
552
|
-
this.go(this.currentLocation.copyWithLocations({
|
|
571
|
+
if (!this._navigationLocked) this.go(this.currentLocation.copyWithLocations({
|
|
553
572
|
fragments: [origHref.substring(1)]
|
|
554
573
|
}), false, () => { });
|
|
555
574
|
} else if(
|
|
@@ -561,7 +580,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
561
580
|
this.listeners.handleLocator(new Link({
|
|
562
581
|
href: origHref,
|
|
563
582
|
}).locator);
|
|
564
|
-
} else {
|
|
583
|
+
} else if (!this._navigationLocked) {
|
|
565
584
|
try {
|
|
566
585
|
this.goLink(new Link({
|
|
567
586
|
href: path.join(path.dirname(this.currentLocation.href), origHref)
|
|
@@ -578,21 +597,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
578
597
|
if(this._layout === Layout.fixed && (this.framePool as FXLFramePoolManager).doNotDisturb)
|
|
579
598
|
edata.doNotDisturb = true;
|
|
580
599
|
|
|
581
|
-
|
|
582
|
-
&& (
|
|
583
|
-
this.currentProgression === ReadingProgression.rtl ||
|
|
584
|
-
this.currentProgression === ReadingProgression.ltr
|
|
585
|
-
)
|
|
586
|
-
) {
|
|
587
|
-
if(this.framePool.currentFrames.length > 1) {
|
|
588
|
-
// Spread page dimensions
|
|
589
|
-
const cfs = this.framePool.currentFrames;
|
|
590
|
-
if(edata.targetFrameSrc === cfs[this.currentProgression === ReadingProgression.rtl ? 0 : 1]?.source) {
|
|
591
|
-
// The right page (screen-wise) was clicked, so we add the left page's width to the click's x
|
|
592
|
-
edata.x += (cfs[this.currentProgression === ReadingProgression.rtl ? 1 : 0]?.iframe.contentWindow?.innerWidth ?? 0) * window.devicePixelRatio;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
600
|
+
edata.x += this._spreadOffsetX(edata.targetFrameSrc);
|
|
596
601
|
|
|
597
602
|
const handled = key === "click" ? this.listeners.click(edata) : this.listeners.tap(edata);
|
|
598
603
|
if(handled) break;
|
|
@@ -600,6 +605,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
600
605
|
const oneQuarter = ((this._cframes.length === 2 ? this._cframes[0]!.window.innerWidth + this._cframes[1]!.window.innerWidth : this._cframes[0]!.window.innerWidth) * window.devicePixelRatio) / 4;
|
|
601
606
|
// open UI if middle screen is clicked/tapped
|
|
602
607
|
if (edata.x >= oneQuarter && edata.x <= oneQuarter * 3) this.listeners.miscPointer(1);
|
|
608
|
+
if (this._navigationLocked) break;
|
|
603
609
|
if (edata.x < oneQuarter) this.goLeft(false, () => { }); // Go left if left quarter clicked
|
|
604
610
|
else if (edata.x > oneQuarter * 3) this.goRight(false, () => { }); // Go right if right quarter clicked
|
|
605
611
|
}
|
|
@@ -608,10 +614,10 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
608
614
|
this.listeners.miscPointer(data as number);
|
|
609
615
|
break;
|
|
610
616
|
case "no_more":
|
|
611
|
-
this.changeResource(1);
|
|
617
|
+
if (!this._navigationLocked) this.changeResource(1);
|
|
612
618
|
break;
|
|
613
619
|
case "no_less":
|
|
614
|
-
this.changeResource(-1);
|
|
620
|
+
if (!this._navigationLocked) this.changeResource(-1);
|
|
615
621
|
break;
|
|
616
622
|
case "swipe":
|
|
617
623
|
// Swipe event
|
|
@@ -622,9 +628,13 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
622
628
|
case "zoom":
|
|
623
629
|
this.listeners.zoom(data as number);
|
|
624
630
|
break;
|
|
625
|
-
case "progress":
|
|
631
|
+
case "progress": {
|
|
632
|
+
// The previous resource's frame can still report once moving to another, which would take its progress.
|
|
633
|
+
const frame = sourceFrame instanceof FrameManager ? (this.framePool as FramePoolManager)._frameFor(this.currentLocation.href.split("#")[0]) : undefined;
|
|
634
|
+
if (sourceFrame instanceof FrameManager && sourceFrame !== frame) break;
|
|
626
635
|
this.syncLocation(data as ProgressionRange);
|
|
627
636
|
break;
|
|
637
|
+
}
|
|
628
638
|
case "content_protection":
|
|
629
639
|
const activity = data as SuspiciousActivityEvent;
|
|
630
640
|
this.listeners.contentProtection(activity.type, activity);
|
|
@@ -685,6 +695,8 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
685
695
|
}
|
|
686
696
|
})
|
|
687
697
|
this._reapplyDecorationsToCurrentFrames();
|
|
698
|
+
this._watchTextLayoutInCurrentFrame();
|
|
699
|
+
this._sendNavigationLock();
|
|
688
700
|
}
|
|
689
701
|
|
|
690
702
|
private async apply() {
|
|
@@ -859,6 +871,100 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
859
871
|
});
|
|
860
872
|
}
|
|
861
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Reports where the text of `locators`, all in one reflowable resource, starts each line or column
|
|
876
|
+
* along the scroll axis, with the viewport. Reported again whenever that resource is scrolled or laid out again.
|
|
877
|
+
* Replaces any previous watch, and returns a function that stops this one.
|
|
878
|
+
*/
|
|
879
|
+
public watchTextLayout(locators: Locator[], cb: (layout: TextLayout) => void): () => void {
|
|
880
|
+
const watch = {
|
|
881
|
+
href: locators[0]?.href.split("#")[0] ?? "",
|
|
882
|
+
pieces: locators.map(locator => [locator.text?.serialize(), getCssSelector(locator.locations)]),
|
|
883
|
+
cb,
|
|
884
|
+
};
|
|
885
|
+
this._textLayoutWatch = watch;
|
|
886
|
+
this._watchTextLayoutInCurrentFrame(true);
|
|
887
|
+
return () => {
|
|
888
|
+
if (this._textLayoutWatch !== watch) return;
|
|
889
|
+
this._textLayoutWatch = undefined;
|
|
890
|
+
this._watchTextLayoutInCurrentFrame();
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Locks or unlocks navigating in the content: scrolling, swiping, tapping its edges and following its links.
|
|
896
|
+
* Calls to the navigator still move it.
|
|
897
|
+
*/
|
|
898
|
+
public lockNavigation(locked: boolean): void {
|
|
899
|
+
this._navigationLocked = locked;
|
|
900
|
+
this._sendNavigationLock();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Fixed-layout frames don't scroll, and their swipes are reported as no_more and no_less.
|
|
904
|
+
private _sendNavigationLock(): void {
|
|
905
|
+
this._cframes.forEach(frame => {
|
|
906
|
+
if (frame instanceof FrameManager) frame.msg?.send("lock_navigation", this._navigationLocked);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** The left page's width when `targetFrameSrc` is the right page (screen-wise) of a fixed-layout spread, else 0. */
|
|
911
|
+
private _spreadOffsetX(targetFrameSrc: string): number {
|
|
912
|
+
if (this._layout !== Layout.fixed || (
|
|
913
|
+
this.currentProgression !== ReadingProgression.rtl &&
|
|
914
|
+
this.currentProgression !== ReadingProgression.ltr
|
|
915
|
+
)) return 0;
|
|
916
|
+
const cfs = this.framePool.currentFrames;
|
|
917
|
+
if (cfs.length < 2) return 0;
|
|
918
|
+
const rtl = this.currentProgression === ReadingProgression.rtl;
|
|
919
|
+
if (targetFrameSrc !== cfs[rtl ? 0 : 1]?.source) return 0;
|
|
920
|
+
return (cfs[rtl ? 1 : 0]?.iframe.contentWindow?.innerWidth ?? 0) * window.devicePixelRatio;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Returns the first of `pieces` under the point of `event`, else the first containing the element it targets,
|
|
925
|
+
* in the resource of the frame it comes from. Undefined when none does.
|
|
926
|
+
*/
|
|
927
|
+
public async findPointedPiece(event: FrameClickEvent, pieces: Locator[]): Promise<Locator | undefined> {
|
|
928
|
+
const frames = this._cframes.filter(f => !!f) as (FrameManager | FXLFrameManager)[];
|
|
929
|
+
const i = frames.findIndex(frame => frame.source === event.targetFrameSrc);
|
|
930
|
+
const href = i >= 0 ? this.viewport.readingOrder[i] : undefined;
|
|
931
|
+
if (!href || !event.cssSelector) return undefined;
|
|
932
|
+
const candidates = pieces.filter(piece => piece.href.split("#")[0] === href);
|
|
933
|
+
if (candidates.length === 0) return undefined;
|
|
934
|
+
|
|
935
|
+
const id = `${++this._pointedPieceRequestId}`;
|
|
936
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
937
|
+
const index = await new Promise<number>(resolve => {
|
|
938
|
+
this._pointedPieceRequests.set(id, resolve);
|
|
939
|
+
timeout = setTimeout(() => resolve(-1), POINTED_PIECE_TIMEOUT);
|
|
940
|
+
const request: PointedPieceRequest = {
|
|
941
|
+
id,
|
|
942
|
+
cssSelector: event.cssSelector!,
|
|
943
|
+
x: event.x - this._spreadOffsetX(event.targetFrameSrc),
|
|
944
|
+
y: event.y,
|
|
945
|
+
pieces: candidates.map(piece => [piece.text?.serialize(), getCssSelector(piece.locations)])
|
|
946
|
+
};
|
|
947
|
+
// The reply is posted before the ack, so a pending request on ack got no reply.
|
|
948
|
+
const sent = frames[i].msg?.send("pointed_piece", request, () => resolve(-1));
|
|
949
|
+
if (!sent) resolve(-1);
|
|
950
|
+
});
|
|
951
|
+
clearTimeout(timeout);
|
|
952
|
+
this._pointedPieceRequests.delete(id);
|
|
953
|
+
return candidates[index];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// The previous frame stops watching, so only the displayed resource reports its layout.
|
|
957
|
+
private _watchTextLayoutInCurrentFrame(resend = false): void {
|
|
958
|
+
const frame = this._cframes[0];
|
|
959
|
+
const watch = this._textLayoutWatch;
|
|
960
|
+
const target = watch && frame instanceof FrameManager && this.currentLocation?.href.split("#")[0] === watch.href ? frame : undefined;
|
|
961
|
+
if (target === this._textLayoutFrame && !resend) return;
|
|
962
|
+
const previous = this._textLayoutFrame;
|
|
963
|
+
if (previous && previous !== target && !previous.isDestroyed) previous.msg?.send("watch_text_layout", []);
|
|
964
|
+
this._textLayoutFrame = target;
|
|
965
|
+
if (target && watch) target.msg?.send("watch_text_layout", watch.pieces);
|
|
966
|
+
}
|
|
967
|
+
|
|
862
968
|
private _handleDecorationActivated(data: DecorationActivatedEvent): boolean {
|
|
863
969
|
const observers = this._decorationObservers.get(data.group);
|
|
864
970
|
if (!observers || observers.size === 0) return false;
|
|
@@ -14,19 +14,22 @@ interface RegistryValue {
|
|
|
14
14
|
cb: CommsAck;
|
|
15
15
|
}
|
|
16
16
|
const REGISTRY_EXPIRY = 10000; // 10 seconds max
|
|
17
|
+
const GC_INTERVAL = 5000;
|
|
17
18
|
|
|
18
19
|
export type FrameCommsListener = (key: CommsEventKey | ManagerEventKey, value: unknown) => void;
|
|
19
20
|
|
|
20
21
|
export class FrameComms {
|
|
21
22
|
private readonly wnd: Window;
|
|
22
23
|
private readonly registry = new Map<string, RegistryValue>();
|
|
23
|
-
private
|
|
24
|
+
private gc: ReturnType<typeof setInterval> | undefined;
|
|
24
25
|
// @ts-ignore
|
|
25
26
|
private readonly origin: string;
|
|
26
27
|
public readonly channelId: string;
|
|
27
28
|
private _ready = false;
|
|
28
29
|
private _listener: FrameCommsListener | undefined;
|
|
29
30
|
private listenerBuffer: [key: CommsEventKey, value: unknown][] = [];
|
|
31
|
+
private visibleTime = 0;
|
|
32
|
+
private visibleSince: number | undefined = document.hidden ? undefined : performance.now();
|
|
30
33
|
|
|
31
34
|
public set listener(listener: FrameCommsListener) {
|
|
32
35
|
if(this.listenerBuffer.length > 0)
|
|
@@ -46,28 +49,60 @@ export class FrameComms {
|
|
|
46
49
|
} catch (error) {
|
|
47
50
|
this.channelId = mid();
|
|
48
51
|
}
|
|
52
|
+
this.startGc();
|
|
53
|
+
window.addEventListener("message", this.handler);
|
|
54
|
+
this.send("_ping", undefined);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private startGc() {
|
|
58
|
+
if (this.gc !== undefined) return;
|
|
59
|
+
this.handleVisibility();
|
|
60
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
49
61
|
this.gc = setInterval(() => {
|
|
62
|
+
const now = this.visibleNow();
|
|
50
63
|
this.registry.forEach((v, k) => {
|
|
51
|
-
if (
|
|
64
|
+
if (now - v.time > REGISTRY_EXPIRY) {
|
|
52
65
|
console.warn(k, "event for", v.key, "was never handled!");
|
|
53
66
|
this.registry.delete(k);
|
|
54
67
|
}
|
|
55
68
|
});
|
|
56
|
-
},
|
|
57
|
-
window.addEventListener("message", this.handler);
|
|
58
|
-
this.send("_ping", undefined);
|
|
69
|
+
}, GC_INTERVAL);
|
|
59
70
|
}
|
|
60
71
|
|
|
72
|
+
private stopGc() {
|
|
73
|
+
this.visibleTime = this.visibleNow();
|
|
74
|
+
this.visibleSince = undefined;
|
|
75
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
76
|
+
clearInterval(this.gc);
|
|
77
|
+
this.gc = undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Frames ack some commands from requestAnimationFrame, which doesn't run while the page is hidden
|
|
81
|
+
private visibleNow() {
|
|
82
|
+
return this.visibleTime + (this.visibleSince === undefined ? 0 : performance.now() - this.visibleSince);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private handleVisibility() {
|
|
86
|
+
if (document.hidden) {
|
|
87
|
+
this.visibleTime = this.visibleNow();
|
|
88
|
+
this.visibleSince = undefined;
|
|
89
|
+
} else if (this.visibleSince === undefined) {
|
|
90
|
+
this.visibleSince = performance.now();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
private visibilityHandler = this.handleVisibility.bind(this);
|
|
94
|
+
|
|
61
95
|
public halt() {
|
|
62
96
|
this._ready = false;
|
|
63
97
|
window.removeEventListener("message", this.handler);
|
|
64
|
-
|
|
98
|
+
this.stopGc();
|
|
65
99
|
this._listener = undefined;
|
|
66
100
|
this.registry.clear();
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
public resume() {
|
|
70
104
|
window.addEventListener("message", this.handler);
|
|
105
|
+
this.startGc();
|
|
71
106
|
this._ready = true;
|
|
72
107
|
}
|
|
73
108
|
|
|
@@ -122,7 +157,7 @@ export class FrameComms {
|
|
|
122
157
|
this.registry.set(id, {
|
|
123
158
|
// Add callback to the registry
|
|
124
159
|
cb: callback,
|
|
125
|
-
time:
|
|
160
|
+
time: this.visibleNow(),
|
|
126
161
|
key,
|
|
127
162
|
});
|
|
128
163
|
this.wnd.postMessage(
|
|
@@ -4,6 +4,7 @@ import type { ReadiumWindow } from "@readium/navigator-html-injectables";
|
|
|
4
4
|
import { sML } from "@readium/helpers";
|
|
5
5
|
import type { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
6
6
|
import { KeyboardConditionBridge } from "../../peripherals/KeyboardConditionBridge.ts";
|
|
7
|
+
import { createSerializer } from "../../helpers/serialize.ts";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
export class FrameManager {
|
|
@@ -19,6 +20,8 @@ export class FrameManager {
|
|
|
19
20
|
private pendingUnwatchSelectors: string[] = [];
|
|
20
21
|
private conditionBridge?: KeyboardConditionBridge;
|
|
21
22
|
private currModules: ModuleName[] = [];
|
|
23
|
+
// Halting comms drops pending acks, so an operation must not start while another awaits one
|
|
24
|
+
private readonly serialize = createSerializer();
|
|
22
25
|
|
|
23
26
|
constructor(
|
|
24
27
|
source: string,
|
|
@@ -46,7 +49,7 @@ export class FrameManager {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
async load(modules: ModuleName[]): Promise<Window> {
|
|
49
|
-
return new Promise((res, rej) => {
|
|
52
|
+
return this.serialize(() => new Promise((res, rej) => {
|
|
50
53
|
if(this.loader) {
|
|
51
54
|
const wnd = this.frame.contentWindow!;
|
|
52
55
|
// Check if currently loaded modules are equal
|
|
@@ -72,7 +75,7 @@ export class FrameManager {
|
|
|
72
75
|
try { rej(err); } catch (error) {}
|
|
73
76
|
}
|
|
74
77
|
this.frame.contentWindow!.location.replace(this.source);
|
|
75
|
-
});
|
|
78
|
+
}));
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
private applyContentProtection() {
|
|
@@ -132,14 +135,20 @@ export class FrameManager {
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
async destroy() {
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
return this.serialize(async () => {
|
|
139
|
+
this.conditionBridge?.destroy();
|
|
140
|
+
await this.hideNow();
|
|
141
|
+
this.loader?.destroy();
|
|
142
|
+
this.frame.remove();
|
|
143
|
+
this.destroyed = true;
|
|
144
|
+
});
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
async hide(): Promise<void> {
|
|
148
|
+
return this.serialize(() => this.hideNow());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async hideNow(): Promise<void> {
|
|
143
152
|
if(this.destroyed) return;
|
|
144
153
|
this.frame.style.visibility = "hidden";
|
|
145
154
|
this.frame.ariaHidden = "true";
|
|
@@ -162,6 +171,10 @@ export class FrameManager {
|
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
async show(atProgress?: number): Promise<void> {
|
|
174
|
+
return this.serialize(() => this.showNow(atProgress));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async showNow(atProgress?: number): Promise<void> {
|
|
165
178
|
if(this.destroyed) throw Error("Trying to show frame when it doesn't exist");
|
|
166
179
|
if(!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
|
|
167
180
|
if(this.comms) this.comms.resume();
|
|
@@ -327,6 +327,11 @@ export class FramePoolManager {
|
|
|
327
327
|
return [this._currentFrame];
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/** The pooled frame of `href`, whether or not it's shown. */
|
|
331
|
+
_frameFor(href: string): FrameManager | undefined {
|
|
332
|
+
return this.pool.get(href);
|
|
333
|
+
}
|
|
334
|
+
|
|
330
335
|
get currentBounds(): DOMRect {
|
|
331
336
|
const ret = {
|
|
332
337
|
x: 0,
|
|
@@ -5,6 +5,7 @@ import { FXLPeripherals, isTypedOMSupported } from "./FXLPeripherals.ts";
|
|
|
5
5
|
import type { ReadiumWindow } from "@readium/navigator-html-injectables";
|
|
6
6
|
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
7
7
|
import { KeyboardConditionBridge } from "../../peripherals/KeyboardConditionBridge.ts";
|
|
8
|
+
import { createSerializer } from "../../helpers/serialize.ts";
|
|
8
9
|
|
|
9
10
|
export class FXLFrameManager {
|
|
10
11
|
private frame: HTMLIFrameElement;
|
|
@@ -26,6 +27,8 @@ export class FXLFrameManager {
|
|
|
26
27
|
private loadPromise: Promise<Window> | undefined;
|
|
27
28
|
private showPromise: Promise<void> | undefined;
|
|
28
29
|
private viewportSize: { width: number, height: number } | undefined = undefined;
|
|
30
|
+
// Halting comms drops pending acks, so an operation must not start while another awaits one
|
|
31
|
+
private readonly serialize = createSerializer();
|
|
29
32
|
|
|
30
33
|
constructor(
|
|
31
34
|
peripherals: FXLPeripherals,
|
|
@@ -65,6 +68,10 @@ export class FXLFrameManager {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
async load(modules: ModuleName[], source: string): Promise<Window> {
|
|
71
|
+
return this.serialize(() => this.loadNow(modules, source));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async loadNow(modules: ModuleName[], source: string): Promise<Window> {
|
|
68
75
|
if(!this.frameIsAppended) {
|
|
69
76
|
this.wrapper.appendChild(this.frame);
|
|
70
77
|
this.frameIsAppended = true;
|
|
@@ -194,13 +201,19 @@ export class FXLFrameManager {
|
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
async destroy() {
|
|
197
|
-
this.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
return this.serialize(async () => {
|
|
205
|
+
this.conditionBridge?.destroy();
|
|
206
|
+
await this.unfocusNow();
|
|
207
|
+
this.loader?.destroy();
|
|
208
|
+
this.wrapper.remove();
|
|
209
|
+
});
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
async unload() {
|
|
213
|
+
return this.serialize(() => this.unloadNow());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async unloadNow() {
|
|
204
217
|
if(!this.loaded) return;
|
|
205
218
|
this.deselect();
|
|
206
219
|
this.frame.style.visibility = "hidden";
|
|
@@ -232,8 +245,13 @@ export class FXLFrameManager {
|
|
|
232
245
|
}
|
|
233
246
|
|
|
234
247
|
async unfocus(): Promise<void> {
|
|
248
|
+
return this.serialize(() => this.unfocusNow());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async unfocusNow(): Promise<void> {
|
|
235
252
|
if(this.frame.parentElement) {
|
|
236
|
-
|
|
253
|
+
// A halted channel no longer receives acks
|
|
254
|
+
if(this.comms === undefined || !this.comms.ready) return;
|
|
237
255
|
// Return focus to the parent document so keyboard events aren't silently
|
|
238
256
|
// swallowed by an off-screen iframe whose comms channel has been halted.
|
|
239
257
|
this.frame.blur();
|
|
@@ -301,6 +319,10 @@ export class FXLFrameManager {
|
|
|
301
319
|
|
|
302
320
|
private cachedPage: Page | undefined = undefined;
|
|
303
321
|
async show(page: Page): Promise<void> {
|
|
322
|
+
return this.serialize(() => this.showNow(page));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async showNow(page: Page): Promise<void> {
|
|
304
326
|
if(!this.frame.parentElement) {
|
|
305
327
|
console.warn("Trying to show frame that is not attached to the DOM");
|
|
306
328
|
return;
|
|
@@ -332,12 +354,12 @@ export class FXLFrameManager {
|
|
|
332
354
|
}
|
|
333
355
|
|
|
334
356
|
async activate(): Promise<void> {
|
|
335
|
-
return new Promise<void>((res, _) => {
|
|
336
|
-
if(!this.comms) return res(); // TODO: investigate when this is the case
|
|
357
|
+
return this.serialize(() => new Promise<void>((res, _) => {
|
|
358
|
+
if(!this.comms?.ready) return res(); // TODO: investigate when this is the case
|
|
337
359
|
this.comms?.send("activate", undefined, () => {
|
|
338
360
|
res();
|
|
339
361
|
});
|
|
340
|
-
});
|
|
362
|
+
}));
|
|
341
363
|
}
|
|
342
364
|
|
|
343
365
|
get element() {
|