@readium/navigator 2.1.1 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './Navigator';
2
+ export * from './webpub';
2
3
  export * from './epub';
3
4
  export * from './audio';
4
5
  export * from './helpers';
@@ -0,0 +1,145 @@
1
+ import { Link, Publication } from "@readium/shared";
2
+
3
+ // Utilities (matching FrameBlobBuilder pattern)
4
+ const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
5
+ const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
6
+ const scriptify = (doc: Document, source: string) => {
7
+ const s = doc.createElement("script");
8
+ s.dataset.readium = "true";
9
+ s.src = source.startsWith("blob:") ? source : blobify(source, "text/javascript");
10
+ return s;
11
+ }
12
+
13
+ type CacheFunction = () => string;
14
+ const resourceBlobCache = new Map<string, string>();
15
+ const cached = (key: string, cacher: CacheFunction) => {
16
+ if (resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
17
+ const value = cacher();
18
+ resourceBlobCache.set(key, value);
19
+ return value;
20
+ };
21
+
22
+ const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selector-generator", () => blobify(
23
+ "!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports._readium_cssSelectorGenerator=e():t._readium_cssSelectorGenerator=e()}(self,(()=>(()=>{\"use strict\";var t,e,n={d:(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})}},o={};function r(t){return t&&t instanceof Element}function i(t=\"unknown problem\",...e){console.warn(`CssSelectorGenerator: ${t}`,...e)}n.r(o),n.d(o,{default:()=>z,getCssSelector:()=>U}),function(t){t.NONE=\"none\",t.DESCENDANT=\"descendant\",t.CHILD=\"child\"}(t||(t={})),function(t){t.id=\"id\",t.class=\"class\",t.tag=\"tag\",t.attribute=\"attribute\",t.nthchild=\"nthchild\",t.nthoftype=\"nthoftype\"}(e||(e={}));const c={selectors:[e.id,e.class,e.tag,e.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function u(t){return t instanceof RegExp}function s(t){return[\"string\",\"function\"].includes(typeof t)||u(t)}function l(t){return Array.isArray(t)?t.filter(s):[]}function a(t){const e=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(t){return t instanceof Node}(t)&&e.includes(t.nodeType)}function f(t,e){if(a(t))return t.contains(e)||i(\"element root mismatch\",\"Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended.\"),t;const n=e.getRootNode({composed:!1});return a(n)?(n!==document&&i(\"shadow root inferred\",\"You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended.\"),n):e.ownerDocument.querySelector(\":root\")}function d(t){return\"number\"==typeof t?t:Number.POSITIVE_INFINITY}function m(t=[]){const[e=[],...n]=t;return 0===n.length?e:n.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function p(t){return[].concat(...t)}function h(t){const e=t.map((t=>{if(u(t))return e=>t.test(e);if(\"function\"==typeof t)return e=>{const n=t(e);return\"boolean\"!=typeof n?(i(\"pattern matcher function invalid\",\"Provided pattern matching function does not return boolean. It\'s result will be ignored.\",t),!1):n};if(\"string\"==typeof t){const e=new RegExp(\"^\"+t.replace(\/[|\\\\{}()[\\]^$+?.]\/g,\"\\\\$&\").replace(\/\\*\/g,\".+\")+\"$\");return t=>e.test(t)}return i(\"pattern matcher invalid\",\"Pattern matching only accepts strings, regular expressions and\/or functions. This item is invalid and will be ignored.\",t),()=>!1}));return t=>e.some((e=>e(t)))}function g(t,e,n){const o=Array.from(f(n,t[0]).querySelectorAll(e));return o.length===t.length&&t.every((t=>o.includes(t)))}function y(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(\":root\")}(t);const n=[];let o=t;for(;r(o)&&o!==e;)n.push(o),o=o.parentElement;return n}function b(t,e){return m(t.map((t=>y(t,e))))}const N={[t.NONE]:{type:t.NONE,value:\"\"},[t.DESCENDANT]:{type:t.DESCENDANT,value:\" > \"},[t.CHILD]:{type:t.CHILD,value:\" \"}},S=new RegExp([\"^$\",\"\\\\s\"].join(\"|\")),E=new RegExp([\"^$\"].join(\"|\")),w=[e.nthoftype,e.tag,e.id,e.class,e.attribute,e.nthchild],v=h([\"class\",\"id\",\"ng-*\"]);function C({nodeName:t}){return`[${t}]`}function O({nodeName:t,nodeValue:e}){return`[${t}=\'${L(e)}\']`}function T(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const n=e.tagName.toLowerCase();return!([\"input\",\"option\"].includes(n)&&\"value\"===t||v(t))}(e,t)));return[...e.map(C),...e.map(O)]}function I(t){return(t.getAttribute(\"class\")||\"\").trim().split(\/\\s+\/).filter((t=>!E.test(t))).map((t=>`.${L(t)}`))}function x(t){const e=t.getAttribute(\"id\")||\"\",n=`#${L(e)}`,o=t.getRootNode({composed:!1});return!S.test(e)&&g([t],n,o)?[n]:[]}function j(t){const e=t.parentNode;if(e){const n=Array.from(e.childNodes).filter(r).indexOf(t);if(n>-1)return[`:nth-child(${n+1})`]}return[]}function A(t){return[L(t.tagName.toLowerCase())]}function D(t){const e=[...new Set(p(t.map(A)))];return 0===e.length||e.length>1?[]:[e[0]]}function $(t){const e=D([t])[0],n=t.parentElement;if(n){const o=Array.from(n.children).filter((t=>t.tagName.toLowerCase()===e)),r=o.indexOf(t);if(r>-1)return[`${e}:nth-of-type(${r+1})`]}return[]}function R(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){const n=[];let o=0,r=k(1);for(;r.length<=t.length&&o<e;)o+=1,n.push(r.map((e=>t[e]))),r=P(r,t.length-1);return n}function P(t=[],e=0){const n=t.length;if(0===n)return[];const o=[...t];o[n-1]+=1;for(let t=n-1;t>=0;t--)if(o[t]>e){if(0===t)return k(n+1);o[t-1]++,o[t]=o[t-1]+1}return o[n-1]>e?k(n+1):o}function k(t=1){return Array.from(Array(t).keys())}const _=\":\".charCodeAt(0).toString(16).toUpperCase(),M=\/[ !\"#$%&\'()\\[\\]{|}<>*+,.\/;=?@^`~\\\\]\/;function L(t=\"\"){var e,n;return null!==(n=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==n?n:function(t=\"\"){return t.split(\"\").map((t=>\":\"===t?`\\\\${_} `:M.test(t)?`\\\\${t}`:escape(t).replace(\/%\/g,\"\\\\\"))).join(\"\")}(t)}const q={tag:D,id:function(t){return 0===t.length||t.length>1?[]:x(t[0])},class:function(t){return m(t.map(I))},attribute:function(t){return m(t.map(T))},nthchild:function(t){return m(t.map(j))},nthoftype:function(t){return m(t.map($))}},F={tag:A,id:x,class:I,attribute:T,nthchild:j,nthoftype:$};function V(t){return t.includes(e.tag)||t.includes(e.nthoftype)?[...t]:[...t,e.tag]}function Y(t={}){const n=[...w];return t[e.tag]&&t[e.nthoftype]&&n.splice(n.indexOf(e.tag),1),n.map((e=>{return(o=t)[n=e]?o[n].join(\"\"):\"\";var n,o})).join(\"\")}function B(t,e,n=\"\",o){const r=function(t,e){return\"\"===e?t:function(t,e){return[...t.map((t=>e+\" \"+t)),...t.map((t=>e+\" > \"+t))]}(t,e)}(function(t,e,n){const o=function(t,e){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=e,c=h(n),u=h(o);return function(t){const{selectors:e,includeTag:n}=t,o=[].concat(e);return n&&!o.includes(\"tag\")&&o.push(\"tag\"),o}(e).reduce(((e,n)=>{const o=function(t,e){var n;return(null!==(n=q[e])&&void 0!==n?n:()=>[])(t)}(t,n),s=function(t=[],e,n){return t.filter((t=>n(t)||!e(t)))}(o,c,u),l=function(t=[],e){return t.sort(((t,n)=>{const o=e(t),r=e(n);return o&&!r?-1:!o&&r?1:0}))}(s,u);return e[n]=r?R(l,{maxResults:i}):l.map((t=>[t])),e}),{})}(t,n),r=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=t,i=n?R(e,{maxResults:r}):e.map((t=>[t]));return o?i.map(V):i}(e).map((e=>function(t,e){const n={};return t.forEach((t=>{const o=e[t];o.length>0&&(n[t]=o)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,n])=>{e=n.flatMap((n=>0===e.length?[{[t]:n}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:n})))))})),e}(n).map(Y)}(e,t))).filter((t=>t.length>0))}(o,n),i=p(r);return[...new Set(i)]}(t,o.root,o),n);for(const e of r)if(g(t,e,o.root))return e;return null}function G(t){return{value:t,include:!1}}function W({selectors:t,operator:n}){let o=[...w];t[e.tag]&&t[e.nthoftype]&&(o=o.filter((t=>t!==e.tag)));let r=\"\";return o.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(r+=t)}))})),n.value+r}function H(n){return[\":root\",...y(n).reverse().map((n=>{const o=function(e,n,o=t.NONE){const r={};return n.forEach((t=>{Reflect.set(r,t,function(t,e){return F[e](t)}(e,t).map(G))})),{element:e,operator:N[o],selectors:r}}(n,[e.nthchild],t.DESCENDANT);return o.selectors.nthchild.forEach((t=>{t.include=!0})),o})).map(W)].join(\"\")}function U(t,n={}){const o=function(t){const e=(Array.isArray(t)?t:[t]).filter(r);return[...new Set(e)]}(t),i=function(t,n={}){const o=Object.assign(Object.assign({},c),n);return{selectors:(r=o.selectors,Array.isArray(r)?r.filter((t=>{return n=e,o=t,Object.values(n).includes(o);var n,o})):[]),whitelist:l(o.whitelist),blacklist:l(o.blacklist),root:f(o.root,t),combineWithinSelector:!!o.combineWithinSelector,combineBetweenSelectors:!!o.combineBetweenSelectors,includeTag:!!o.includeTag,maxCombinations:d(o.maxCombinations),maxCandidates:d(o.maxCandidates)};var r}(o[0],n);let u=\"\",s=i.root;function a(){return function(t,e,n=\"\",o){if(0===t.length)return null;const r=[t.length>1?t:[],...b(t,e).map((t=>[t]))];for(const t of r){const e=B(t,0,n,o);if(e)return{foundElements:t,selector:e}}return null}(o,s,u,i)}let m=a();for(;m;){const{foundElements:t,selector:e}=m;if(g(o,e,i.root))return e;s=t[0],u=e,m=a()}return o.length>1?o.map((t=>U(t,i))).join(\", \"):function(t){return t.map(H).join(\", \")}(o)}const z=U;return o})()));",
24
+ "text/javascript"
25
+ )));
26
+
27
+ const readiumPropertiesScript = `
28
+ window._readium_blockedEvents = [];
29
+ window._readium_blockEvents = false; // WebPub doesn't need event blocking
30
+ window._readium_eventBlocker = null;
31
+ `;
32
+
33
+ const rBefore = (doc: Document) => scriptify(doc, cached("webpub-js-before", () => blobify(stripJS(readiumPropertiesScript), "text/javascript")));
34
+ const rAfter = (doc: Document) => scriptify(doc, cached("webpub-js-after", () => blobify(stripJS(`
35
+ if(window.onload) window.onload = new Proxy(window.onload, {
36
+ apply: function(target, receiver, args) {
37
+ if(!window._readium_blockEvents) {
38
+ Reflect.apply(target, receiver, args);
39
+ return;
40
+ }
41
+ _readium_blockedEvents.push([0, target, receiver, args]);
42
+ }
43
+ });`), "text/javascript")));
44
+
45
+ export class WebPubBlobBuilder {
46
+ private readonly item: Link;
47
+ private readonly burl: string;
48
+ private readonly pub: Publication;
49
+
50
+ constructor(pub: Publication, baseURL: string, item: Link) {
51
+ this.pub = pub;
52
+ this.item = item;
53
+ this.burl = item.toURL(baseURL) || "";
54
+ }
55
+
56
+ public async build(): Promise<string> {
57
+ if (!this.item.mediaType.isHTML) {
58
+ throw new Error(`Unsupported media type for WebPub: ${this.item.mediaType.string}`);
59
+ }
60
+
61
+ return await this.buildHtmlFrame();
62
+ }
63
+
64
+ private async buildHtmlFrame(): Promise<string> {
65
+ // Load the HTML resource
66
+ const txt = await this.pub.get(this.item).readAsString();
67
+ if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
68
+ const doc = new DOMParser().parseFromString(
69
+ txt,
70
+ this.item.mediaType.string as DOMParserSupportedType
71
+ );
72
+ const perror = doc.querySelector("parsererror");
73
+ if(perror) {
74
+ const details = perror.querySelector("div");
75
+ throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
76
+ }
77
+ return this.finalizeDOM(doc, this.burl, this.item.mediaType, txt);
78
+ }
79
+
80
+ private hasExecutable(doc: Document): boolean {
81
+ return (
82
+ !!doc.querySelector("script") ||
83
+ !!doc.querySelector("body[onload]:not(body[onload=''])")
84
+ );
85
+ }
86
+
87
+ private finalizeDOM(doc: Document, base: string | undefined, mediaType: any, txt?: string): string {
88
+ if(!doc) return "";
89
+
90
+ doc.body.querySelectorAll("img").forEach((img) => {
91
+ img.setAttribute("fetchpriority", "high");
92
+ });
93
+
94
+ if(base !== undefined) {
95
+ const b = doc.createElement("base");
96
+ b.href = base;
97
+ b.dataset.readium = "true";
98
+ doc.head.firstChild!.before(b);
99
+ }
100
+
101
+
102
+ const hasExecutable = this.hasExecutable(doc);
103
+ if(hasExecutable) doc.head.firstChild!.before(rBefore(doc));
104
+ doc.head.firstChild!.before(cssSelectorGenerator(doc));
105
+ if(hasExecutable) doc.head.appendChild(rAfter(doc));
106
+
107
+ // Serialize properly based on content type
108
+ let serializedContent: string;
109
+
110
+ if (mediaType.string === "application/xhtml+xml") {
111
+ // XHTML: Use XMLSerializer for proper XML formatting
112
+ serializedContent = new XMLSerializer().serializeToString(doc);
113
+ } else {
114
+ // HTML: Use custom HTML serialization to preserve HTML formatting
115
+ serializedContent = this.serializeAsHTML(doc, txt || "");
116
+ }
117
+
118
+ // Make blob from doc
119
+ return URL.createObjectURL(
120
+ new Blob([serializedContent], {
121
+ type: mediaType.isHTML
122
+ ? mediaType.string
123
+ : "application/xhtml+xml",
124
+ })
125
+ );
126
+ }
127
+
128
+ private serializeAsHTML(doc: Document, txt: string): string {
129
+ // For HTML content, try to preserve the original HTML structure
130
+ // while injecting our scripts
131
+
132
+ // Extract the original DOCTYPE if present
133
+ const doctypeMatch = txt.match(/<!DOCTYPE[^>]*>/i);
134
+ const doctype = doctypeMatch ? doctypeMatch[0] + "\n" : "";
135
+
136
+ // Get the HTML element and serialize it as HTML
137
+ const htmlElement = doc.documentElement;
138
+ let htmlContent = htmlElement.outerHTML;
139
+
140
+ // Try to preserve the original HTML structure
141
+ // This is a best-effort approach since there's no perfect HTML serializer
142
+
143
+ return doctype + htmlContent;
144
+ }
145
+ }
@@ -0,0 +1,140 @@
1
+ import { Loader, ModuleName } from "@readium/navigator-html-injectables";
2
+ import { FrameComms } from "../epub/frame/FrameComms";
3
+ import { ReadiumWindow } from "../../../navigator-html-injectables/types/src/helpers/dom";
4
+ import { sML } from "../helpers";
5
+
6
+ export class WebPubFrameManager {
7
+ private frame: HTMLIFrameElement;
8
+ private loader: Loader | undefined;
9
+ public readonly source: string;
10
+ private comms: FrameComms | undefined;
11
+ private destroyed: boolean = false;
12
+
13
+ private currModules: ModuleName[] = [];
14
+
15
+ constructor(source: string) {
16
+ this.frame = document.createElement("iframe");
17
+ this.frame.classList.add("readium-navigator-iframe");
18
+ this.frame.style.visibility = "hidden";
19
+ this.frame.style.setProperty("aria-hidden", "true");
20
+ this.frame.style.opacity = "0";
21
+ this.frame.style.position = "absolute";
22
+ this.frame.style.pointerEvents = "none";
23
+ this.frame.style.transition = "visibility 0s, opacity 0.1s linear";
24
+ // Protect against background color bleeding
25
+ this.frame.style.backgroundColor = "#FFFFFF";
26
+ this.source = source;
27
+ }
28
+
29
+ async load(modules: ModuleName[] = []): Promise<Window> {
30
+ return new Promise((res, rej) => {
31
+ if(this.loader) {
32
+ const wnd = this.frame.contentWindow!;
33
+ // Check if currently loaded modules are equal
34
+ if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
35
+ try { res(wnd); } catch (error) {};
36
+ return;
37
+ }
38
+ this.comms?.halt();
39
+ this.loader.destroy();
40
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
41
+ this.currModules = modules;
42
+ this.comms = undefined;
43
+ try { res(wnd); } catch (error) {}
44
+ return;
45
+ }
46
+ this.frame.onload = () => {
47
+ const wnd = this.frame.contentWindow!;
48
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
49
+ this.currModules = modules;
50
+ try { res(wnd); } catch (error) {}
51
+ };
52
+ this.frame.onerror = (err) => {
53
+ try { rej(err); } catch (error) {}
54
+ }
55
+ this.frame.contentWindow!.location.replace(this.source);
56
+ });
57
+ }
58
+
59
+ async destroy() {
60
+ await this.hide();
61
+ this.loader?.destroy();
62
+ this.frame.remove();
63
+ this.destroyed = true;
64
+ }
65
+
66
+ async hide(): Promise<void> {
67
+ if(this.destroyed) return;
68
+ this.frame.style.visibility = "hidden";
69
+ this.frame.style.setProperty("aria-hidden", "true");
70
+ this.frame.style.opacity = "0";
71
+ this.frame.style.pointerEvents = "none";
72
+
73
+ if(this.frame.parentElement) {
74
+ if(this.comms === undefined || !this.comms.ready) return;
75
+ return new Promise((res, _) => {
76
+ this.comms?.send("unfocus", undefined, (_: boolean) => {
77
+ this.comms?.halt();
78
+ res();
79
+ });
80
+ });
81
+ } else {
82
+ this.comms?.halt();
83
+ }
84
+ }
85
+
86
+ async show(atProgress?: number): Promise<void> {
87
+ if (this.destroyed) throw Error("Trying to show frame when it doesn't exist");
88
+ if (!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
89
+ if (this.comms) this.comms.resume();
90
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
91
+
92
+ return new Promise((res, _) => {
93
+ this.comms?.send("activate", undefined, () => {
94
+ this.comms?.send("focus", undefined, () => {
95
+ const remove = () => {
96
+ this.frame.style.removeProperty("visibility");
97
+ this.frame.style.removeProperty("aria-hidden");
98
+ this.frame.style.removeProperty("opacity");
99
+ this.frame.style.removeProperty("pointer-events");
100
+
101
+ if (sML.UA.WebKit) {
102
+ this.comms?.send("force_webkit_recalc", undefined);
103
+ }
104
+
105
+ res();
106
+ }
107
+
108
+ if (atProgress !== undefined) {
109
+ this.comms?.send("go_progression", atProgress, remove);
110
+ } else {
111
+ remove();
112
+ }
113
+ });
114
+ });
115
+ });
116
+ }
117
+
118
+ get iframe() {
119
+ if(this.destroyed) throw Error("Trying to use frame when it doesn't exist");
120
+ return this.frame;
121
+ }
122
+
123
+ get realSize() {
124
+ if(this.destroyed) throw Error("Trying to use frame client rect when it doesn't exist");
125
+ return this.frame.getBoundingClientRect();
126
+ }
127
+
128
+ get window() {
129
+ if(this.destroyed || !this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
130
+ return this.frame.contentWindow;
131
+ }
132
+
133
+ get msg() {
134
+ return this.comms;
135
+ }
136
+
137
+ get ldr() {
138
+ return this.loader;
139
+ }
140
+ }
@@ -0,0 +1,174 @@
1
+ import { ModuleName } from "@readium/navigator-html-injectables";
2
+ import { Locator, Publication } from "@readium/shared";
3
+ import { WebPubBlobBuilder } from "./WebPubBlobBuilder";
4
+ import { WebPubFrameManager } from "./WebPubFrameManager";
5
+
6
+ export class WebPubFramePoolManager {
7
+ private readonly container: HTMLElement;
8
+ private _currentFrame: WebPubFrameManager | undefined;
9
+ private readonly pool: Map<string, WebPubFrameManager> = new Map();
10
+ private readonly blobs: Map<string, string> = new Map();
11
+ private readonly inprogress: Map<string, Promise<void>> = new Map();
12
+ private currentBaseURL: string | undefined;
13
+
14
+ constructor(container: HTMLElement) {
15
+ this.container = container;
16
+ }
17
+
18
+ async destroy() {
19
+ // Wait for all in-progress loads to complete
20
+ let iit = this.inprogress.values();
21
+ let inp = iit.next();
22
+ const inprogressPromises: Promise<void>[] = [];
23
+ while(inp.value) {
24
+ inprogressPromises.push(inp.value);
25
+ inp = iit.next();
26
+ }
27
+ if(inprogressPromises.length > 0) {
28
+ await Promise.allSettled(inprogressPromises);
29
+ }
30
+ this.inprogress.clear();
31
+
32
+ // Destroy all frames
33
+ let fit = this.pool.values();
34
+ let frm = fit.next();
35
+ while(frm.value) {
36
+ await (frm.value as WebPubFrameManager).destroy();
37
+ frm = fit.next();
38
+ }
39
+ this.pool.clear();
40
+
41
+ // Revoke all blobs
42
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
43
+ this.blobs.clear();
44
+
45
+ // Empty container of elements
46
+ this.container.childNodes.forEach(v => {
47
+ if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
48
+ })
49
+ }
50
+
51
+ async update(pub: Publication, locator: Locator, modules: ModuleName[]) {
52
+ const readingOrder = pub.readingOrder.items;
53
+ let i = readingOrder.findIndex(l => l.href === locator.href);
54
+ if(i < 0) throw Error(`Locator not found in reading order: ${locator.href}`);
55
+ const newHref = readingOrder[i].href;
56
+
57
+ if(this.inprogress.has(newHref))
58
+ await this.inprogress.get(newHref);
59
+
60
+ const progressPromise = new Promise<void>(async (resolve, reject) => {
61
+ const disposal: string[] = [];
62
+ const creation: string[] = [];
63
+ pub.readingOrder.items.forEach((l, j) => {
64
+ // Dispose everything except current, previous, and next
65
+ if(j !== i && j !== i - 1 && j !== i + 1) {
66
+ if(!disposal.includes(l.href)) disposal.push(l.href);
67
+ }
68
+
69
+ // CURRENT FRAME: always create the frame we're navigating to
70
+ if(j === i) {
71
+ if(!creation.includes(l.href)) creation.push(l.href);
72
+ }
73
+
74
+ // PREVIOUS/NEXT FRAMES: create adjacent chapters for smooth navigation
75
+ // if((j === i - 1 || j === i + 1) && j >= 0 && j < pub.readingOrder.items.length) {
76
+ // if(!creation.includes(l.href)) creation.push(l.href);
77
+ // }
78
+ });
79
+ disposal.forEach(async href => {
80
+ if(creation.includes(href)) return;
81
+ if(!this.pool.has(href)) return;
82
+ await this.pool.get(href)?.destroy();
83
+ this.pool.delete(href);
84
+ });
85
+
86
+ if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
87
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
88
+ this.blobs.clear();
89
+ }
90
+ this.currentBaseURL = pub.baseURL;
91
+
92
+ const creator = async (href: string) => {
93
+ if(this.pool.has(href)) {
94
+ const fm = this.pool.get(href)!;
95
+ if(!this.blobs.has(href)) {
96
+ await fm.destroy();
97
+ this.pool.delete(href);
98
+ } else {
99
+ await fm.load(modules);
100
+ return;
101
+ }
102
+ }
103
+ const itm = pub.readingOrder.findWithHref(href);
104
+ if(!itm) return;
105
+ if(!this.blobs.has(href)) {
106
+ const blobBuilder = new WebPubBlobBuilder(pub, this.currentBaseURL || "", itm);
107
+ const blobURL = await blobBuilder.build();
108
+ this.blobs.set(href, blobURL);
109
+ }
110
+
111
+ const fm = new WebPubFrameManager(this.blobs.get(href)!);
112
+ if(href !== newHref) await fm.hide();
113
+ this.container.appendChild(fm.iframe);
114
+ await fm.load(modules);
115
+ this.pool.set(href, fm);
116
+ }
117
+ try {
118
+ await Promise.all(creation.map(href => creator(href)));
119
+ } catch (error) {
120
+ reject(error);
121
+ }
122
+
123
+ const newFrame = this.pool.get(newHref)!;
124
+ if(newFrame?.source !== this._currentFrame?.source) {
125
+ await this._currentFrame?.hide();
126
+ if(newFrame)
127
+ await newFrame.load(modules);
128
+
129
+ if(newFrame)
130
+ await newFrame.show(locator.locations.progression);
131
+
132
+ this._currentFrame = newFrame;
133
+ }
134
+ resolve();
135
+ });
136
+
137
+ this.inprogress.set(newHref, progressPromise);
138
+ await progressPromise;
139
+ this.inprogress.delete(newHref);
140
+ }
141
+
142
+ get currentFrames(): (WebPubFrameManager | undefined)[] {
143
+ return [this._currentFrame];
144
+ }
145
+
146
+ get currentBounds(): DOMRect {
147
+ const ret = {
148
+ x: 0,
149
+ y: 0,
150
+ width: 0,
151
+ height: 0,
152
+ top: 0,
153
+ right: 0,
154
+ bottom: 0,
155
+ left: 0,
156
+ toJSON() {
157
+ return this;
158
+ },
159
+ };
160
+ this.currentFrames.forEach(f => {
161
+ if(!f) return;
162
+ const b = f.realSize;
163
+ ret.x = Math.min(ret.x, b.x);
164
+ ret.y = Math.min(ret.y, b.y);
165
+ ret.width += b.width;
166
+ ret.height = Math.max(ret.height, b.height);
167
+ ret.top = Math.min(ret.top, b.top);
168
+ ret.right = Math.min(ret.right, b.right);
169
+ ret.bottom = Math.min(ret.bottom, b.bottom);
170
+ ret.left = Math.min(ret.left, b.left);
171
+ });
172
+ return ret as DOMRect;
173
+ }
174
+ }