@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.
Files changed (64) hide show
  1. package/dist/divina/DivinaNavigator.js +1 -1
  2. package/dist/epub/EpubNavigator.js +1 -1
  3. package/dist/epub/frame/FrameComms.js +1 -1
  4. package/dist/epub/frame/FrameManager.js +1 -1
  5. package/dist/epub/frame/FramePoolManager.js +1 -1
  6. package/dist/epub/fxl/FXLFrameManager.js +1 -1
  7. package/dist/helpers/serialize.js +1 -0
  8. package/dist/index.js +1 -1
  9. package/dist/preferences/guards.js +1 -1
  10. package/dist/readaloud/GuidedNavigationProvider.js +1 -0
  11. package/dist/readaloud/ReadAloudNavigator.js +1 -0
  12. package/dist/readaloud/ReadingUnit.js +1 -0
  13. package/dist/readaloud/SpeechProgress.js +1 -0
  14. package/dist/readaloud/preferences/ReadAloudDefaults.js +1 -0
  15. package/dist/readaloud/preferences/ReadAloudPreferences.js +1 -0
  16. package/dist/readaloud/preferences/ReadAloudPreferencesEditor.js +1 -0
  17. package/dist/readaloud/preferences/ReadAloudSettings.js +1 -0
  18. package/dist/webpub/WebPubFrameManager.js +1 -1
  19. package/dist/webpub/WebPubFramePoolManager.js +1 -1
  20. package/dist/webpub/WebPubNavigator.js +1 -1
  21. package/package.json +8 -6
  22. package/src/divina/DivinaNavigator.ts +1 -0
  23. package/src/epub/EpubNavigator.ts +127 -21
  24. package/src/epub/frame/FrameComms.ts +42 -7
  25. package/src/epub/frame/FrameManager.ts +20 -7
  26. package/src/epub/frame/FramePoolManager.ts +5 -0
  27. package/src/epub/fxl/FXLFrameManager.ts +30 -8
  28. package/src/helpers/serialize.ts +9 -0
  29. package/src/index.ts +1 -0
  30. package/src/preferences/guards.ts +11 -0
  31. package/src/readaloud/GuidedNavigationProvider.ts +87 -0
  32. package/src/readaloud/ReadAloudNavigator.ts +949 -0
  33. package/src/readaloud/ReadingUnit.ts +154 -0
  34. package/src/readaloud/SpeechProgress.ts +60 -0
  35. package/src/readaloud/index.ts +17 -0
  36. package/src/readaloud/preferences/ReadAloudDefaults.ts +24 -0
  37. package/src/readaloud/preferences/ReadAloudPreferences.ts +104 -0
  38. package/src/readaloud/preferences/ReadAloudPreferencesEditor.ts +115 -0
  39. package/src/readaloud/preferences/ReadAloudSettings.ts +44 -0
  40. package/src/readaloud/preferences/index.ts +4 -0
  41. package/src/webpub/WebPubFrameManager.ts +24 -7
  42. package/src/webpub/WebPubFramePoolManager.ts +5 -0
  43. package/src/webpub/WebPubNavigator.ts +114 -10
  44. package/types/src/epub/EpubNavigator.d.ts +26 -1
  45. package/types/src/epub/frame/FrameComms.d.ts +8 -1
  46. package/types/src/epub/frame/FrameManager.d.ts +3 -0
  47. package/types/src/epub/frame/FramePoolManager.d.ts +2 -0
  48. package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -0
  49. package/types/src/helpers/serialize.d.ts +1 -0
  50. package/types/src/index.d.ts +1 -0
  51. package/types/src/preferences/guards.d.ts +1 -0
  52. package/types/src/readaloud/GuidedNavigationProvider.d.ts +28 -0
  53. package/types/src/readaloud/ReadAloudNavigator.d.ts +158 -0
  54. package/types/src/readaloud/ReadingUnit.d.ts +58 -0
  55. package/types/src/readaloud/SpeechProgress.d.ts +19 -0
  56. package/types/src/readaloud/index.d.ts +6 -0
  57. package/types/src/readaloud/preferences/ReadAloudDefaults.d.ts +11 -0
  58. package/types/src/readaloud/preferences/ReadAloudPreferences.d.ts +58 -0
  59. package/types/src/readaloud/preferences/ReadAloudPreferencesEditor.d.ts +37 -0
  60. package/types/src/readaloud/preferences/ReadAloudSettings.d.ts +26 -0
  61. package/types/src/readaloud/preferences/index.d.ts +4 -0
  62. package/types/src/webpub/WebPubFrameManager.d.ts +4 -0
  63. package/types/src/webpub/WebPubFramePoolManager.d.ts +2 -0
  64. 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.11.0",
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": ">=18"
44
+ "node": ">=24"
45
45
  },
46
46
  "dependencies": {
47
- "@readium/decorator": "1.2.4",
48
- "@readium/shared": "2.6.0",
49
- "@readium/helpers": "1.1.1",
50
- "@readium/navigator-html-injectables": "2.8.4"
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",
@@ -349,6 +349,7 @@ export class DivinaNavigator extends VisualNavigator implements Configurable<Con
349
349
  cssSelector: undefined,
350
350
  targetElement: "",
351
351
  targetFrameSrc: "",
352
+ onContent: false,
352
353
  x: e.x,
353
354
  y: e.y
354
355
  };
@@ -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
- if(this._layout === Layout.fixed
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 readonly gc: ReturnType<typeof setInterval>;
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 (performance.now() - v.time > REGISTRY_EXPIRY) {
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
- }, 5000);
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
- clearInterval(this.gc);
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: performance.now(),
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.conditionBridge?.destroy();
136
- await this.hide();
137
- this.loader?.destroy();
138
- this.frame.remove();
139
- this.destroyed = true;
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.conditionBridge?.destroy();
198
- await this.unfocus();
199
- this.loader?.destroy();
200
- this.wrapper.remove();
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
- if(this.comms === undefined) return;
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() {