@readium/navigator 1.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/LICENSE +28 -0
- package/README.MD +11 -0
- package/dist/assets/AccessibleDfA.otf +0 -0
- package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
- package/dist/index.js +6263 -0
- package/dist/index.umd.cjs +107 -0
- package/package.json +65 -0
- package/src/Navigator.ts +66 -0
- package/src/audio/engine/AudioEngine.ts +136 -0
- package/src/audio/engine/WebAudioEngine.ts +286 -0
- package/src/audio/engine/index.ts +2 -0
- package/src/audio/index.ts +1 -0
- package/src/epub/EpubNavigator.ts +507 -0
- package/src/epub/frame/FrameBlobBuilder.ts +211 -0
- package/src/epub/frame/FrameComms.ts +142 -0
- package/src/epub/frame/FrameManager.ts +134 -0
- package/src/epub/frame/FramePoolManager.ts +179 -0
- package/src/epub/frame/index.ts +3 -0
- package/src/epub/fxl/FXLCoordinator.ts +152 -0
- package/src/epub/fxl/FXLFrameManager.ts +286 -0
- package/src/epub/fxl/FXLFramePoolManager.ts +632 -0
- package/src/epub/fxl/FXLPeripherals.ts +587 -0
- package/src/epub/fxl/FXLPeripheralsDebug.ts +46 -0
- package/src/epub/fxl/FXLSpreader.ts +95 -0
- package/src/epub/fxl/index.ts +5 -0
- package/src/epub/index.ts +3 -0
- package/src/helpers/sML.ts +120 -0
- package/src/index.ts +3 -0
- package/types/src/Navigator.d.ts +41 -0
- package/types/src/audio/engine/AudioEngine.d.ts +114 -0
- package/types/src/audio/engine/WebAudioEngine.d.ts +107 -0
- package/types/src/audio/engine/index.d.ts +2 -0
- package/types/src/audio/index.d.ts +1 -0
- package/types/src/epub/EpubNavigator.d.ts +66 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +13 -0
- package/types/src/epub/frame/FrameComms.d.ts +26 -0
- package/types/src/epub/frame/FrameManager.d.ts +21 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +17 -0
- package/types/src/epub/frame/index.d.ts +3 -0
- package/types/src/epub/fxl/FXLCoordinator.d.ts +37 -0
- package/types/src/epub/fxl/FXLFrameManager.d.ts +41 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +93 -0
- package/types/src/epub/fxl/FXLPeripherals.d.ts +97 -0
- package/types/src/epub/fxl/FXLPeripheralsDebug.d.ts +13 -0
- package/types/src/epub/fxl/FXLSpreader.d.ts +12 -0
- package/types/src/epub/fxl/index.d.ts +5 -0
- package/types/src/epub/index.d.ts +3 -0
- package/types/src/helpers/sML.d.ts +51 -0
- package/types/src/index.d.ts +3 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { MediaType } from "@readium/shared";
|
|
2
|
+
import { Link, Publication } from "@readium/shared";
|
|
3
|
+
|
|
4
|
+
// Readium CSS imports
|
|
5
|
+
// The "?inline" query is to prevent some bundlers from injecting these into the page (e.g. vite)
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
import readiumCSSAfter from "readium-css/css/dist/ReadiumCSS-after.css?inline";
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import readiumCSSBefore from "readium-css/css/dist/ReadiumCSS-before.css?inline";
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
import readiumCSSDefault from "readium-css/css/dist/ReadiumCSS-default.css?inline";
|
|
12
|
+
|
|
13
|
+
// Utilities
|
|
14
|
+
const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
|
|
15
|
+
const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
|
|
16
|
+
const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, '').replace(/ {2,}/g, ' ')
|
|
17
|
+
// Fully resolve absolute local URLs created by bundlers since it's going into a blob
|
|
18
|
+
.replace(/url\((?!(https?:)?\/\/)("?)\/([^\)]+)/g, `url($2${window.location.origin}/$3`);
|
|
19
|
+
const scriptify = (doc: Document, source: string) => {
|
|
20
|
+
const s = doc.createElement("script");
|
|
21
|
+
s.dataset.readium = "true";
|
|
22
|
+
s.src = source.startsWith("blob:") ? source : blobify(source, "text/javascript");
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
const styleify = (doc: Document, source: string) => {
|
|
26
|
+
const s = doc.createElement("link");
|
|
27
|
+
s.dataset.readium = "true";
|
|
28
|
+
s.rel = "stylesheet";
|
|
29
|
+
s.type = "text/css";
|
|
30
|
+
s.href = source.startsWith("blob:") ? source : blobify(source, "text/css");
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type CacheFunction = () => string;
|
|
35
|
+
const resourceBlobCache = new Map<string, string>();
|
|
36
|
+
const cached = (key: string, cacher: CacheFunction) => {
|
|
37
|
+
if(resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
|
|
38
|
+
const value = cacher();
|
|
39
|
+
resourceBlobCache.set(key, value);
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// https://unpkg.com/css-selector-generator@3.6.4/build/index.js
|
|
44
|
+
// CssSelectorGenerator --> _readium_cssSelectorGenerator
|
|
45
|
+
// This has to be injected because you need to be in the iframe's context for it to work properly
|
|
46
|
+
const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selector-generator", () => blobify(
|
|
47
|
+
"!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})()));",
|
|
48
|
+
"text/javascript"
|
|
49
|
+
)));
|
|
50
|
+
|
|
51
|
+
// Note: we aren't blocking some of the events right now to try and be as nonintrusive as possible.
|
|
52
|
+
// For a more comprehensive implementation, see https://github.com/hackademix/noscript/blob/3a83c0e4a506f175e38b0342dad50cdca3eae836/src/content/syncFetchPolicy.js#L142
|
|
53
|
+
const rBefore = (doc: Document) => scriptify(doc, cached("JS-Before", () => blobify(stripJS(`
|
|
54
|
+
window._readium_blockedEvents = [];
|
|
55
|
+
window._readium_blockEvents = true;
|
|
56
|
+
window._readium_eventBlocker = (e) => {
|
|
57
|
+
if(!window._readium_blockEvents) return;
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
e.stopImmediatePropagation();
|
|
60
|
+
_readium_blockedEvents.push([
|
|
61
|
+
1, e, e.currentTarget || e.target
|
|
62
|
+
]);
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener("DOMContentLoaded", window._readium_eventBlocker, true);
|
|
65
|
+
window.addEventListener("load", window._readium_eventBlocker, true);`
|
|
66
|
+
), "text/javascript")));
|
|
67
|
+
const rAfter = (doc: Document) => scriptify(doc, cached("JS-After", () => blobify(stripJS(`
|
|
68
|
+
if(window.onload) window.onload = new Proxy(window.onload, {
|
|
69
|
+
apply: function(target, receiver, args) {
|
|
70
|
+
if(!window._readium_blockEvents) {
|
|
71
|
+
Reflect.apply(target, receiver, args);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
_readium_blockedEvents.push([
|
|
75
|
+
0, target, receiver, args
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
});`
|
|
79
|
+
), "text/javascript")));
|
|
80
|
+
|
|
81
|
+
export default class FrameBlobBuider {
|
|
82
|
+
private readonly item: Link;
|
|
83
|
+
private readonly burl: string;
|
|
84
|
+
private readonly pub: Publication;
|
|
85
|
+
|
|
86
|
+
constructor(pub: Publication, baseURL: string, item: Link) {
|
|
87
|
+
this.pub = pub;
|
|
88
|
+
this.item = item;
|
|
89
|
+
this.burl = item.toURL(baseURL) || "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public async build(fxl = false): Promise<string> {
|
|
93
|
+
if(!this.item.mediaType.isHTML) {
|
|
94
|
+
if(this.item.mediaType.isBitmap) {
|
|
95
|
+
return this.buildImageFrame();
|
|
96
|
+
} else
|
|
97
|
+
throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
|
|
98
|
+
} else {
|
|
99
|
+
return await this.buildHtmlFrame(fxl);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async buildHtmlFrame(fxl = false): Promise<string> {
|
|
104
|
+
// Load the HTML resource
|
|
105
|
+
const txt = await this.pub.get(this.item).readAsString();
|
|
106
|
+
if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
|
|
107
|
+
const doc = new DOMParser().parseFromString(
|
|
108
|
+
txt,
|
|
109
|
+
this.item.mediaType.string as DOMParserSupportedType
|
|
110
|
+
);
|
|
111
|
+
const perror = doc.querySelector("parsererror");
|
|
112
|
+
if(perror) {
|
|
113
|
+
const details = perror.querySelector("div");
|
|
114
|
+
throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
|
|
115
|
+
}
|
|
116
|
+
return this.finalizeDOM(doc, this.burl, this.item.mediaType, fxl);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private buildImageFrame(): string {
|
|
120
|
+
// Rudimentary image display
|
|
121
|
+
const doc = document.implementation.createHTMLDocument(this.item.title || this.item.href);
|
|
122
|
+
const simg = document.createElement("img");
|
|
123
|
+
simg.src = this.burl || "";
|
|
124
|
+
simg.alt = this.item.title || "";
|
|
125
|
+
simg.decoding = "async";
|
|
126
|
+
doc.body.appendChild(simg);
|
|
127
|
+
return this.finalizeDOM(doc, this.burl, this.item.mediaType, true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Has JS that may have side-effects when the document is loaded, without any user interaction
|
|
131
|
+
private hasExecutable(doc: Document): boolean {
|
|
132
|
+
// This is not a 100% comprehensive check of all possibilities for JS execution,
|
|
133
|
+
// but it covers what the prevention scripts cover. Other possibilities include:
|
|
134
|
+
// - <iframe> src
|
|
135
|
+
// - <img> with onload/onerror
|
|
136
|
+
// - <meta http-equiv="refresh" content="xxx">
|
|
137
|
+
return (
|
|
138
|
+
!!doc.querySelector("script") || // Any <script> elements
|
|
139
|
+
!!doc.querySelector("body[onload]:not(body[onload=''])") // <body> that executes JS on load
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private hasStyle(doc: Document): boolean {
|
|
144
|
+
if(
|
|
145
|
+
doc.querySelector("link[rel='stylesheet']") || // Any CSS link
|
|
146
|
+
doc.querySelector("style") || // Any <style> element
|
|
147
|
+
doc.querySelector("[style]:not([style=''])") // Any element with style attribute set
|
|
148
|
+
) return true;
|
|
149
|
+
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private finalizeDOM(doc: Document, base: string | undefined, mediaType: MediaType, fxl = false): string {
|
|
154
|
+
if(!doc) return "";
|
|
155
|
+
|
|
156
|
+
// Inject styles
|
|
157
|
+
if(!fxl) {
|
|
158
|
+
// Readium CSS Before
|
|
159
|
+
const rcssBefore = styleify(doc, cached("ReadiumCSS-before", () => blobify(stripCSS(readiumCSSBefore), "text/css")));
|
|
160
|
+
doc.head.firstChild ? doc.head.firstChild.before(rcssBefore) : doc.head.appendChild(rcssBefore);
|
|
161
|
+
|
|
162
|
+
// Patch
|
|
163
|
+
const patch = doc.createElement("style");
|
|
164
|
+
patch.dataset.readium = "true";
|
|
165
|
+
patch.innerHTML = `audio[controls] { width: revert; height: revert; }`; // https://github.com/readium/readium-css/issues/94
|
|
166
|
+
rcssBefore.after(patch);
|
|
167
|
+
|
|
168
|
+
// Readium CSS defaults
|
|
169
|
+
if(!this.hasStyle(doc))
|
|
170
|
+
rcssBefore.after(styleify(doc, cached("ReadiumCSS-default", () => blobify(stripCSS(readiumCSSDefault), "text/css"))))
|
|
171
|
+
|
|
172
|
+
// Readium CSS After
|
|
173
|
+
doc.head.appendChild(styleify(doc, cached("ReadiumCSS-after", () => blobify(stripCSS(readiumCSSAfter), "text/css"))));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Set all <img> elements to high priority
|
|
177
|
+
// From what I understand, browser heuristics
|
|
178
|
+
// de-prioritize <iframe> resources. This causes the <img>
|
|
179
|
+
// elements to be loaded in sequence, which in documents
|
|
180
|
+
// with many images causes significant impact to rendering
|
|
181
|
+
// speed. When you increase the priority, the <img> data is
|
|
182
|
+
// loaded in parallel, greatly increasing overall speed.
|
|
183
|
+
doc.body.querySelectorAll("img").forEach((img) => {
|
|
184
|
+
img.setAttribute("fetchpriority", "high");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if(base !== undefined) {
|
|
188
|
+
// Set all URL bases. Very convenient!
|
|
189
|
+
const b = doc.createElement("base");
|
|
190
|
+
b.href = base;
|
|
191
|
+
b.dataset.readium = "true";
|
|
192
|
+
doc.head.firstChild!.before(b);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Inject script to prevent in-publication scripts from executing until we want them to
|
|
196
|
+
const hasExecutable = this.hasExecutable(doc);
|
|
197
|
+
if(hasExecutable) doc.head.firstChild!.before(rBefore(doc));
|
|
198
|
+
doc.head.firstChild!.before(cssSelectorGenerator(doc)); // CSS selector utility
|
|
199
|
+
if(hasExecutable) doc.head.appendChild(rAfter(doc)); // Another execution prevention script
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// Make blob from doc
|
|
203
|
+
return URL.createObjectURL(
|
|
204
|
+
new Blob([new XMLSerializer().serializeToString(doc)], {
|
|
205
|
+
type: mediaType.isHTML
|
|
206
|
+
? mediaType.string
|
|
207
|
+
: "application/xhtml+xml", // Fallback to XHTML
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMMS_VERSION,
|
|
3
|
+
CommsMessage,
|
|
4
|
+
CommsCommandKey,
|
|
5
|
+
CommsAck,
|
|
6
|
+
mid,
|
|
7
|
+
CommsEventKey,
|
|
8
|
+
} from "@readium/navigator-html-injectables";
|
|
9
|
+
import { ManagerEventKey } from "../EpubNavigator";
|
|
10
|
+
|
|
11
|
+
interface RegistryValue {
|
|
12
|
+
time: number;
|
|
13
|
+
key: CommsCommandKey;
|
|
14
|
+
cb: CommsAck;
|
|
15
|
+
}
|
|
16
|
+
const REGISTRY_EXPIRY = 10000; // 10 seconds max
|
|
17
|
+
|
|
18
|
+
export type FrameCommsListener = (key: CommsEventKey | ManagerEventKey, value: unknown) => void;
|
|
19
|
+
|
|
20
|
+
export class FrameComms {
|
|
21
|
+
private readonly wnd: Window;
|
|
22
|
+
private readonly registry = new Map<string, RegistryValue>();
|
|
23
|
+
private readonly gc: ReturnType<typeof setInterval>;
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
private readonly origin: string;
|
|
26
|
+
public readonly channelId: string;
|
|
27
|
+
private _ready = false;
|
|
28
|
+
private _listener: FrameCommsListener | undefined;
|
|
29
|
+
private listenerBuffer: [key: CommsEventKey, value: unknown][] = [];
|
|
30
|
+
|
|
31
|
+
public set listener(listener: FrameCommsListener) {
|
|
32
|
+
if(this.listenerBuffer.length > 0)
|
|
33
|
+
this.listenerBuffer.forEach(msg => listener(msg[0], msg[1]));
|
|
34
|
+
this.listenerBuffer = [];
|
|
35
|
+
this._listener = listener;
|
|
36
|
+
}
|
|
37
|
+
public clearListener() {
|
|
38
|
+
if(typeof this._listener === "function") this._listener = undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
constructor(wnd: Window, origin: string) {
|
|
42
|
+
this.wnd = wnd;
|
|
43
|
+
this.origin = origin;
|
|
44
|
+
try {
|
|
45
|
+
this.channelId = window.crypto.randomUUID();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.channelId = mid();
|
|
48
|
+
}
|
|
49
|
+
this.gc = setInterval(() => {
|
|
50
|
+
this.registry.forEach((v, k) => {
|
|
51
|
+
if (performance.now() - v.time > REGISTRY_EXPIRY) {
|
|
52
|
+
console.warn(k, "event for", v.key, "was never handled!");
|
|
53
|
+
this.registry.delete(k);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}, 5000);
|
|
57
|
+
window.addEventListener("message", this.handler);
|
|
58
|
+
this.send("_ping", undefined);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public halt() {
|
|
62
|
+
this._ready = false;
|
|
63
|
+
window.removeEventListener("message", this.handler);
|
|
64
|
+
clearInterval(this.gc);
|
|
65
|
+
this._listener = undefined;
|
|
66
|
+
this.registry.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public resume() {
|
|
70
|
+
window.addEventListener("message", this.handler);
|
|
71
|
+
this._ready = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private handle(e: MessageEvent) {
|
|
75
|
+
const dt = e.data as CommsMessage;
|
|
76
|
+
if (!dt._readium) {
|
|
77
|
+
console.warn("Ignoring", dt);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if(dt._channel !== this.channelId) return; // Not meant for us
|
|
81
|
+
switch (dt.key) {
|
|
82
|
+
case "_ack": {
|
|
83
|
+
if (!dt.id) return;
|
|
84
|
+
const v = this.registry.get(dt.id);
|
|
85
|
+
if (!v) return;
|
|
86
|
+
this.registry.delete(dt.id);
|
|
87
|
+
v.cb(!!dt.data);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
case "_pong": {
|
|
92
|
+
this._ready = true;
|
|
93
|
+
}
|
|
94
|
+
default: {
|
|
95
|
+
if(!this.ready) return;
|
|
96
|
+
if(typeof this._listener === "function")
|
|
97
|
+
this._listener(dt.key as CommsEventKey, dt.data);
|
|
98
|
+
else
|
|
99
|
+
this.listenerBuffer.push([dt.key as CommsEventKey, dt.data]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
private handler = this.handle.bind(this);
|
|
104
|
+
|
|
105
|
+
public get ready() {
|
|
106
|
+
return this._ready;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send a message to the window using postMessage-based comms communication
|
|
111
|
+
* @returns Identifier associated with the message
|
|
112
|
+
*/
|
|
113
|
+
public send(
|
|
114
|
+
key: CommsCommandKey,
|
|
115
|
+
data: unknown,
|
|
116
|
+
callback?: CommsAck,
|
|
117
|
+
strict = false,
|
|
118
|
+
transfer: Transferable[] = []
|
|
119
|
+
): string {
|
|
120
|
+
const id = mid(); // Generate reasonably unique identifier
|
|
121
|
+
if (callback)
|
|
122
|
+
this.registry.set(id, {
|
|
123
|
+
// Add callback to the registry
|
|
124
|
+
cb: callback,
|
|
125
|
+
time: performance.now(),
|
|
126
|
+
key,
|
|
127
|
+
});
|
|
128
|
+
this.wnd.postMessage(
|
|
129
|
+
{
|
|
130
|
+
_readium: COMMS_VERSION,
|
|
131
|
+
_channel: this.channelId,
|
|
132
|
+
id,
|
|
133
|
+
data,
|
|
134
|
+
key,
|
|
135
|
+
strict,
|
|
136
|
+
} as CommsMessage,
|
|
137
|
+
"/", // Same origin
|
|
138
|
+
transfer
|
|
139
|
+
);
|
|
140
|
+
return id;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Loader, ModuleName } from "@readium/navigator-html-injectables";
|
|
2
|
+
import { FrameComms } from "./FrameComms";
|
|
3
|
+
import { ReadiumWindow } from "../../../../navigator-html-injectables/types/src/helpers/dom";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class FrameManager {
|
|
7
|
+
private frame: HTMLIFrameElement;
|
|
8
|
+
private loader: Loader | undefined;
|
|
9
|
+
public readonly source: string;
|
|
10
|
+
private comms: FrameComms | undefined;
|
|
11
|
+
|
|
12
|
+
private currModules: ModuleName[] = [];
|
|
13
|
+
|
|
14
|
+
constructor(source: string) {
|
|
15
|
+
this.frame = document.createElement("iframe");
|
|
16
|
+
this.frame.classList.add("readium-navigator-iframe");
|
|
17
|
+
this.frame.style.visibility = "hidden";
|
|
18
|
+
this.frame.style.setProperty("aria-hidden", "true");
|
|
19
|
+
this.frame.style.opacity = "0";
|
|
20
|
+
this.frame.style.position = "absolute";
|
|
21
|
+
this.frame.style.pointerEvents = "none";
|
|
22
|
+
this.frame.style.transition = "visibility 0s, opacity 0.1s linear";
|
|
23
|
+
this.source = source;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async load(modules: ModuleName[]): Promise<Window> {
|
|
27
|
+
return new Promise((res, rej) => {
|
|
28
|
+
if(this.loader) {
|
|
29
|
+
const wnd = this.frame.contentWindow!;
|
|
30
|
+
// Check if currently loaded modules are equal
|
|
31
|
+
if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
|
|
32
|
+
try { res(wnd); } catch (error) {};
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.comms?.halt();
|
|
36
|
+
this.loader.destroy();
|
|
37
|
+
this.loader = new Loader(wnd as ReadiumWindow, modules);
|
|
38
|
+
this.currModules = modules;
|
|
39
|
+
this.comms = undefined;
|
|
40
|
+
try { res(wnd); } catch (error) {}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.frame.onload = () => {
|
|
44
|
+
const wnd = this.frame.contentWindow!;
|
|
45
|
+
this.loader = new Loader(wnd as ReadiumWindow, modules);
|
|
46
|
+
this.currModules = modules;
|
|
47
|
+
try { res(wnd); } catch (error) {}
|
|
48
|
+
};
|
|
49
|
+
this.frame.onerror = (err) => {
|
|
50
|
+
try { rej(err); } catch (error) {}
|
|
51
|
+
}
|
|
52
|
+
this.frame.contentWindow!.location.replace(this.source);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async destroy() {
|
|
57
|
+
await this.hide();
|
|
58
|
+
this.loader?.destroy();
|
|
59
|
+
this.frame.remove();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async hide(): Promise<void> {
|
|
63
|
+
this.frame.style.visibility = "hidden";
|
|
64
|
+
this.frame.style.setProperty("aria-hidden", "true");
|
|
65
|
+
this.frame.style.opacity = "0";
|
|
66
|
+
this.frame.style.pointerEvents = "none";
|
|
67
|
+
if(this.frame.parentElement) {
|
|
68
|
+
if(this.comms === undefined) return;
|
|
69
|
+
return new Promise((res, _) => {
|
|
70
|
+
this.comms?.send("unfocus", undefined, (_: boolean) => {
|
|
71
|
+
this.comms?.halt();
|
|
72
|
+
res();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
} else
|
|
76
|
+
this.comms?.halt();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async show(atProgress?: number): Promise<void> {
|
|
80
|
+
if(!this.frame.parentElement) {
|
|
81
|
+
console.warn("Trying to show frame that is not attached to the DOM");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if(this.comms) this.comms.resume();
|
|
85
|
+
else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
|
|
86
|
+
return new Promise((res, _) => {
|
|
87
|
+
this.comms?.send("activate", undefined, () => {
|
|
88
|
+
this.comms?.send("focus", undefined, () => {
|
|
89
|
+
const remove = () => {
|
|
90
|
+
this.frame.style.removeProperty("visibility");
|
|
91
|
+
this.frame.style.removeProperty("aria-hidden");
|
|
92
|
+
this.frame.style.removeProperty("opacity");
|
|
93
|
+
this.frame.style.removeProperty("pointer-events");
|
|
94
|
+
res();
|
|
95
|
+
}
|
|
96
|
+
if(atProgress && atProgress > 0) {
|
|
97
|
+
this.comms?.send("go_progression", atProgress, remove);
|
|
98
|
+
} else {
|
|
99
|
+
remove();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get iframe() {
|
|
107
|
+
return this.frame;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get realSize() {
|
|
111
|
+
return this.frame.getBoundingClientRect();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get window() {
|
|
115
|
+
if(!this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
|
|
116
|
+
return this.frame.contentWindow;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get atLeft() {
|
|
120
|
+
return this.window.scrollX < 5;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get atRight() {
|
|
124
|
+
return this.window.scrollX > this.window.document.scrollingElement!.scrollWidth - this.window.innerWidth - 5
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get msg() {
|
|
128
|
+
return this.comms;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get ldr() {
|
|
132
|
+
return this.loader;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { ModuleName } from "@readium/navigator-html-injectables";
|
|
2
|
+
import { Locator, Publication } from "@readium/shared";
|
|
3
|
+
import FrameBlobBuider from "./FrameBlobBuilder";
|
|
4
|
+
import { FrameManager } from "./FrameManager";
|
|
5
|
+
|
|
6
|
+
const UPPER_BOUNDARY = 5;
|
|
7
|
+
const LOWER_BOUNDARY = 3;
|
|
8
|
+
|
|
9
|
+
export class FramePoolManager {
|
|
10
|
+
private readonly container: HTMLElement;
|
|
11
|
+
private readonly positions: Locator[];
|
|
12
|
+
private _currentFrame: FrameManager | undefined;
|
|
13
|
+
private readonly pool: Map<string, FrameManager> = new Map();
|
|
14
|
+
private readonly blobs: Map<string, string> = new Map();
|
|
15
|
+
private readonly inprogress: Map<string, Promise<void>> = new Map();
|
|
16
|
+
private currentBaseURL: string | undefined;
|
|
17
|
+
|
|
18
|
+
constructor(container: HTMLElement, positions: Locator[]) {
|
|
19
|
+
this.container = container;
|
|
20
|
+
this.positions = positions;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async destroy() {
|
|
24
|
+
// Wait for all in-progress loads to complete
|
|
25
|
+
let iit = this.inprogress.values();
|
|
26
|
+
let inp = iit.next();
|
|
27
|
+
const inprogressPromises: Promise<void>[] = [];
|
|
28
|
+
while(inp.value) {
|
|
29
|
+
inprogressPromises.push(inp.value);
|
|
30
|
+
inp = iit.next();
|
|
31
|
+
}
|
|
32
|
+
if(inprogressPromises.length > 0) {
|
|
33
|
+
await Promise.allSettled(inprogressPromises);
|
|
34
|
+
}
|
|
35
|
+
this.inprogress.clear();
|
|
36
|
+
|
|
37
|
+
// Destroy all frames
|
|
38
|
+
let fit = this.pool.values();
|
|
39
|
+
let frm = fit.next();
|
|
40
|
+
while(frm.value) {
|
|
41
|
+
await (frm.value as FrameManager).destroy();
|
|
42
|
+
frm = fit.next();
|
|
43
|
+
}
|
|
44
|
+
this.pool.clear();
|
|
45
|
+
|
|
46
|
+
// Revoke all blobs
|
|
47
|
+
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
48
|
+
|
|
49
|
+
// Empty container of elements
|
|
50
|
+
this.container.childNodes.forEach(v => {
|
|
51
|
+
if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async update(pub: Publication, locator: Locator, modules: ModuleName[], force=false) {
|
|
56
|
+
let i = this.positions.findIndex(l => l.locations.position === locator.locations.position);
|
|
57
|
+
if(i < 0) throw Error(`Locator not found in position list: ${locator.locations.position} > ${this.positions.reduce<number>((acc, l) => l.locations.position || 0 > acc ? l.locations.position || 0 : acc, 0) }`);
|
|
58
|
+
const newHref = this.positions[i].href;
|
|
59
|
+
|
|
60
|
+
if(this.inprogress.has(newHref))
|
|
61
|
+
// If this same href is already being loaded, block until the other function
|
|
62
|
+
// call has finished executing so we don't end up e.g. loading the blob twice.
|
|
63
|
+
await this.inprogress.get(newHref);
|
|
64
|
+
|
|
65
|
+
// Create a new progress that doesn't resolve until complete
|
|
66
|
+
// loading of the resource and its dependencies has finished.
|
|
67
|
+
const progressPromise = new Promise<void>(async (resolve, reject) => {
|
|
68
|
+
const disposal: string[] = [];
|
|
69
|
+
const creation: string[] = [];
|
|
70
|
+
this.positions.forEach((l, j) => {
|
|
71
|
+
if(j > (i + UPPER_BOUNDARY) || j < (i - UPPER_BOUNDARY)) {
|
|
72
|
+
if(!disposal.includes(l.href)) disposal.push(l.href);
|
|
73
|
+
}
|
|
74
|
+
if(j < (i + LOWER_BOUNDARY) && j > (i - LOWER_BOUNDARY)) {
|
|
75
|
+
if(!creation.includes(l.href)) creation.push(l.href);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
disposal.forEach(async href => {
|
|
79
|
+
if(creation.includes(href)) return;
|
|
80
|
+
if(!this.pool.has(href)) return;
|
|
81
|
+
await this.pool.get(href)?.destroy();
|
|
82
|
+
this.pool.delete(href);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Check if base URL of publication has changed
|
|
86
|
+
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
87
|
+
// Revoke all blobs
|
|
88
|
+
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
89
|
+
this.blobs.clear();
|
|
90
|
+
}
|
|
91
|
+
this.currentBaseURL = pub.baseURL;
|
|
92
|
+
|
|
93
|
+
const creator = async (href: string) => {
|
|
94
|
+
if(this.pool.has(href)) {
|
|
95
|
+
const fm = this.pool.get(href)!;
|
|
96
|
+
if(!this.blobs.has(href)) {
|
|
97
|
+
await fm.destroy();
|
|
98
|
+
this.pool.delete(href);
|
|
99
|
+
} else {
|
|
100
|
+
await fm.load(modules);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const itm = pub.readingOrder.findWithHref(href);
|
|
105
|
+
if(!itm) return; // TODO throw?
|
|
106
|
+
if(!this.blobs.has(href)) {
|
|
107
|
+
const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm);
|
|
108
|
+
const blobURL = await blobBuilder.build();
|
|
109
|
+
this.blobs.set(href, blobURL);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create <iframe>
|
|
113
|
+
const fm = new FrameManager(this.blobs.get(href)!);
|
|
114
|
+
if(href !== newHref) await fm.hide(); // Avoid unecessary hide
|
|
115
|
+
this.container.appendChild(fm.iframe);
|
|
116
|
+
await fm.load(modules);
|
|
117
|
+
this.pool.set(href, fm);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await Promise.all(creation.map(href => creator(href)));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
reject(error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Update current frame
|
|
126
|
+
const newFrame = this.pool.get(newHref)!;
|
|
127
|
+
if(newFrame?.source !== this._currentFrame?.source || force) {
|
|
128
|
+
await this._currentFrame?.hide(); // Hide current frame. It's possible it no longer even exists in the DOM at this point
|
|
129
|
+
if(newFrame) // If user is speeding through the publication, this can get destroyed
|
|
130
|
+
await newFrame.load(modules); // In order to ensure modules match the latest configuration
|
|
131
|
+
|
|
132
|
+
// Update progression if necessary and show the new frame
|
|
133
|
+
const hasProgression = (locator?.locations?.progression ?? 0) > 0;
|
|
134
|
+
if(newFrame) // If user is speeding through the publication, this can get destroyed
|
|
135
|
+
await newFrame.show(hasProgression ? locator.locations.progression! : undefined); // Show/activate new frame
|
|
136
|
+
|
|
137
|
+
this._currentFrame = newFrame;
|
|
138
|
+
}
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this.inprogress.set(newHref, progressPromise); // Add the job to the in progress map
|
|
143
|
+
await progressPromise; // Wait on the job to finish...
|
|
144
|
+
this.inprogress.delete(newHref); // Delete it from the in progress map!
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get currentFrames(): (FrameManager | undefined)[] {
|
|
148
|
+
return [this._currentFrame];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get currentBounds(): DOMRect {
|
|
152
|
+
const ret = {
|
|
153
|
+
x: 0,
|
|
154
|
+
y: 0,
|
|
155
|
+
width: 0,
|
|
156
|
+
height: 0,
|
|
157
|
+
top: 0,
|
|
158
|
+
right: 0,
|
|
159
|
+
bottom: 0,
|
|
160
|
+
left: 0,
|
|
161
|
+
toJSON() {
|
|
162
|
+
return this;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
this.currentFrames.forEach(f => {
|
|
166
|
+
if(!f) return;
|
|
167
|
+
const b = f.realSize;
|
|
168
|
+
ret.x = Math.min(ret.x, b.x);
|
|
169
|
+
ret.y = Math.min(ret.y, b.y);
|
|
170
|
+
ret.width += b.width; // TODO different in vertical
|
|
171
|
+
ret.height = Math.max(ret.height, b.height);
|
|
172
|
+
ret.top = Math.min(ret.top, b.top);
|
|
173
|
+
ret.right = Math.min(ret.right, b.right);
|
|
174
|
+
ret.bottom = Math.min(ret.bottom, b.bottom);
|
|
175
|
+
ret.left = Math.min(ret.left, b.left);
|
|
176
|
+
});
|
|
177
|
+
return ret as DOMRect;
|
|
178
|
+
}
|
|
179
|
+
}
|