@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/dist/index.js +2626 -2000
- package/dist/index.umd.cjs +85 -71
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/webpub/WebPubBlobBuilder.ts +145 -0
- package/src/webpub/WebPubFrameManager.ts +140 -0
- package/src/webpub/WebPubFramePoolManager.ts +174 -0
- package/src/webpub/WebPubNavigator.ts +417 -0
- package/src/webpub/index.ts +4 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/web/WebPubBlobBuilder.d.ts +10 -0
- package/types/src/web/WebPubFrameManager.d.ts +20 -0
- package/types/src/web/WebPubNavigator.d.ts +48 -0
- package/types/src/web/index.d.ts +3 -0
- package/types/src/webpub/WebPubBlobBuilder.d.ts +12 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +20 -0
- package/types/src/webpub/WebPubFramePoolManager.d.ts +16 -0
- package/types/src/webpub/WebPubNavigator.d.ts +50 -0
- package/types/src/webpub/index.d.ts +4 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -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
|
+
}
|