aksos-react-native-epub-reader 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +338 -2
- package/lib/EpubReader.d.ts +63 -0
- package/lib/EpubReader.js +199 -0
- package/lib/EpubReaderView.d.ts +24 -0
- package/lib/EpubReaderView.js +504 -0
- package/lib/assets/page-turn-1.wav +0 -0
- package/lib/assets/page-turn-2.wav +0 -0
- package/lib/assets/page-turn-3.wav +0 -0
- package/lib/assets/page-turn-4.wav +0 -0
- package/lib/engine/html.d.ts +1 -0
- package/lib/engine/html.js +5 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +30 -0
- package/lib/sound.d.ts +1 -0
- package/lib/sound.js +71 -0
- package/lib/themes.d.ts +56 -0
- package/lib/themes.js +166 -0
- package/lib/types.d.ts +133 -0
- package/lib/types.js +2 -0
- package/lib/ui/AppearanceSheet.d.ts +15 -0
- package/lib/ui/AppearanceSheet.js +127 -0
- package/lib/ui/ContentsSheet.d.ts +27 -0
- package/lib/ui/ContentsSheet.js +87 -0
- package/lib/ui/CustomizeSheet.d.ts +16 -0
- package/lib/ui/CustomizeSheet.js +139 -0
- package/lib/ui/Glass.d.ts +15 -0
- package/lib/ui/Glass.js +27 -0
- package/lib/ui/HighlightMenu.d.ts +25 -0
- package/lib/ui/HighlightMenu.js +43 -0
- package/lib/ui/Icons.d.ts +17 -0
- package/lib/ui/Icons.js +193 -0
- package/lib/ui/LineGuide.d.ts +21 -0
- package/lib/ui/LineGuide.js +92 -0
- package/lib/ui/NoteEditor.d.ts +12 -0
- package/lib/ui/NoteEditor.js +61 -0
- package/lib/ui/Scrubber.d.ts +15 -0
- package/lib/ui/Scrubber.js +86 -0
- package/lib/ui/SearchSheet.d.ts +14 -0
- package/lib/ui/SearchSheet.js +67 -0
- package/lib/ui/Sheet.d.ts +19 -0
- package/lib/ui/Sheet.js +38 -0
- package/lib/ui/Slider.d.ts +14 -0
- package/lib/ui/Slider.js +53 -0
- package/package.json +75 -5
- package/src/EpubReader.tsx +256 -0
- package/src/EpubReaderView.tsx +721 -0
- package/src/assets/page-turn-1.wav +0 -0
- package/src/assets/page-turn-2.wav +0 -0
- package/src/assets/page-turn-3.wav +0 -0
- package/src/assets/page-turn-4.wav +0 -0
- package/src/engine/html.ts +2 -0
- package/src/engine/reader.js +1696 -0
- package/src/index.ts +6 -0
- package/src/sound.ts +66 -0
- package/src/themes.ts +188 -0
- package/src/types.ts +138 -0
- package/src/ui/AppearanceSheet.tsx +167 -0
- package/src/ui/ContentsSheet.tsx +144 -0
- package/src/ui/CustomizeSheet.tsx +189 -0
- package/src/ui/Glass.tsx +37 -0
- package/src/ui/HighlightMenu.tsx +64 -0
- package/src/ui/Icons.tsx +246 -0
- package/src/ui/LineGuide.tsx +87 -0
- package/src/ui/NoteEditor.tsx +55 -0
- package/src/ui/Scrubber.tsx +78 -0
- package/src/ui/SearchSheet.tsx +79 -0
- package/src/ui/Sheet.tsx +69 -0
- package/src/ui/Slider.tsx +30 -0
|
@@ -0,0 +1,1696 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* EPUB reader engine. Runs inside the WebView (or any browser).
|
|
3
|
+
*
|
|
4
|
+
* native -> engine : window.__cmd(jsonString)
|
|
5
|
+
* engine -> native : ReactNativeWebView.postMessage(JSON {type, data})
|
|
6
|
+
*
|
|
7
|
+
* Offsets everywhere are character offsets into the concatenated text nodes of
|
|
8
|
+
* a chapter body (see collectSegs). They survive font/size/theme changes, which
|
|
9
|
+
* is why locations, highlights and bookmarks are stored as {spine, offset}.
|
|
10
|
+
*
|
|
11
|
+
* Page turn modes: 'slide' (finger-follow), 'curl' (hinged page flip),
|
|
12
|
+
* 'fade' (cross-fade) and 'scroll' (continuous vertical scroll per chapter).
|
|
13
|
+
* Curl and fade animate a "ghost" iframe that holds a copy of the chapter.
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
var RN = window.ReactNativeWebView;
|
|
19
|
+
var XLINK = 'http://www.w3.org/1999/xlink';
|
|
20
|
+
var XHTML = 'http://www.w3.org/1999/xhtml';
|
|
21
|
+
var RTL_LANGS = ['ar', 'fa', 'ps', 'ur', 'he', 'yi', 'ckb', 'ug', 'sd', 'dv'];
|
|
22
|
+
var HL_COLORS = {
|
|
23
|
+
yellow: ['rgba(255,214,10,.42)', '#e6b800'],
|
|
24
|
+
green: ['rgba(52,199,89,.38)', '#2fb24f'],
|
|
25
|
+
blue: ['rgba(10,132,255,.32)', '#0a84ff'],
|
|
26
|
+
pink: ['rgba(255,55,95,.32)', '#ff375f'],
|
|
27
|
+
purple: ['rgba(191,90,242,.34)', '#bf5af2']
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var frame = document.getElementById('frame');
|
|
31
|
+
var ghost = document.getElementById('ghost');
|
|
32
|
+
var hdr = document.getElementById('hdr');
|
|
33
|
+
var ftr = document.getElementById('ftr');
|
|
34
|
+
|
|
35
|
+
var S = {
|
|
36
|
+
zip: null, names: {}, spine: [], toc: [], rtl: false, meta: {},
|
|
37
|
+
texts: [], cum: [], totalLen: 0, indexed: false,
|
|
38
|
+
cur: -1, page: 0, pageCount: 1, token: 0,
|
|
39
|
+
doc: null, win: null, segs: [], segMap: null, curText: '',
|
|
40
|
+
pageStarts: {}, dataCache: {},
|
|
41
|
+
highlights: [], sel: null, searchId: 0, dragged: false,
|
|
42
|
+
G: null, ghostDirty: true, labelsExternal: false,
|
|
43
|
+
settings: {
|
|
44
|
+
bg: '#ffffff', fg: '#000000', link: '#0a60ff',
|
|
45
|
+
fontFamily: 'original', fontSize: 18, lineHeight: 1.5,
|
|
46
|
+
marginX: 28, gap: 48, justify: false, bold: false,
|
|
47
|
+
columns: 'auto', letterSpacing: 0, wordSpacing: 0, mode: 'slide',
|
|
48
|
+
insets: { top: 60, bottom: 52 }, direction: 'auto',
|
|
49
|
+
showHeader: true, showFooter: true
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function post(type, data) {
|
|
54
|
+
var m = JSON.stringify({ type: type, data: data || {} });
|
|
55
|
+
if (RN) RN.postMessage(m);
|
|
56
|
+
else if (window.__harnessPost) window.__harnessPost(type, data || {});
|
|
57
|
+
}
|
|
58
|
+
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
|
|
59
|
+
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
|
|
60
|
+
function debounce(fn, ms) {
|
|
61
|
+
var t;
|
|
62
|
+
return function () {
|
|
63
|
+
var a = arguments, self = this;
|
|
64
|
+
clearTimeout(t);
|
|
65
|
+
t = setTimeout(function () { fn.apply(self, a); }, ms);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ------------------------------------------------------------ zip paths */
|
|
70
|
+
|
|
71
|
+
function dirOf(p) { var i = p.lastIndexOf('/'); return i < 0 ? '' : p.slice(0, i + 1); }
|
|
72
|
+
|
|
73
|
+
function resolvePath(base, rel) {
|
|
74
|
+
if (rel == null || /^[a-z][a-z0-9+.-]*:/i.test(rel)) return null;
|
|
75
|
+
var p = rel.split('#')[0].split('?')[0];
|
|
76
|
+
if (!p) return base;
|
|
77
|
+
var full = p.charAt(0) === '/' ? p.slice(1) : dirOf(base) + p;
|
|
78
|
+
var out = [];
|
|
79
|
+
full.split('/').forEach(function (s) {
|
|
80
|
+
if (s === '..') out.pop();
|
|
81
|
+
else if (s !== '.' && s !== '') out.push(s);
|
|
82
|
+
});
|
|
83
|
+
var joined = out.join('/');
|
|
84
|
+
try { return decodeURIComponent(joined); } catch (e) { return joined; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getFile(path) {
|
|
88
|
+
if (!path) return null;
|
|
89
|
+
var f = S.zip.file(path);
|
|
90
|
+
if (f) return f;
|
|
91
|
+
var real = S.names[path.toLowerCase()];
|
|
92
|
+
return real ? S.zip.file(real) : null;
|
|
93
|
+
}
|
|
94
|
+
function readText(path) {
|
|
95
|
+
var f = getFile(path);
|
|
96
|
+
return f ? f.async('string') : Promise.resolve(null);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var MIME = {
|
|
100
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
|
|
101
|
+
svg: 'image/svg+xml', webp: 'image/webp', css: 'text/css',
|
|
102
|
+
ttf: 'font/ttf', otf: 'font/otf', woff: 'font/woff', woff2: 'font/woff2'
|
|
103
|
+
};
|
|
104
|
+
function mimeOf(path) {
|
|
105
|
+
var m = /\.([a-z0-9]+)$/i.exec(path);
|
|
106
|
+
return (m && MIME[m[1].toLowerCase()]) || 'application/octet-stream';
|
|
107
|
+
}
|
|
108
|
+
function dataUri(path) {
|
|
109
|
+
if (S.dataCache[path]) return Promise.resolve(S.dataCache[path]);
|
|
110
|
+
var f = getFile(path);
|
|
111
|
+
if (!f) return Promise.resolve(null);
|
|
112
|
+
return f.async('base64').then(function (b64) {
|
|
113
|
+
var uri = 'data:' + mimeOf(path) + ';base64,' + b64;
|
|
114
|
+
S.dataCache[path] = uri;
|
|
115
|
+
return uri;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ---------------------------------------------------------------- xml */
|
|
120
|
+
|
|
121
|
+
function parseXml(s) { return new DOMParser().parseFromString(s, 'application/xml'); }
|
|
122
|
+
function byLocal(root, name) {
|
|
123
|
+
return Array.prototype.slice.call(root.getElementsByTagNameNS('*', name));
|
|
124
|
+
}
|
|
125
|
+
function firstLocal(root, name) { return byLocal(root, name)[0] || null; }
|
|
126
|
+
function attrLocal(el, name) {
|
|
127
|
+
for (var i = 0; i < el.attributes.length; i++) {
|
|
128
|
+
if (el.attributes[i].localName === name) return el.attributes[i].value;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function parseMarkup(raw) {
|
|
133
|
+
var d = new DOMParser().parseFromString(raw, 'application/xhtml+xml');
|
|
134
|
+
if (d.getElementsByTagName('parsererror').length || !d.body) {
|
|
135
|
+
d = new DOMParser().parseFromString(raw, 'text/html');
|
|
136
|
+
}
|
|
137
|
+
return d;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* --------------------------------------------------------- book opening */
|
|
141
|
+
|
|
142
|
+
async function openBook(b64) {
|
|
143
|
+
S.zip = await JSZip.loadAsync(b64, { base64: true });
|
|
144
|
+
S.names = {};
|
|
145
|
+
Object.keys(S.zip.files).forEach(function (n) { S.names[n.toLowerCase()] = n; });
|
|
146
|
+
|
|
147
|
+
var enc = await readText('META-INF/encryption.xml');
|
|
148
|
+
if (enc) {
|
|
149
|
+
var algos = byLocal(parseXml(enc), 'EncryptionMethod').map(function (e) { return e.getAttribute('Algorithm') || ''; });
|
|
150
|
+
var drm = algos.some(function (a) {
|
|
151
|
+
return a.indexOf('idpf.org/2008/embedding') < 0 && a.indexOf('ns.adobe.com/pdf/enc#RC') < 0;
|
|
152
|
+
});
|
|
153
|
+
if (drm) throw new Error('This book is protected by DRM and cannot be opened.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var container = await readText('META-INF/container.xml');
|
|
157
|
+
if (!container) throw new Error('Not a valid EPUB (container.xml is missing).');
|
|
158
|
+
var rootfile = firstLocal(parseXml(container), 'rootfile');
|
|
159
|
+
var opfPath = rootfile && rootfile.getAttribute('full-path');
|
|
160
|
+
var opfText = opfPath && await readText(opfPath);
|
|
161
|
+
if (!opfText) throw new Error('Not a valid EPUB (package document is missing).');
|
|
162
|
+
var opf = parseXml(opfText);
|
|
163
|
+
|
|
164
|
+
var manifest = {};
|
|
165
|
+
byLocal(opf, 'item').forEach(function (it) {
|
|
166
|
+
var href = it.getAttribute('href');
|
|
167
|
+
manifest[it.getAttribute('id')] = {
|
|
168
|
+
id: it.getAttribute('id'), href: href,
|
|
169
|
+
path: resolvePath(opfPath, href),
|
|
170
|
+
type: it.getAttribute('media-type') || '',
|
|
171
|
+
props: it.getAttribute('properties') || ''
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
var spineEl = firstLocal(opf, 'spine');
|
|
176
|
+
S.spine = byLocal(spineEl, 'itemref').map(function (r) {
|
|
177
|
+
return manifest[r.getAttribute('idref')];
|
|
178
|
+
}).filter(function (m) {
|
|
179
|
+
return m && /html|xml/i.test(m.type) || (m && /\.x?html?$/i.test(m.path));
|
|
180
|
+
});
|
|
181
|
+
if (!S.spine.length) throw new Error('This EPUB has no readable content.');
|
|
182
|
+
|
|
183
|
+
var title = firstLocal(opf, 'title');
|
|
184
|
+
var lang = firstLocal(opf, 'language');
|
|
185
|
+
S.meta = {
|
|
186
|
+
title: title && !/^(unknown|untitled|none|n\/a)$/i.test(title.textContent.trim()) ? title.textContent.trim() : '',
|
|
187
|
+
author: byLocal(opf, 'creator').map(function (c) { return c.textContent.trim(); }).join(', '),
|
|
188
|
+
language: lang ? lang.textContent.trim() : '',
|
|
189
|
+
publisher: (firstLocal(opf, 'publisher') || { textContent: '' }).textContent.trim()
|
|
190
|
+
};
|
|
191
|
+
var ppd = spineEl.getAttribute('page-progression-direction');
|
|
192
|
+
var langBase = S.meta.language.toLowerCase().split('-')[0];
|
|
193
|
+
var autoRtl = ppd ? ppd === 'rtl' : RTL_LANGS.indexOf(langBase) >= 0;
|
|
194
|
+
var dirSetting = S.settings.direction;
|
|
195
|
+
S.rtl = dirSetting === 'rtl' ? true : dirSetting === 'ltr' ? false : autoRtl;
|
|
196
|
+
|
|
197
|
+
if (dirSetting === 'auto' && !S.rtl) {
|
|
198
|
+
var byContent = await contentIsRtl();
|
|
199
|
+
if (byContent) S.rtl = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
S.toc = await parseToc(opf, manifest, spineEl, opfPath);
|
|
203
|
+
post('metadata', S.meta);
|
|
204
|
+
post('toc', { items: S.toc, rtl: S.rtl, spineCount: S.spine.length });
|
|
205
|
+
sendCover(opf, manifest);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// true when the first chapters are mostly Arabic-script text (Pashto, Dari, Persian, Arabic, Urdu ...)
|
|
209
|
+
async function contentIsRtl() {
|
|
210
|
+
var arabic = 0, latin = 0;
|
|
211
|
+
var AR = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/g, LA = /[A-Za-z]/g;
|
|
212
|
+
for (var i = 0; i < Math.min(S.spine.length, 6) && arabic + latin < 1500; i++) {
|
|
213
|
+
try {
|
|
214
|
+
var text = (await parseChapter(i)).body.textContent || '';
|
|
215
|
+
arabic += (text.match(AR) || []).length;
|
|
216
|
+
latin += (text.match(LA) || []).length;
|
|
217
|
+
} catch (e) { /* ignore unreadable chapters */ }
|
|
218
|
+
}
|
|
219
|
+
return arabic > 40 && arabic > latin * 1.5;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function spineIndexOfPath(path) {
|
|
223
|
+
for (var i = 0; i < S.spine.length; i++) if (S.spine[i].path === path) return i;
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function parseToc(opf, manifest, spineEl, opfPath) {
|
|
228
|
+
var items = [];
|
|
229
|
+
function add(label, href, level, base) {
|
|
230
|
+
var path = resolvePath(base, href);
|
|
231
|
+
var frag = href && href.indexOf('#') >= 0 ? href.split('#')[1] : '';
|
|
232
|
+
var idx = path ? spineIndexOfPath(path) : -1;
|
|
233
|
+
if (idx < 0) return;
|
|
234
|
+
items.push({ label: (label || '').replace(/\s+/g, ' ').trim(), spine: idx, fragment: frag, level: level });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var navItem = null;
|
|
238
|
+
Object.keys(manifest).forEach(function (k) {
|
|
239
|
+
if (/(^|\s)nav(\s|$)/.test(manifest[k].props)) navItem = manifest[k];
|
|
240
|
+
});
|
|
241
|
+
if (navItem) {
|
|
242
|
+
var navText = await readText(navItem.path);
|
|
243
|
+
if (navText) {
|
|
244
|
+
var nd = parseMarkup(navText);
|
|
245
|
+
var navs = Array.prototype.slice.call(nd.getElementsByTagName('nav'));
|
|
246
|
+
var navEl = navs.filter(function (n) { return /toc/.test(attrLocal(n, 'type') || ''); })[0] || navs[0];
|
|
247
|
+
if (navEl) {
|
|
248
|
+
(function walk(ol, level) {
|
|
249
|
+
Array.prototype.forEach.call(ol.children, function (li) {
|
|
250
|
+
if (li.localName !== 'li') return;
|
|
251
|
+
var a = null, sub = null;
|
|
252
|
+
Array.prototype.forEach.call(li.children, function (c) {
|
|
253
|
+
if (c.localName === 'a' || c.localName === 'span') a = a || c;
|
|
254
|
+
if (c.localName === 'ol') sub = c;
|
|
255
|
+
});
|
|
256
|
+
if (a) add(a.textContent, a.getAttribute('href'), level, navItem.path);
|
|
257
|
+
if (sub) walk(sub, level + 1);
|
|
258
|
+
});
|
|
259
|
+
})(navEl.getElementsByTagName('ol')[0] || navEl, 0);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (items.length) return items;
|
|
264
|
+
|
|
265
|
+
var ncxId = spineEl.getAttribute('toc');
|
|
266
|
+
var ncx = (ncxId && manifest[ncxId]) ||
|
|
267
|
+
Object.keys(manifest).map(function (k) { return manifest[k]; }).filter(function (m) { return /ncx/.test(m.type); })[0];
|
|
268
|
+
if (ncx) {
|
|
269
|
+
var ncxText = await readText(ncx.path);
|
|
270
|
+
if (ncxText) {
|
|
271
|
+
var map = firstLocal(parseXml(ncxText), 'navMap');
|
|
272
|
+
(function walk(parent, level) {
|
|
273
|
+
Array.prototype.forEach.call(parent.children, function (np) {
|
|
274
|
+
if (np.localName !== 'navPoint') return;
|
|
275
|
+
var label = firstLocal(np, 'text');
|
|
276
|
+
var content = firstLocal(np, 'content');
|
|
277
|
+
if (content) add(label ? label.textContent : '', content.getAttribute('src'), level, ncx.path);
|
|
278
|
+
walk(np, level + 1);
|
|
279
|
+
});
|
|
280
|
+
})(map || { children: [] }, 0);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return items;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sendCover(opf, manifest) {
|
|
287
|
+
var item = null;
|
|
288
|
+
Object.keys(manifest).forEach(function (k) {
|
|
289
|
+
if (/cover-image/.test(manifest[k].props)) item = manifest[k];
|
|
290
|
+
});
|
|
291
|
+
if (!item) {
|
|
292
|
+
var meta = byLocal(opf, 'meta').filter(function (m) { return m.getAttribute('name') === 'cover'; })[0];
|
|
293
|
+
if (meta) item = manifest[meta.getAttribute('content')];
|
|
294
|
+
}
|
|
295
|
+
if (item && /image/.test(item.type)) {
|
|
296
|
+
dataUri(item.path).then(function (uri) { if (uri) post('cover', { uri: uri }); });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function chapterTitle(i) {
|
|
301
|
+
var best = '';
|
|
302
|
+
for (var k = 0; k < S.toc.length; k++) {
|
|
303
|
+
if (S.toc[k].spine <= i) best = S.toc[k].label; else break;
|
|
304
|
+
}
|
|
305
|
+
return best || S.meta.title || '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ------------------------------------------------------------- chapters */
|
|
309
|
+
|
|
310
|
+
function collectSegs(root) {
|
|
311
|
+
var w = root.ownerDocument.createTreeWalker(root, 4, null);
|
|
312
|
+
var segs = [], pos = 0, n;
|
|
313
|
+
while ((n = w.nextNode())) {
|
|
314
|
+
var len = n.data.length;
|
|
315
|
+
segs.push({ node: n, start: pos, end: pos + len });
|
|
316
|
+
pos += len;
|
|
317
|
+
}
|
|
318
|
+
segs.total = pos;
|
|
319
|
+
return segs;
|
|
320
|
+
}
|
|
321
|
+
function segsText(segs) { return segs.map(function (s) { return s.node.data; }).join(''); }
|
|
322
|
+
|
|
323
|
+
async function parseChapter(i) {
|
|
324
|
+
var it = S.spine[i];
|
|
325
|
+
var raw = await readText(it.path);
|
|
326
|
+
if (raw == null) raw = '<html><body><p></p></body></html>';
|
|
327
|
+
var doc = parseMarkup(raw);
|
|
328
|
+
var body = doc.body || doc.documentElement;
|
|
329
|
+
var refs = [];
|
|
330
|
+
Array.prototype.slice.call(doc.querySelectorAll('link,style')).forEach(function (el) {
|
|
331
|
+
if (el.namespaceURI !== XHTML) return;
|
|
332
|
+
if (el.localName === 'style') refs.push({ text: el.textContent, base: it.path });
|
|
333
|
+
else if (/stylesheet/i.test(el.getAttribute('rel') || '')) refs.push({ href: el.getAttribute('href'), base: it.path });
|
|
334
|
+
el.parentNode.removeChild(el);
|
|
335
|
+
});
|
|
336
|
+
Array.prototype.slice.call(body.querySelectorAll('script,noscript,iframe,object,embed')).forEach(function (el) {
|
|
337
|
+
el.parentNode.removeChild(el);
|
|
338
|
+
});
|
|
339
|
+
return { index: i, path: it.path, body: body, refs: refs, lang: doc.documentElement.getAttribute('xml:lang') || doc.documentElement.getAttribute('lang') || '', css: null };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function rewriteCss(css, basePath) {
|
|
343
|
+
css = css.replace(/@import[^;]*;/g, '');
|
|
344
|
+
var re = /url\(\s*(['"]?)([^)'"]+)\1\s*\)/g, m, seen = {}, jobs = [];
|
|
345
|
+
while ((m = re.exec(css))) {
|
|
346
|
+
if (seen[m[0]] || /^[a-z][a-z0-9+.-]*:/i.test(m[2])) continue;
|
|
347
|
+
seen[m[0]] = 1;
|
|
348
|
+
jobs.push({ full: m[0], rel: m[2] });
|
|
349
|
+
}
|
|
350
|
+
for (var k = 0; k < jobs.length; k++) {
|
|
351
|
+
var p = resolvePath(basePath, jobs[k].rel);
|
|
352
|
+
var uri = p && await dataUri(p);
|
|
353
|
+
if (uri) css = css.split(jobs[k].full).join('url("' + uri + '")');
|
|
354
|
+
}
|
|
355
|
+
return css;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function inlineChapter(ch) {
|
|
359
|
+
ch.css = [];
|
|
360
|
+
for (var r = 0; r < ch.refs.length; r++) {
|
|
361
|
+
var ref = ch.refs[r], text, base;
|
|
362
|
+
if (ref.href != null) {
|
|
363
|
+
base = resolvePath(ref.base, ref.href);
|
|
364
|
+
text = base && await readText(base);
|
|
365
|
+
} else { text = ref.text; base = ref.base; }
|
|
366
|
+
if (text) ch.css.push(await rewriteCss(text, base));
|
|
367
|
+
}
|
|
368
|
+
var imgs = Array.prototype.slice.call(ch.body.querySelectorAll('img,image'));
|
|
369
|
+
for (var k = 0; k < imgs.length; k++) {
|
|
370
|
+
var el = imgs[k];
|
|
371
|
+
var src = el.localName === 'img' ? el.getAttribute('src')
|
|
372
|
+
: (el.getAttributeNS(XLINK, 'href') || el.getAttribute('href'));
|
|
373
|
+
var path = src && resolvePath(ch.path, src);
|
|
374
|
+
var uri = path && await dataUri(path);
|
|
375
|
+
if (!uri) continue;
|
|
376
|
+
if (el.localName === 'img') { el.setAttribute('src', uri); el.removeAttribute('srcset'); }
|
|
377
|
+
else { el.setAttribute('href', uri); el.setAttributeNS(XLINK, 'xlink:href', uri); }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function mountChapter(ch) {
|
|
382
|
+
var d = frame.contentDocument;
|
|
383
|
+
d.open();
|
|
384
|
+
d.write('<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>');
|
|
385
|
+
d.close();
|
|
386
|
+
d.documentElement.setAttribute('dir', S.rtl ? 'rtl' : 'ltr');
|
|
387
|
+
d.documentElement.setAttribute('lang', ch.lang || S.meta.language || '');
|
|
388
|
+
ch.css.forEach(function (t) {
|
|
389
|
+
var st = d.createElement('style');
|
|
390
|
+
st.textContent = t;
|
|
391
|
+
d.head.appendChild(st);
|
|
392
|
+
});
|
|
393
|
+
var app = d.createElement('style');
|
|
394
|
+
app.id = 'app';
|
|
395
|
+
d.head.appendChild(app);
|
|
396
|
+
Array.prototype.forEach.call(ch.body.attributes, function (a) {
|
|
397
|
+
if (a.name !== 'style' && a.name.indexOf('on') !== 0) d.body.setAttribute(a.name, a.value);
|
|
398
|
+
});
|
|
399
|
+
var holder = d.body;
|
|
400
|
+
var scrollMode = S.settings.mode === 'scroll';
|
|
401
|
+
if (scrollMode) {
|
|
402
|
+
holder = d.createElement('div');
|
|
403
|
+
holder.className = '__ch';
|
|
404
|
+
holder.setAttribute('data-spine', ch.index);
|
|
405
|
+
d.body.appendChild(holder);
|
|
406
|
+
}
|
|
407
|
+
Array.prototype.forEach.call(ch.body.childNodes, function (n) {
|
|
408
|
+
holder.appendChild(d.importNode(n, true));
|
|
409
|
+
});
|
|
410
|
+
S.doc = d;
|
|
411
|
+
S.win = frame.contentWindow;
|
|
412
|
+
S.root = holder;
|
|
413
|
+
S.sections = scrollMode ? [{ index: ch.index, el: holder }] : [];
|
|
414
|
+
S.cssSeen = {};
|
|
415
|
+
ch.css.forEach(function (t) { S.cssSeen[t] = true; });
|
|
416
|
+
S.ghostDirty = true;
|
|
417
|
+
refreshSegs();
|
|
418
|
+
S.curText = segsText(S.segs);
|
|
419
|
+
attachListeners(d);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function refreshSegs() {
|
|
423
|
+
S.segs = collectSegs(rootEl());
|
|
424
|
+
S.segMap = new Map();
|
|
425
|
+
S.segs.forEach(function (s) { S.segMap.set(s.node, s); });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function waitAssets() {
|
|
429
|
+
var d = S.doc;
|
|
430
|
+
var pending = Array.prototype.slice.call(d.images || []).filter(function (im) { return !im.complete; })
|
|
431
|
+
.map(function (im) { return new Promise(function (r) { im.onload = im.onerror = r; }); });
|
|
432
|
+
if (d.fonts && d.fonts.ready) pending.push(d.fonts.ready);
|
|
433
|
+
if (pending.length) await Promise.race([Promise.all(pending), sleep(1500)]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* --------------------------------------------------------------- layout */
|
|
437
|
+
|
|
438
|
+
function geometry() {
|
|
439
|
+
var s = S.settings, W = window.innerWidth, H = window.innerHeight;
|
|
440
|
+
var mx = s.marginX, ins = s.insets;
|
|
441
|
+
var scroll = s.mode === 'scroll';
|
|
442
|
+
var pageW = Math.max(120, W - 2 * mx), pageH = Math.max(120, H - ins.top - ins.bottom);
|
|
443
|
+
var cols = 1;
|
|
444
|
+
if (!scroll) cols = s.columns === 'auto' ? (pageW >= 640 ? 2 : 1) : (+s.columns === 2 ? 2 : 1);
|
|
445
|
+
var colW = pageW;
|
|
446
|
+
if (cols === 2) { colW = Math.floor((pageW - s.gap) / 2); pageW = colW * 2 + s.gap; }
|
|
447
|
+
return { W: W, H: H, mx: mx, top: ins.top, gap: s.gap, pageW: pageW, pageH: pageH, cols: cols, colW: colW, scroll: scroll, mode: s.mode };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function appCss(G) {
|
|
451
|
+
var s = S.settings;
|
|
452
|
+
var ff = s.fontFamily && s.fontFamily !== 'original' ? s.fontFamily : null;
|
|
453
|
+
var dir = S.rtl ? 'rtl' : 'ltr';
|
|
454
|
+
var css = [
|
|
455
|
+
'html{margin:0!important;padding:0!important;background:' + s.bg + '!important;-webkit-text-size-adjust:none;direction:' + dir + '!important;' +
|
|
456
|
+
(G.scroll ? 'overflow-x:hidden!important;overflow-y:scroll!important;touch-action:pan-y;overflow-anchor:none;' : 'overflow:hidden!important;touch-action:none;') + '}',
|
|
457
|
+
'html::-webkit-scrollbar{display:none}',
|
|
458
|
+
'body{margin:0!important;padding:0!important;border:0!important;width:' + G.pageW + 'px!important;max-width:none!important;min-width:0!important;' +
|
|
459
|
+
(G.scroll ? 'height:auto!important;'
|
|
460
|
+
: 'height:' + G.pageH + 'px!important;column-width:' + G.colW + 'px!important;-webkit-column-width:' + G.colW + 'px!important;column-gap:' + G.gap + 'px!important;-webkit-column-gap:' + G.gap + 'px!important;column-fill:auto!important;overflow:hidden!important;') +
|
|
461
|
+
'color:' + s.fg + '!important;background:transparent!important;font-size:' + s.fontSize + 'px!important;line-height:' + s.lineHeight + '!important;direction:' + dir + '!important;' +
|
|
462
|
+
(ff ? 'font-family:' + ff + '!important;' : '') +
|
|
463
|
+
(s.justify ? 'text-align:justify!important;hyphens:auto;-webkit-hyphens:auto;' : '') +
|
|
464
|
+
(s.bold ? 'font-weight:600!important;' : '') +
|
|
465
|
+
(s.letterSpacing ? 'letter-spacing:' + s.letterSpacing + 'em!important;' : '') +
|
|
466
|
+
(s.wordSpacing ? 'word-spacing:' + s.wordSpacing + 'em!important;' : '') +
|
|
467
|
+
'-webkit-touch-callout:none;-webkit-user-select:text;user-select:text;overflow-wrap:break-word;word-wrap:break-word}',
|
|
468
|
+
'body *{color:inherit!important;background-color:transparent!important;' + (ff ? 'font-family:inherit!important;' : '') + (s.bold ? 'font-weight:inherit;' : '') + 'box-sizing:border-box}',
|
|
469
|
+
'body a,body a *{color:' + s.link + '!important}',
|
|
470
|
+
'body img,body svg,body video{max-width:100%!important;max-height:' + G.pageH + 'px!important;object-fit:contain;break-inside:avoid}',
|
|
471
|
+
'body img{height:auto}',
|
|
472
|
+
'body h1,body h2,body h3,body h4{break-after:avoid;-webkit-column-break-after:avoid}',
|
|
473
|
+
'body table{max-width:100%}',
|
|
474
|
+
'::selection{background:rgba(10,132,255,.3)}',
|
|
475
|
+
'mark{color:inherit!important;border-radius:2px;padding:0!important}'
|
|
476
|
+
];
|
|
477
|
+
Object.keys(HL_COLORS).forEach(function (c) {
|
|
478
|
+
css.push('body mark.hl-' + c + '{background-color:' + HL_COLORS[c][0] + '!important}');
|
|
479
|
+
css.push('body mark.ul-' + c + '{background-color:transparent!important;border-bottom:2px solid ' + HL_COLORS[c][1] + ';border-radius:0}');
|
|
480
|
+
});
|
|
481
|
+
css.push('body mark.hit{background-color:rgba(255,149,0,.5)!important}');
|
|
482
|
+
return css.join('\n');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function place(el, G) {
|
|
486
|
+
el.style.left = G.mx + 'px';
|
|
487
|
+
el.style.top = G.top + 'px';
|
|
488
|
+
el.style.width = G.pageW + 'px';
|
|
489
|
+
el.style.height = G.pageH + 'px';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function layout() {
|
|
493
|
+
var G = S.G = geometry(), s = S.settings, d = S.doc;
|
|
494
|
+
d.getElementById('app').textContent = appCss(G);
|
|
495
|
+
place(frame, G);
|
|
496
|
+
place(ghost, G);
|
|
497
|
+
placeCurl(G);
|
|
498
|
+
document.documentElement.style.setProperty('--bg', s.bg);
|
|
499
|
+
document.body.style.background = s.bg;
|
|
500
|
+
hdr.style.top = Math.max(10, G.top - 34) + 'px';
|
|
501
|
+
ftr.style.bottom = Math.max(10, s.insets.bottom - 34) + 'px';
|
|
502
|
+
hdr.style.color = ftr.style.color = s.fg;
|
|
503
|
+
hdr.style.display = s.showHeader ? '' : 'none';
|
|
504
|
+
ftr.style.display = s.showFooter ? '' : 'none';
|
|
505
|
+
var b = d.body;
|
|
506
|
+
var prev = b.style.transform;
|
|
507
|
+
b.style.transition = 'none';
|
|
508
|
+
b.style.transform = 'none';
|
|
509
|
+
if (G.scroll) {
|
|
510
|
+
S.pageCount = Math.max(1, Math.ceil(rootEl().offsetHeight / G.pageH - 0.02));
|
|
511
|
+
} else {
|
|
512
|
+
var step = G.pageW + G.gap;
|
|
513
|
+
S.pageCount = Math.max(1, Math.ceil((d.body.scrollWidth + G.gap) / step - 0.01));
|
|
514
|
+
}
|
|
515
|
+
b.style.transform = prev;
|
|
516
|
+
S.pageStarts = {};
|
|
517
|
+
S.ghostDirty = true;
|
|
518
|
+
scheduleGhost();
|
|
519
|
+
if (G.scroll) scheduleExtras(); else clearExtras();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function autoLabels() {
|
|
523
|
+
if (S.labelsExternal) return;
|
|
524
|
+
hdr.textContent = chapterTitle(S.cur);
|
|
525
|
+
ftr.textContent = (S.page + 1) + ' / ' + S.pageCount;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function showPage(p, animate) {
|
|
529
|
+
p = clamp(p, 0, S.pageCount - 1);
|
|
530
|
+
S.page = p;
|
|
531
|
+
var G = S.G, b = S.doc.body;
|
|
532
|
+
if (G.scroll) {
|
|
533
|
+
b.style.transition = 'none';
|
|
534
|
+
b.style.transform = 'none';
|
|
535
|
+
S.win.scrollTo({ top: bodyTop() + p * G.pageH, behavior: animate ? 'smooth' : 'auto' });
|
|
536
|
+
} else {
|
|
537
|
+
scrollTo(S.doc.body, pageX(p), animate);
|
|
538
|
+
}
|
|
539
|
+
autoLabels();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// horizontal scroll position of page p (negative in right-to-left documents)
|
|
543
|
+
function pageX(p) {
|
|
544
|
+
return (S.rtl ? -1 : 1) * p * (S.G.pageW + S.G.gap);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function currentX() {
|
|
548
|
+
return S.doc.body.scrollLeft;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
var scrollAnim = 0;
|
|
552
|
+
// scroll an element horizontally, optionally with a short ease-out animation
|
|
553
|
+
function scrollTo(el, x, animate) {
|
|
554
|
+
var id = ++scrollAnim;
|
|
555
|
+
if (!animate) { el.scrollLeft = x; return; }
|
|
556
|
+
var from = el.scrollLeft, t0 = null, dur = 240;
|
|
557
|
+
function step(ts) {
|
|
558
|
+
if (id !== scrollAnim) return;
|
|
559
|
+
if (t0 == null) t0 = ts;
|
|
560
|
+
var t = Math.min(1, (ts - t0) / dur);
|
|
561
|
+
el.scrollLeft = from + (x - from) * (1 - Math.pow(1 - t, 3));
|
|
562
|
+
if (t < 1) requestAnimationFrame(step);
|
|
563
|
+
}
|
|
564
|
+
requestAnimationFrame(step);
|
|
565
|
+
setTimeout(function () { if (id === scrollAnim) el.scrollLeft = x; }, dur + 80);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* ----------------------------------------- ghost iframe: curl and fade */
|
|
569
|
+
|
|
570
|
+
var ghostTimer;
|
|
571
|
+
function scheduleGhost() {
|
|
572
|
+
clearTimeout(ghostTimer);
|
|
573
|
+
if (!S.G || S.G.scroll) return;
|
|
574
|
+
ghostTimer = setTimeout(function () { if (S.ghostDirty && S.doc && !S.ghostLock) buildGhost(); }, 250);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildGhost() {
|
|
578
|
+
if (S.ghostLock) return;
|
|
579
|
+
buildInto(ghost);
|
|
580
|
+
var g2 = document.getElementById("ghost2");
|
|
581
|
+
if (g2) buildInto(g2);
|
|
582
|
+
S.ghostDirty = false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function buildInto(frameEl) {
|
|
586
|
+
var src = S.doc, g = frameEl.contentDocument;
|
|
587
|
+
g.open();
|
|
588
|
+
g.write('<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>');
|
|
589
|
+
g.close();
|
|
590
|
+
g.documentElement.setAttribute('dir', S.rtl ? 'rtl' : 'ltr');
|
|
591
|
+
Array.prototype.forEach.call(src.head.querySelectorAll('style'), function (st) {
|
|
592
|
+
var c = g.createElement('style');
|
|
593
|
+
c.textContent = st.textContent;
|
|
594
|
+
g.head.appendChild(c);
|
|
595
|
+
});
|
|
596
|
+
Array.prototype.forEach.call(src.body.attributes, function (a) {
|
|
597
|
+
if (a.name !== 'style') g.body.setAttribute(a.name, a.value);
|
|
598
|
+
});
|
|
599
|
+
Array.prototype.forEach.call(src.body.childNodes, function (n) {
|
|
600
|
+
g.body.appendChild(g.importNode(n, true));
|
|
601
|
+
});
|
|
602
|
+
S.ghostDirty = false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function setGhostPage(p) {
|
|
606
|
+
ghost.contentDocument.body.scrollLeft = pageX(p);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Animate from the current page to `to` using the ghost. Resolves when finished.
|
|
610
|
+
function turn(to, forward) {
|
|
611
|
+
if (S.G.mode === 'curl') return curlTurn(to, forward);
|
|
612
|
+
var from = S.page, kind = S.G.mode;
|
|
613
|
+
return new Promise(function (resolve) {
|
|
614
|
+
if (S.ghostDirty) buildGhost();
|
|
615
|
+
syncLabels(oldPage);
|
|
616
|
+
var angle = (S.rtl ? 1 : -1) * 90, dur = kind === 'fade' ? 180 : 420;
|
|
617
|
+
oldPage.style.transition = 'none';
|
|
618
|
+
oldPage.style.transformOrigin = S.rtl ? 'right center' : 'left center';
|
|
619
|
+
oldPage.style.boxShadow = 'none';
|
|
620
|
+
oldPage.style.visibility = 'visible';
|
|
621
|
+
function finish() {
|
|
622
|
+
oldPage.style.visibility = 'hidden';
|
|
623
|
+
oldPage.style.transition = 'none';
|
|
624
|
+
oldPage.style.transform = 'none';
|
|
625
|
+
oldPage.style.opacity = '1';
|
|
626
|
+
oldPage.style.boxShadow = 'none';
|
|
627
|
+
resolve();
|
|
628
|
+
}
|
|
629
|
+
if (forward) {
|
|
630
|
+
// old page lifts away over the new one
|
|
631
|
+
setGhostPage(from);
|
|
632
|
+
showPage(to, false);
|
|
633
|
+
oldPage.style.opacity = '1';
|
|
634
|
+
oldPage.style.transform = 'perspective(1600px) rotateY(0deg)';
|
|
635
|
+
void oldPage.offsetWidth;
|
|
636
|
+
if (kind === 'fade') {
|
|
637
|
+
oldPage.style.transition = 'opacity ' + dur + 'ms ease';
|
|
638
|
+
oldPage.style.opacity = '0';
|
|
639
|
+
} else {
|
|
640
|
+
oldPage.style.transition = 'transform ' + dur + 'ms cubic-bezier(.45,.05,.55,.95), box-shadow ' + dur + 'ms';
|
|
641
|
+
oldPage.style.boxShadow = '0 0 38px rgba(0,0,0,.45)';
|
|
642
|
+
oldPage.style.transform = 'perspective(1600px) rotateY(' + angle + 'deg)';
|
|
643
|
+
}
|
|
644
|
+
setTimeout(finish, dur + 30);
|
|
645
|
+
} else {
|
|
646
|
+
// previous page falls back over the current one
|
|
647
|
+
setGhostPage(to);
|
|
648
|
+
if (kind === 'fade') {
|
|
649
|
+
oldPage.style.opacity = '0';
|
|
650
|
+
oldPage.style.transform = 'none';
|
|
651
|
+
} else {
|
|
652
|
+
oldPage.style.transform = 'perspective(1600px) rotateY(' + angle + 'deg)';
|
|
653
|
+
oldPage.style.boxShadow = '0 0 38px rgba(0,0,0,.45)';
|
|
654
|
+
}
|
|
655
|
+
void oldPage.offsetWidth;
|
|
656
|
+
if (kind === 'fade') {
|
|
657
|
+
oldPage.style.transition = 'opacity ' + dur + 'ms ease';
|
|
658
|
+
oldPage.style.opacity = '1';
|
|
659
|
+
} else {
|
|
660
|
+
oldPage.style.transition = 'transform ' + dur + 'ms cubic-bezier(.45,.05,.55,.95)';
|
|
661
|
+
oldPage.style.transform = 'perspective(1600px) rotateY(0deg)';
|
|
662
|
+
}
|
|
663
|
+
setTimeout(function () { showPage(to, false); finish(); }, dur + 30);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
/* ---------------------------------------------------- page curl (corner fold) */
|
|
670
|
+
// The whole screen is the page. A corner A is dragged to F. The fold line is the perpendicular
|
|
671
|
+
// bisector of A-F:
|
|
672
|
+
// - the part of the old page on the far side stays flat (oldPage, clipped to a polygon),
|
|
673
|
+
// - the part on A's side is mirrored across the fold line and drawn as the back of the page (flap),
|
|
674
|
+
// - the next page shows underneath (the real frame).
|
|
675
|
+
// Page corners are rounded like the device screen.
|
|
676
|
+
var oldPage = document.getElementById('oldPage');
|
|
677
|
+
var curlBox = document.getElementById('curlBox');
|
|
678
|
+
var flap = document.getElementById('flap');
|
|
679
|
+
var flapClip = document.getElementById('flapClip');
|
|
680
|
+
var flapBack = document.getElementById('flapBack');
|
|
681
|
+
var foldShade = document.getElementById('foldShade');
|
|
682
|
+
var ghost2 = document.getElementById('ghost2');
|
|
683
|
+
var curlBusy = false;
|
|
684
|
+
|
|
685
|
+
function shade(hex, amt) {
|
|
686
|
+
var v = [1, 3, 5].map(function (i) { return parseInt(hex.slice(i, i + 2), 16); });
|
|
687
|
+
return 'rgb(' + v.map(function (c) { return Math.max(0, Math.min(255, Math.round(c + c * amt))); }).join(',') + ')';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function placeCurl(G) {
|
|
691
|
+
var bg = S.settings.bg;
|
|
692
|
+
[oldPage, curlBox, flap, flapClip, flapBack].forEach(function (el) {
|
|
693
|
+
el.style.left = '0px';
|
|
694
|
+
el.style.top = '0px';
|
|
695
|
+
el.style.width = G.W + 'px';
|
|
696
|
+
el.style.height = G.H + 'px';
|
|
697
|
+
});
|
|
698
|
+
oldPage.style.backgroundColor = bg;
|
|
699
|
+
// the text iframes sit inside the page at the text position
|
|
700
|
+
place(ghost2, G);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// running header / footer are part of the page, so they curl with it
|
|
704
|
+
function syncLabels(c) {
|
|
705
|
+
if (!c._lbl) {
|
|
706
|
+
c._lbl = [document.createElement('div'), document.createElement('div')];
|
|
707
|
+
c._lbl.forEach(function (e) { e.className = 'lbl'; c.appendChild(e); });
|
|
708
|
+
}
|
|
709
|
+
c._lbl[0].textContent = hdr.textContent;
|
|
710
|
+
c._lbl[0].style.top = hdr.style.top || '20px';
|
|
711
|
+
c._lbl[0].style.color = hdr.style.color;
|
|
712
|
+
c._lbl[0].style.display = hdr.style.display;
|
|
713
|
+
c._lbl[1].textContent = ftr.textContent;
|
|
714
|
+
c._lbl[1].style.bottom = ftr.style.bottom || '20px';
|
|
715
|
+
c._lbl[1].style.color = ftr.style.color;
|
|
716
|
+
c._lbl[1].style.display = ftr.style.display;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// keep the part of a convex polygon on one side of the line through M with normal n
|
|
720
|
+
function clipPoly(poly, M, n, keepNegative) {
|
|
721
|
+
var out = [];
|
|
722
|
+
for (var i = 0; i < poly.length; i++) {
|
|
723
|
+
var p = poly[i], q = poly[(i + 1) % poly.length];
|
|
724
|
+
var sp = (p.x - M.x) * n.x + (p.y - M.y) * n.y, sq = (q.x - M.x) * n.x + (q.y - M.y) * n.y;
|
|
725
|
+
var inP = keepNegative ? sp <= 0 : sp >= 0, inQ = keepNegative ? sq <= 0 : sq >= 0;
|
|
726
|
+
if (inP) out.push(p);
|
|
727
|
+
if (inP !== inQ) {
|
|
728
|
+
var t = sp / (sp - sq);
|
|
729
|
+
out.push({ x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
function polyCss(pts) {
|
|
735
|
+
if (pts.length < 3) return 'polygon(0px 0px,0px 0px,0px 0px)';
|
|
736
|
+
return 'polygon(' + pts.map(function (p) { return p.x.toFixed(1) + 'px ' + p.y.toFixed(1) + 'px'; }).join(',') + ')';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function setGhostPages(p) {
|
|
740
|
+
setGhostPage(p);
|
|
741
|
+
ghost2.contentDocument.body.scrollLeft = pageX(p);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Where the dragged corner is for progress q (0 = flat page, 1 = page turned over).
|
|
745
|
+
// The corner first lifts up and across, then sweeps over the page: a diagonal fold that straightens out.
|
|
746
|
+
function curlCorner(st, q) {
|
|
747
|
+
var G = S.G, H = G.H;
|
|
748
|
+
var lift = 0.3 * H * Math.sin(Math.PI * Math.min(1, q));
|
|
749
|
+
return {
|
|
750
|
+
x: st.A.x + (st.E.x - st.A.x) * q,
|
|
751
|
+
y: st.A.y + (st.A.y > H / 2 ? -lift : lift)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function curlBegin(to, forward, touchY, raw) {
|
|
756
|
+
var G = S.G, W = G.W, H = G.H, rtl = S.rtl, from = S.page;
|
|
757
|
+
if (!raw) {
|
|
758
|
+
if (S.ghostDirty) buildGhost();
|
|
759
|
+
if (forward) { setGhostPages(from); showPage(to, false); } else { setGhostPages(to); }
|
|
760
|
+
}
|
|
761
|
+
var bottom = touchY == null ? true : touchY > H / 2;
|
|
762
|
+
var st = {
|
|
763
|
+
from: from, to: to, forward: forward, p: 0,
|
|
764
|
+
A: { x: rtl ? 0 : W, y: bottom ? H : 0 },
|
|
765
|
+
E: { x: rtl ? 2 * W : -W, y: bottom ? H : 0 }
|
|
766
|
+
};
|
|
767
|
+
syncLabels(oldPage);
|
|
768
|
+
syncLabels(flapClip);
|
|
769
|
+
oldPage.style.visibility = 'visible';
|
|
770
|
+
oldPage.style.transition = 'none';
|
|
771
|
+
oldPage.style.transform = 'none';
|
|
772
|
+
oldPage.style.opacity = '1';
|
|
773
|
+
oldPage.style.boxShadow = 'none';
|
|
774
|
+
curlBox.style.visibility = 'visible';
|
|
775
|
+
flapBack.style.backgroundColor = shade(S.settings.bg, -0.08);
|
|
776
|
+
ghost2.style.opacity = '0.32';
|
|
777
|
+
foldShade.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0) 0px, rgba(0,0,0,.16) 20px, rgba(0,0,0,.42) 25px, rgba(0,0,0,.14) 40px, rgba(0,0,0,0) 90px)';
|
|
778
|
+
hdr.style.visibility = 'hidden';
|
|
779
|
+
ftr.style.visibility = 'hidden';
|
|
780
|
+
curlBusy = true;
|
|
781
|
+
return st;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// p: 0 = start of the turn, 1 = finished (for a backward turn the same curl is played in reverse)
|
|
785
|
+
function curlApply(st, p) {
|
|
786
|
+
var G = S.G, W = G.W, H = G.H;
|
|
787
|
+
var q = st.forward ? p : 1 - p;
|
|
788
|
+
var F = curlCorner(st, q), A = st.A;
|
|
789
|
+
var dx = F.x - A.x, dy = F.y - A.y, dist = Math.sqrt(dx * dx + dy * dy);
|
|
790
|
+
if (dist < 1.5) {
|
|
791
|
+
oldPage.style.clipPath = 'none';
|
|
792
|
+
oldPage.style.webkitClipPath = 'none';
|
|
793
|
+
flapClip.style.clipPath = 'polygon(0px 0px,0px 0px,0px 0px)';
|
|
794
|
+
foldShade.style.display = 'none';
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
var n = { x: dx / dist, y: dy / dist }, M = { x: (A.x + F.x) / 2, y: (A.y + F.y) / 2 };
|
|
798
|
+
var rect = [{ x: 0, y: 0 }, { x: W, y: 0 }, { x: W, y: H }, { x: 0, y: H }];
|
|
799
|
+
var flat = polyCss(clipPoly(rect, M, n, false));
|
|
800
|
+
oldPage.style.clipPath = flat;
|
|
801
|
+
oldPage.style.webkitClipPath = flat;
|
|
802
|
+
var fold = polyCss(clipPoly(rect, M, n, true));
|
|
803
|
+
flapClip.style.clipPath = fold;
|
|
804
|
+
flapClip.style.webkitClipPath = fold;
|
|
805
|
+
var mn = M.x * n.x + M.y * n.y;
|
|
806
|
+
flap.style.transform = 'matrix(' + [1 - 2 * n.x * n.x, -2 * n.x * n.y, -2 * n.x * n.y, 1 - 2 * n.y * n.y, 2 * mn * n.x, 2 * mn * n.y].join(',') + ')';
|
|
807
|
+
// shading strip centred on the fold line; local +y points towards the corner A
|
|
808
|
+
var angle = Math.atan2(n.x, -n.y); // direction of the fold line (perpendicular to n)
|
|
809
|
+
var L = 2 * (W + H);
|
|
810
|
+
foldShade.style.display = 'block';
|
|
811
|
+
foldShade.style.width = L + 'px';
|
|
812
|
+
foldShade.style.height = '90px';
|
|
813
|
+
foldShade.style.transform = 'translate(' + M.x + 'px,' + M.y + 'px) rotate(' + angle + 'rad) translate(' + (-L / 2) + 'px,-25px)';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function curlHide() {
|
|
817
|
+
oldPage.style.visibility = 'hidden';
|
|
818
|
+
oldPage.style.clipPath = 'none';
|
|
819
|
+
oldPage.style.webkitClipPath = 'none';
|
|
820
|
+
curlBox.style.visibility = 'hidden';
|
|
821
|
+
hdr.style.visibility = '';
|
|
822
|
+
ftr.style.visibility = '';
|
|
823
|
+
curlBusy = false;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function curlEnd(st, commit) {
|
|
827
|
+
curlHide();
|
|
828
|
+
if (st.forward && !commit) showPage(st.from, false);
|
|
829
|
+
if (!st.forward && commit) showPage(st.to, false);
|
|
830
|
+
curlBusy = false;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
|
|
834
|
+
|
|
835
|
+
function curlAnimate(st, p0, p1, dur) {
|
|
836
|
+
return new Promise(function (resolve) {
|
|
837
|
+
var t0 = null, done = false;
|
|
838
|
+
function finish() { if (!done) { done = true; resolve(); } }
|
|
839
|
+
function step(ts) {
|
|
840
|
+
if (done) return;
|
|
841
|
+
if (t0 == null) t0 = ts;
|
|
842
|
+
var t = Math.min(1, (ts - t0) / dur);
|
|
843
|
+
st.p = p0 + (p1 - p0) * easeInOutCubic(t);
|
|
844
|
+
curlApply(st, st.p);
|
|
845
|
+
if (t < 1) requestAnimationFrame(step); else finish();
|
|
846
|
+
}
|
|
847
|
+
curlApply(st, p0);
|
|
848
|
+
requestAnimationFrame(step);
|
|
849
|
+
setTimeout(function () { if (done) return; st.p = p1; curlApply(st, p1); finish(); }, dur + 400);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function curlTurn(to, forward) {
|
|
854
|
+
post('pageTurn', { mode: 'curl' });
|
|
855
|
+
if (S.ghostDirty) {
|
|
856
|
+
buildGhost();
|
|
857
|
+
var fonts = [ghost, ghost2].map(function (g) { return g.contentDocument.fonts && g.contentDocument.fonts.ready; }).filter(Boolean);
|
|
858
|
+
if (fonts.length) await Promise.race([Promise.all(fonts), sleep(400)]);
|
|
859
|
+
}
|
|
860
|
+
var st = curlBegin(to, forward, null);
|
|
861
|
+
await curlAnimate(st, 0, 1, 900);
|
|
862
|
+
curlEnd(st, true);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// finger-follow: called from touchmove while dragging in curl mode
|
|
866
|
+
function curlDrag(t0, dx) {
|
|
867
|
+
if (t0.curl === undefined) {
|
|
868
|
+
var forward = S.rtl ? dx > 0 : dx < 0;
|
|
869
|
+
var to = S.page + (forward ? 1 : -1);
|
|
870
|
+
t0.curl = to >= 0 && to < S.pageCount && !curlBusy ? curlBegin(to, forward, t0.y + S.G.top) : null;
|
|
871
|
+
}
|
|
872
|
+
if (t0.curl) {
|
|
873
|
+
t0.curl.p = Math.min(1, Math.abs(dx) / (S.G.W * 0.95));
|
|
874
|
+
curlApply(t0.curl, t0.curl.p);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/* ----------------------------------------- curl across chapter boundaries */
|
|
879
|
+
|
|
880
|
+
// write a chapter into one of the ghost iframes (used to show the previous chapter's last page)
|
|
881
|
+
function mountInto(frameEl, ch) {
|
|
882
|
+
var d = frameEl.contentDocument;
|
|
883
|
+
d.open();
|
|
884
|
+
d.write('<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>');
|
|
885
|
+
d.close();
|
|
886
|
+
d.documentElement.setAttribute('dir', S.rtl ? 'rtl' : 'ltr');
|
|
887
|
+
ch.css.forEach(function (t) {
|
|
888
|
+
var st = d.createElement('style');
|
|
889
|
+
st.textContent = t;
|
|
890
|
+
d.head.appendChild(st);
|
|
891
|
+
});
|
|
892
|
+
var app = d.createElement('style');
|
|
893
|
+
app.textContent = appCss(S.G);
|
|
894
|
+
d.head.appendChild(app);
|
|
895
|
+
Array.prototype.forEach.call(ch.body.attributes, function (a) {
|
|
896
|
+
if (a.name !== 'style' && a.name.indexOf('on') !== 0) d.body.setAttribute(a.name, a.value);
|
|
897
|
+
});
|
|
898
|
+
Array.prototype.forEach.call(ch.body.childNodes, function (n) {
|
|
899
|
+
d.body.appendChild(d.importNode(n, true));
|
|
900
|
+
});
|
|
901
|
+
return d;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async function loadGhostChapter(index, first) {
|
|
905
|
+
var ch = await parseChapter(index);
|
|
906
|
+
await inlineChapter(ch);
|
|
907
|
+
var d1 = mountInto(ghost, ch);
|
|
908
|
+
mountInto(ghost2, ch);
|
|
909
|
+
var imgs = Array.prototype.slice.call(d1.images || []).filter(function (im) { return !im.complete; })
|
|
910
|
+
.map(function (im) { return new Promise(function (r) { im.onload = im.onerror = r; }); });
|
|
911
|
+
if (d1.fonts && d1.fonts.ready) imgs.push(d1.fonts.ready);
|
|
912
|
+
if (imgs.length) await Promise.race([Promise.all(imgs), sleep(1500)]);
|
|
913
|
+
var G = S.G, count = Math.max(1, Math.ceil((d1.body.scrollWidth + G.gap) / (G.pageW + G.gap) - 0.01));
|
|
914
|
+
setGhostPages(first ? 0 : count - 1);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Turn from the last page of a chapter to the first of the next (forward), or the reverse.
|
|
918
|
+
async function curlChapter(index, forward) {
|
|
919
|
+
post('pageTurn', { mode: 'curl' });
|
|
920
|
+
S.ghostLock = true;
|
|
921
|
+
try {
|
|
922
|
+
var st;
|
|
923
|
+
if (forward) {
|
|
924
|
+
if (S.ghostDirty) { S.ghostLock = false; buildGhost(); S.ghostLock = true; }
|
|
925
|
+
setGhostPages(S.page);
|
|
926
|
+
st = curlBegin(index, true, null, true);
|
|
927
|
+
curlApply(st, 0);
|
|
928
|
+
await renderChapter(index, 'start'); // the new chapter loads underneath the old page
|
|
929
|
+
await curlAnimate(st, 0, 1, 900);
|
|
930
|
+
curlHide();
|
|
931
|
+
} else {
|
|
932
|
+
await loadGhostChapter(index); // previous chapter's last page goes into the ghosts
|
|
933
|
+
st = curlBegin(index, false, null, true);
|
|
934
|
+
curlApply(st, 0);
|
|
935
|
+
await curlAnimate(st, 0, 1, 900);
|
|
936
|
+
await renderChapter(index, 'end'); // load it underneath, then drop the cover
|
|
937
|
+
curlHide();
|
|
938
|
+
}
|
|
939
|
+
} finally {
|
|
940
|
+
S.ghostLock = false;
|
|
941
|
+
S.ghostDirty = true;
|
|
942
|
+
scheduleGhost();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/* ----------------------------------------------- slide: a sheet laid over another */
|
|
947
|
+
// Forward: the next page slides in over the current one (which moves back a little).
|
|
948
|
+
// Backward: the current page slides off and reveals the previous one underneath.
|
|
949
|
+
// The moving sheet is the ghost layer (a copy of the page); the real frame is the page underneath.
|
|
950
|
+
var slideBusy = false;
|
|
951
|
+
|
|
952
|
+
function slideBegin(to, forward, raw) {
|
|
953
|
+
var G = S.G, sign = S.rtl ? -1 : 1;
|
|
954
|
+
var st = { from: S.page, to: to, forward: forward, sign: sign, p: 0 };
|
|
955
|
+
if (!raw) {
|
|
956
|
+
if (S.ghostDirty) buildGhost();
|
|
957
|
+
if (forward) setGhostPage(to);
|
|
958
|
+
else { setGhostPage(S.page); showPage(to, false); }
|
|
959
|
+
}
|
|
960
|
+
syncLabels(oldPage);
|
|
961
|
+
oldPage.style.clipPath = 'none';
|
|
962
|
+
oldPage.style.webkitClipPath = 'none';
|
|
963
|
+
oldPage.style.transition = 'none';
|
|
964
|
+
oldPage.style.opacity = '1';
|
|
965
|
+
oldPage.style.boxShadow = (sign > 0 ? '-14px' : '14px') + ' 0 32px rgba(0,0,0,.38)';
|
|
966
|
+
oldPage.style.visibility = 'visible';
|
|
967
|
+
hdr.style.visibility = 'hidden';
|
|
968
|
+
ftr.style.visibility = 'hidden';
|
|
969
|
+
slideBusy = true;
|
|
970
|
+
slideApply(st, 0);
|
|
971
|
+
return st;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// p: 0 = start of the turn, 1 = finished
|
|
975
|
+
function slideApply(st, p) {
|
|
976
|
+
var W = S.G.W;
|
|
977
|
+
var sheet = st.forward ? st.sign * W * (1 - p) : st.sign * W * p;
|
|
978
|
+
var under = st.forward ? -st.sign * W * 0.22 * p : -st.sign * W * 0.22 * (1 - p);
|
|
979
|
+
oldPage.style.transform = 'translate3d(' + sheet.toFixed(1) + 'px,0,0)';
|
|
980
|
+
frame.style.transform = 'translate3d(' + under.toFixed(1) + 'px,0,0)';
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function slideHide() {
|
|
984
|
+
oldPage.style.visibility = 'hidden';
|
|
985
|
+
oldPage.style.transform = 'none';
|
|
986
|
+
oldPage.style.boxShadow = 'none';
|
|
987
|
+
frame.style.transform = 'none';
|
|
988
|
+
hdr.style.visibility = '';
|
|
989
|
+
ftr.style.visibility = '';
|
|
990
|
+
slideBusy = false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function slideEnd(st, commit) {
|
|
994
|
+
slideHide();
|
|
995
|
+
if (st.forward && commit) showPage(st.to, false);
|
|
996
|
+
if (!st.forward && !commit) showPage(st.from, false);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
|
|
1000
|
+
|
|
1001
|
+
function slideAnimate(st, p0, p1, dur) {
|
|
1002
|
+
return new Promise(function (resolve) {
|
|
1003
|
+
var t0 = null, done = false;
|
|
1004
|
+
function finish() { if (!done) { done = true; resolve(); } }
|
|
1005
|
+
function step(ts) {
|
|
1006
|
+
if (done) return;
|
|
1007
|
+
if (t0 == null) t0 = ts;
|
|
1008
|
+
var t = Math.min(1, (ts - t0) / dur);
|
|
1009
|
+
st.p = p0 + (p1 - p0) * easeOutCubic(t);
|
|
1010
|
+
slideApply(st, st.p);
|
|
1011
|
+
if (t < 1) requestAnimationFrame(step); else finish();
|
|
1012
|
+
}
|
|
1013
|
+
requestAnimationFrame(step);
|
|
1014
|
+
setTimeout(function () { if (done) return; st.p = p1; slideApply(st, p1); finish(); }, dur + 300);
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function slideTurn(to, forward) {
|
|
1019
|
+
post('pageTurn', { mode: 'slide' });
|
|
1020
|
+
if (S.ghostDirty) {
|
|
1021
|
+
buildGhost();
|
|
1022
|
+
var fonts = [ghost].map(function (g) { return g.contentDocument.fonts && g.contentDocument.fonts.ready; }).filter(Boolean);
|
|
1023
|
+
if (fonts.length) await Promise.race([Promise.all(fonts), sleep(300)]);
|
|
1024
|
+
}
|
|
1025
|
+
var st = slideBegin(to, forward, false);
|
|
1026
|
+
await slideAnimate(st, 0, 1, 420);
|
|
1027
|
+
slideEnd(st, true);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// finger-follow: called from touchmove while dragging in slide mode
|
|
1031
|
+
function slideDrag(t0, dx) {
|
|
1032
|
+
if (t0.slide === undefined) {
|
|
1033
|
+
var forward = S.rtl ? dx > 0 : dx < 0;
|
|
1034
|
+
var to = S.page + (forward ? 1 : -1);
|
|
1035
|
+
t0.slide = to >= 0 && to < S.pageCount && !slideBusy && !curlBusy ? slideBegin(to, forward, false) : null;
|
|
1036
|
+
}
|
|
1037
|
+
if (t0.slide) {
|
|
1038
|
+
t0.slide.p = Math.min(1, Math.abs(dx) / (S.G.W * 0.9));
|
|
1039
|
+
slideApply(t0.slide, t0.slide.p);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Slide across a chapter boundary
|
|
1044
|
+
async function slideChapter(index, forward) {
|
|
1045
|
+
post('pageTurn', { mode: 'slide' });
|
|
1046
|
+
S.ghostLock = true;
|
|
1047
|
+
try {
|
|
1048
|
+
var st;
|
|
1049
|
+
if (forward) {
|
|
1050
|
+
await loadGhostChapter(index, true); // first page of the next chapter is the sheet that slides in
|
|
1051
|
+
st = slideBegin(index, true, true);
|
|
1052
|
+
await slideAnimate(st, 0, 1, 420);
|
|
1053
|
+
await renderChapter(index, 'start');
|
|
1054
|
+
slideHide();
|
|
1055
|
+
} else {
|
|
1056
|
+
if (S.ghostDirty) { S.ghostLock = false; buildGhost(); S.ghostLock = true; }
|
|
1057
|
+
setGhostPages(S.page); // the current page is the sheet that slides off
|
|
1058
|
+
st = slideBegin(index, false, true);
|
|
1059
|
+
await renderChapter(index, 'end'); // previous chapter underneath
|
|
1060
|
+
await slideAnimate(st, 0, 1, 420);
|
|
1061
|
+
slideHide();
|
|
1062
|
+
}
|
|
1063
|
+
} finally {
|
|
1064
|
+
S.ghostLock = false;
|
|
1065
|
+
S.ghostDirty = true;
|
|
1066
|
+
scheduleGhost();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/* --------------------------------------------- continuous scrolling (Scroll mode) */
|
|
1071
|
+
// Chapters are stacked as sections inside the body; the next ones are loaded below the current one and the
|
|
1072
|
+
// previous one above it. Nothing is moved or rewritten while the user scrolls: "current chapter" simply
|
|
1073
|
+
// follows the middle of the screen (the page offsets are measured against that chapter's own section).
|
|
1074
|
+
|
|
1075
|
+
function rootEl() { return S.root || S.doc.body; }
|
|
1076
|
+
|
|
1077
|
+
function bodyTop() {
|
|
1078
|
+
var d = S.doc, el = d.scrollingElement || d.documentElement;
|
|
1079
|
+
return rootEl().getBoundingClientRect().top + el.scrollTop;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function clearExtras() { /* switching mode re-renders the chapter, so there is nothing to remove */ }
|
|
1083
|
+
|
|
1084
|
+
function applyHighlightsFor(el, index) {
|
|
1085
|
+
S.root = el;
|
|
1086
|
+
refreshSegs();
|
|
1087
|
+
S.highlights.filter(function (h) { return h.spine === index; })
|
|
1088
|
+
.sort(function (x, y) { return x.start - y.start; })
|
|
1089
|
+
.forEach(function (h) {
|
|
1090
|
+
var color = HL_COLORS[h.color] ? h.color : 'yellow';
|
|
1091
|
+
wrapRange(h.start, h.end, (h.style === 'underline' ? 'ul-' : 'hl-') + color, h.id);
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function mountExtra(index, where) {
|
|
1096
|
+
var token = S.token;
|
|
1097
|
+
var ch = await parseChapter(index);
|
|
1098
|
+
await inlineChapter(ch);
|
|
1099
|
+
if (token !== S.token || !S.doc || !S.G.scroll) return;
|
|
1100
|
+
if (S.sections.some(function (x) { return x.index === index; })) return;
|
|
1101
|
+
var d = S.doc, el = d.createElement('div');
|
|
1102
|
+
el.className = '__ch';
|
|
1103
|
+
el.setAttribute('data-spine', index);
|
|
1104
|
+
Array.prototype.forEach.call(ch.body.childNodes, function (n) { el.appendChild(d.importNode(n, true)); });
|
|
1105
|
+
ch.css.forEach(function (t) {
|
|
1106
|
+
if (S.cssSeen[t]) return;
|
|
1107
|
+
S.cssSeen[t] = true;
|
|
1108
|
+
var st = d.createElement('style');
|
|
1109
|
+
st.textContent = t;
|
|
1110
|
+
d.head.insertBefore(st, d.getElementById('app')); // before the app style so the theme still wins
|
|
1111
|
+
});
|
|
1112
|
+
var sc = d.scrollingElement || d.documentElement, cur = S.root;
|
|
1113
|
+
if (where === 'tail') {
|
|
1114
|
+
d.body.appendChild(el);
|
|
1115
|
+
S.sections.push({ index: index, el: el });
|
|
1116
|
+
} else {
|
|
1117
|
+
d.body.insertBefore(el, S.sections[0].el);
|
|
1118
|
+
S.sections.unshift({ index: index, el: el });
|
|
1119
|
+
sc.scrollTop += el.offsetHeight; // keep what is on screen where it is
|
|
1120
|
+
}
|
|
1121
|
+
applyHighlightsFor(el, index);
|
|
1122
|
+
S.root = cur;
|
|
1123
|
+
refreshSegs();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async function ensureExtras() {
|
|
1127
|
+
if (!S.doc || !S.G || !S.G.scroll || S.extraBusy || !S.sections || !S.sections.length) return;
|
|
1128
|
+
S.extraBusy = true;
|
|
1129
|
+
try {
|
|
1130
|
+
var last = S.sections[S.sections.length - 1].index;
|
|
1131
|
+
if (last < S.spine.length - 1 && last - S.cur < 2) await mountExtra(last + 1, 'tail');
|
|
1132
|
+
var first = S.sections[0].index;
|
|
1133
|
+
if (first > 0 && S.cur - first < 1) await mountExtra(first - 1, 'head');
|
|
1134
|
+
} catch (e) { /* neighbours are optional */ } finally { S.extraBusy = false; }
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
var extrasTimer;
|
|
1138
|
+
function scheduleExtras() {
|
|
1139
|
+
clearTimeout(extrasTimer);
|
|
1140
|
+
extrasTimer = setTimeout(ensureExtras, 350);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// follow the middle of the screen; true when the current chapter changed
|
|
1144
|
+
function checkSection() {
|
|
1145
|
+
var d = S.doc;
|
|
1146
|
+
if (!d || !S.G.scroll || !S.sections || !S.sections.length) return false;
|
|
1147
|
+
var sc = d.scrollingElement || d.documentElement, mid = sc.scrollTop + S.G.pageH / 2;
|
|
1148
|
+
for (var i = 0; i < S.sections.length; i++) {
|
|
1149
|
+
var sec = S.sections[i], r = sec.el.getBoundingClientRect(), top = r.top + sc.scrollTop;
|
|
1150
|
+
if (mid >= top && mid < top + r.height) {
|
|
1151
|
+
if (sec.index === S.cur) return false;
|
|
1152
|
+
setCurrentSection(sec);
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function setCurrentSection(sec) {
|
|
1160
|
+
var d = S.doc, sc = d.scrollingElement || d.documentElement;
|
|
1161
|
+
S.cur = sec.index;
|
|
1162
|
+
S.root = sec.el;
|
|
1163
|
+
refreshSegs();
|
|
1164
|
+
S.curText = segsText(S.segs);
|
|
1165
|
+
S.pageStarts = {};
|
|
1166
|
+
S.pageCount = Math.max(1, Math.ceil(sec.el.offsetHeight / S.G.pageH - 0.02));
|
|
1167
|
+
S.page = clamp(Math.round((sc.scrollTop - bodyTop()) / S.G.pageH), 0, S.pageCount - 1);
|
|
1168
|
+
autoLabels();
|
|
1169
|
+
relocate();
|
|
1170
|
+
scheduleExtras();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function goPage(to, forward) {
|
|
1174
|
+
var G = S.G;
|
|
1175
|
+
if (G.scroll) showPage(to, true);
|
|
1176
|
+
else if (G.mode === 'curl' || G.mode === 'fade') await turn(to, forward);
|
|
1177
|
+
else if (G.mode === 'slide') await slideTurn(to, forward);
|
|
1178
|
+
else showPage(to, true);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/* -------------------------------------------- offset <-> page geometry */
|
|
1182
|
+
|
|
1183
|
+
function segFor(o) {
|
|
1184
|
+
var lo = 0, hi = S.segs.length - 1;
|
|
1185
|
+
while (lo <= hi) {
|
|
1186
|
+
var mid = (lo + hi) >> 1, s = S.segs[mid];
|
|
1187
|
+
if (o < s.start) hi = mid - 1;
|
|
1188
|
+
else if (o >= s.end) lo = mid + 1;
|
|
1189
|
+
else return { node: s.node, pos: o - s.start };
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function pageOfRect(r) {
|
|
1195
|
+
var G = S.G, b = (G.scroll ? rootEl() : S.doc.body).getBoundingClientRect();
|
|
1196
|
+
if (G.scroll) return Math.floor((r.top - b.top + 2) / G.pageH);
|
|
1197
|
+
var sl = S.doc.body.scrollLeft;
|
|
1198
|
+
var rel = S.rtl ? b.right - r.right - sl : r.left - b.left + sl;
|
|
1199
|
+
return Math.floor((rel + 2) / (G.pageW + G.gap));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// page of the first renderable character at or after offset o (Infinity if none nearby)
|
|
1203
|
+
function pageAt(o) {
|
|
1204
|
+
var total = S.segs.total, end = Math.min(o + 400, total);
|
|
1205
|
+
for (var k = Math.max(0, o); k < end; k++) {
|
|
1206
|
+
var loc = segFor(k);
|
|
1207
|
+
if (!loc) continue;
|
|
1208
|
+
var r = S.doc.createRange();
|
|
1209
|
+
r.setStart(loc.node, loc.pos);
|
|
1210
|
+
r.setEnd(loc.node, loc.pos + 1);
|
|
1211
|
+
var rects = r.getClientRects();
|
|
1212
|
+
if (rects.length && rects[0].width + rects[0].height > 0) return pageOfRect(rects[0]);
|
|
1213
|
+
}
|
|
1214
|
+
return Infinity;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function pageStart(p) {
|
|
1218
|
+
var total = S.segs.total;
|
|
1219
|
+
if (p <= 0) return 0;
|
|
1220
|
+
if (p >= S.pageCount) return total;
|
|
1221
|
+
if (S.pageStarts[p] != null) return S.pageStarts[p];
|
|
1222
|
+
var lo = 0, hi = total;
|
|
1223
|
+
while (lo < hi) {
|
|
1224
|
+
var mid = (lo + hi) >> 1;
|
|
1225
|
+
if (pageAt(mid) >= p) hi = mid; else lo = mid + 1;
|
|
1226
|
+
}
|
|
1227
|
+
return (S.pageStarts[p] = lo);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function pageForOffset(o) {
|
|
1231
|
+
var p = pageAt(o);
|
|
1232
|
+
return p === Infinity ? S.pageCount - 1 : clamp(p, 0, S.pageCount - 1);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function boundaryOffset(node, off) {
|
|
1236
|
+
if (node.nodeType === 3) {
|
|
1237
|
+
var s = S.segMap.get(node);
|
|
1238
|
+
return s ? s.start + off : 0;
|
|
1239
|
+
}
|
|
1240
|
+
var ref = node.childNodes[off], pred;
|
|
1241
|
+
if (ref) {
|
|
1242
|
+
pred = function (sn) { return sn === ref || (ref.compareDocumentPosition(sn) & 4) !== 0; };
|
|
1243
|
+
} else {
|
|
1244
|
+
pred = function (sn) {
|
|
1245
|
+
var c = node.compareDocumentPosition(sn);
|
|
1246
|
+
return (c & 4) !== 0 && (c & 16) === 0;
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
var lo = 0, hi = S.segs.length;
|
|
1250
|
+
while (lo < hi) {
|
|
1251
|
+
var mid = (lo + hi) >> 1;
|
|
1252
|
+
if (pred(S.segs[mid].node)) hi = mid; else lo = mid + 1;
|
|
1253
|
+
}
|
|
1254
|
+
return lo < S.segs.length ? S.segs[lo].start : S.segs.total;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function rectOf(x) {
|
|
1258
|
+
var r = x.getBoundingClientRect();
|
|
1259
|
+
return { x: r.left + frame.offsetLeft, y: r.top + frame.offsetTop, width: r.width, height: r.height };
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/* ------------------------------------------------------------- progress */
|
|
1263
|
+
|
|
1264
|
+
function bookProgress(offset) {
|
|
1265
|
+
if (S.indexed && S.totalLen > 0) return clamp((S.cum[S.cur] + offset) / S.totalLen, 0, 1);
|
|
1266
|
+
return clamp((S.cur + S.page / S.pageCount) / S.spine.length, 0, 1);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function relocate() {
|
|
1270
|
+
var start = pageStart(S.page);
|
|
1271
|
+
var end = S.page + 1 >= S.pageCount ? S.segs.total : pageStart(S.page + 1);
|
|
1272
|
+
post('relocate', {
|
|
1273
|
+
spine: S.cur, offset: start, end: end,
|
|
1274
|
+
page: S.page, pageCount: S.pageCount, spineCount: S.spine.length,
|
|
1275
|
+
textLength: S.segs.total,
|
|
1276
|
+
progress: bookProgress(start), chapter: chapterTitle(S.cur),
|
|
1277
|
+
excerpt: S.curText.slice(start, start + 120).replace(/\s+/g, ' ').trim(),
|
|
1278
|
+
indexed: S.indexed
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/* ----------------------------------------------------------- navigation */
|
|
1283
|
+
|
|
1284
|
+
async function renderChapter(i, target) {
|
|
1285
|
+
i = clamp(i, 0, S.spine.length - 1);
|
|
1286
|
+
var token = ++S.token;
|
|
1287
|
+
clearHit(true);
|
|
1288
|
+
S.dataCache = {};
|
|
1289
|
+
var ch = await parseChapter(i);
|
|
1290
|
+
await inlineChapter(ch);
|
|
1291
|
+
if (token !== S.token) return;
|
|
1292
|
+
S.cur = i;
|
|
1293
|
+
S.sel = null;
|
|
1294
|
+
mountChapter(ch);
|
|
1295
|
+
await waitAssets();
|
|
1296
|
+
if (token !== S.token) return;
|
|
1297
|
+
layout();
|
|
1298
|
+
applyHighlightsDom();
|
|
1299
|
+
var p = 0;
|
|
1300
|
+
if (target === 'end') p = S.pageCount - 1;
|
|
1301
|
+
else if (target && target.offset != null) p = pageForOffset(target.offset);
|
|
1302
|
+
else if (target && target.fragment) p = pageOfFragment(target.fragment);
|
|
1303
|
+
showPage(p, false);
|
|
1304
|
+
relocate();
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function pageOfFragment(id) {
|
|
1308
|
+
var d = S.doc;
|
|
1309
|
+
var el = d.getElementById(id) || d.getElementsByName(id)[0];
|
|
1310
|
+
if (!el) return 0;
|
|
1311
|
+
var r = el.getBoundingClientRect();
|
|
1312
|
+
return clamp(pageOfRect(r), 0, S.pageCount - 1);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function next() {
|
|
1316
|
+
clearHit();
|
|
1317
|
+
if (S.page < S.pageCount - 1) { await goPage(S.page + 1, true); relocate(); }
|
|
1318
|
+
else if (S.cur < S.spine.length - 1) {
|
|
1319
|
+
if (S.G.mode === 'curl') await curlChapter(S.cur + 1, true);
|
|
1320
|
+
else if (S.G.mode === 'slide') await slideChapter(S.cur + 1, true);
|
|
1321
|
+
else await renderChapter(S.cur + 1, 'start');
|
|
1322
|
+
}
|
|
1323
|
+
else { showPage(S.page, true); post('end', { edge: 'end' }); }
|
|
1324
|
+
}
|
|
1325
|
+
async function prev() {
|
|
1326
|
+
clearHit();
|
|
1327
|
+
if (S.page > 0) { await goPage(S.page - 1, false); relocate(); }
|
|
1328
|
+
else if (S.cur > 0) {
|
|
1329
|
+
if (S.G.mode === 'curl') await curlChapter(S.cur - 1, false);
|
|
1330
|
+
else if (S.G.mode === 'slide') await slideChapter(S.cur - 1, false);
|
|
1331
|
+
else await renderChapter(S.cur - 1, 'end');
|
|
1332
|
+
}
|
|
1333
|
+
else { showPage(S.page, true); post('end', { edge: 'start' }); }
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function gotoLocation(loc) {
|
|
1337
|
+
loc = loc || {};
|
|
1338
|
+
if (loc.progress != null && loc.spine == null) return gotoProgress(loc.progress);
|
|
1339
|
+
var spine = loc.spine != null ? clamp(loc.spine, 0, S.spine.length - 1) : 0;
|
|
1340
|
+
var target = loc.fragment ? { fragment: loc.fragment } : { offset: loc.offset || 0 };
|
|
1341
|
+
if (spine === S.cur && S.doc) {
|
|
1342
|
+
clearHit();
|
|
1343
|
+
showPage(target.fragment ? pageOfFragment(target.fragment) : pageForOffset(target.offset), false);
|
|
1344
|
+
relocate();
|
|
1345
|
+
} else await renderChapter(spine, target);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async function gotoProgress(p) {
|
|
1349
|
+
p = clamp(p, 0, 1);
|
|
1350
|
+
if (!S.indexed) { await renderChapter(Math.floor(p * (S.spine.length - 1)), 'start'); return; }
|
|
1351
|
+
var t = p * S.totalLen, i = 0;
|
|
1352
|
+
while (i < S.spine.length - 1 && S.cum[i + 1] <= t) i++;
|
|
1353
|
+
await renderChapter(i, { offset: Math.floor(t - S.cum[i]) });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/* ---------------------------------------------------------- settings */
|
|
1357
|
+
|
|
1358
|
+
function applySettings(partial) {
|
|
1359
|
+
var s = S.settings, dirBefore = s.direction;
|
|
1360
|
+
// anchor is measured with the layout that is still on screen
|
|
1361
|
+
var anchor = S.doc ? pageStart(S.page) : 0;
|
|
1362
|
+
Object.keys(partial || {}).forEach(function (k) {
|
|
1363
|
+
if (k === 'insets') s.insets = { top: partial.insets.top, bottom: partial.insets.bottom };
|
|
1364
|
+
else if (partial[k] != null) s[k] = partial[k];
|
|
1365
|
+
});
|
|
1366
|
+
if (s.direction !== dirBefore && S.spine.length) {
|
|
1367
|
+
var lang = S.meta.language.toLowerCase().split('-')[0];
|
|
1368
|
+
S.rtl = s.direction === 'rtl' ? true : s.direction === 'ltr' ? false : RTL_LANGS.indexOf(lang) >= 0;
|
|
1369
|
+
if (S.doc) { renderChapter(S.cur, { offset: anchor }); return; }
|
|
1370
|
+
}
|
|
1371
|
+
if (S.doc && S.G && S.G.scroll !== (s.mode === 'scroll')) {
|
|
1372
|
+
renderChapter(S.cur, { offset: anchor });
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (S.doc) {
|
|
1376
|
+
layout();
|
|
1377
|
+
showPage(pageForOffset(anchor), false);
|
|
1378
|
+
relocate();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function relayout() {
|
|
1383
|
+
var anchor = pageStart(S.page);
|
|
1384
|
+
layout();
|
|
1385
|
+
showPage(pageForOffset(anchor), false);
|
|
1386
|
+
relocate();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/* ----------------------------------------------------------- highlights */
|
|
1390
|
+
|
|
1391
|
+
function unwrapMarks(selector) {
|
|
1392
|
+
var marks = Array.prototype.slice.call(S.doc.querySelectorAll(selector));
|
|
1393
|
+
var parents = [];
|
|
1394
|
+
marks.forEach(function (m) {
|
|
1395
|
+
var p = m.parentNode;
|
|
1396
|
+
while (m.firstChild) p.insertBefore(m.firstChild, m);
|
|
1397
|
+
p.removeChild(m);
|
|
1398
|
+
parents.push(p);
|
|
1399
|
+
});
|
|
1400
|
+
parents.forEach(function (p) { if (p.parentNode) p.normalize(); });
|
|
1401
|
+
return marks.length;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function wrapRange(a, b, cls, id) {
|
|
1405
|
+
var items = [];
|
|
1406
|
+
S.segs.forEach(function (seg) {
|
|
1407
|
+
if (seg.end <= a || seg.start >= b) return;
|
|
1408
|
+
var la = Math.max(a, seg.start) - seg.start, lb = Math.min(b, seg.end) - seg.start;
|
|
1409
|
+
if (lb <= la) return;
|
|
1410
|
+
if (!seg.node.data.slice(la, lb).trim()) {
|
|
1411
|
+
var disp = seg.node.parentNode && S.win.getComputedStyle(seg.node.parentNode).display;
|
|
1412
|
+
if (!disp || disp.indexOf('inline') !== 0) return;
|
|
1413
|
+
}
|
|
1414
|
+
items.push({ node: seg.node, la: la, lb: lb });
|
|
1415
|
+
});
|
|
1416
|
+
items.forEach(function (it) {
|
|
1417
|
+
var mid = it.node;
|
|
1418
|
+
if (it.la > 0) mid = mid.splitText(it.la);
|
|
1419
|
+
if (it.lb - it.la < mid.data.length) mid.splitText(it.lb - it.la);
|
|
1420
|
+
var m = S.doc.createElement('mark');
|
|
1421
|
+
m.className = cls;
|
|
1422
|
+
m.setAttribute('data-hl', id);
|
|
1423
|
+
mid.parentNode.insertBefore(m, mid);
|
|
1424
|
+
m.appendChild(mid);
|
|
1425
|
+
});
|
|
1426
|
+
refreshSegs();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function applyHighlightsDom() {
|
|
1430
|
+
unwrapMarks('mark[data-hl]:not(.hit)');
|
|
1431
|
+
var cur = S.root;
|
|
1432
|
+
if (S.sections && S.sections.length) S.sections.forEach(function (sec) { applyHighlightsFor(sec.el, sec.index); });
|
|
1433
|
+
else applyHighlightsFor(S.doc.body, S.cur);
|
|
1434
|
+
S.root = cur;
|
|
1435
|
+
refreshSegs();
|
|
1436
|
+
S.ghostDirty = true;
|
|
1437
|
+
scheduleGhost();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function showHit(offset, length) {
|
|
1441
|
+
clearHit(true);
|
|
1442
|
+
wrapRange(offset, offset + length, 'hit', '__hit');
|
|
1443
|
+
}
|
|
1444
|
+
function clearHit(skipRefresh) {
|
|
1445
|
+
if (!S.doc) return;
|
|
1446
|
+
if (unwrapMarks('mark.hit') && !skipRefresh) refreshSegs();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/* ------------------------------------------------------------- search */
|
|
1450
|
+
|
|
1451
|
+
var FOLD = { 'ي': 'ی', 'ك': 'ک', 'ة': 'ه', 'ى': 'ی', 'أ': 'ا', 'إ': 'ا', 'آ': 'ا', 'ۀ': 'ه', 'ہ': 'ه', 'ە': 'ه' };
|
|
1452
|
+
function fold(s) {
|
|
1453
|
+
return s.toLowerCase().replace(/[يكةىأإآۀہە]/g, function (c) { return FOLD[c]; });
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async function search(query, id) {
|
|
1457
|
+
S.searchId = id;
|
|
1458
|
+
var q = fold(query.trim());
|
|
1459
|
+
if (q.length < 2) { post('searchResults', { id: id, results: [], done: true }); return; }
|
|
1460
|
+
while (!S.indexed) {
|
|
1461
|
+
if (S.searchId !== id) return;
|
|
1462
|
+
await sleep(80);
|
|
1463
|
+
}
|
|
1464
|
+
var total = 0;
|
|
1465
|
+
for (var i = 0; i < S.spine.length; i++) {
|
|
1466
|
+
if (S.searchId !== id) return;
|
|
1467
|
+
var text = S.texts[i], folded = fold(text), from = 0, found = [], at;
|
|
1468
|
+
while ((at = folded.indexOf(q, from)) >= 0 && total + found.length < 500) {
|
|
1469
|
+
found.push({
|
|
1470
|
+
spine: i, offset: at, length: q.length, chapter: chapterTitle(i),
|
|
1471
|
+
before: text.slice(Math.max(0, at - 40), at).replace(/\s+/g, ' '),
|
|
1472
|
+
match: text.slice(at, at + q.length),
|
|
1473
|
+
after: text.slice(at + q.length, at + q.length + 60).replace(/\s+/g, ' ')
|
|
1474
|
+
});
|
|
1475
|
+
from = at + q.length;
|
|
1476
|
+
}
|
|
1477
|
+
total += found.length;
|
|
1478
|
+
post('searchResults', { id: id, results: found, done: i === S.spine.length - 1 || total >= 500 });
|
|
1479
|
+
if (total >= 500) return;
|
|
1480
|
+
if (i % 4 === 3) await sleep(0);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async function buildIndex() {
|
|
1485
|
+
var cum = 0, id = S.openId;
|
|
1486
|
+
var texts = [], cums = [];
|
|
1487
|
+
for (var i = 0; i < S.spine.length; i++) {
|
|
1488
|
+
var ch = await parseChapter(i);
|
|
1489
|
+
if (id !== S.openId) return;
|
|
1490
|
+
var t = segsText(collectSegs(ch.body));
|
|
1491
|
+
texts[i] = t; cums[i] = cum; cum += t.length;
|
|
1492
|
+
if (i % 3 === 2) await sleep(0);
|
|
1493
|
+
}
|
|
1494
|
+
if (id !== S.openId) return;
|
|
1495
|
+
S.texts = texts; S.cum = cums;
|
|
1496
|
+
S.totalLen = cum;
|
|
1497
|
+
S.indexed = true;
|
|
1498
|
+
post('indexed', { totalLength: cum, cum: S.cum });
|
|
1499
|
+
if (S.doc) relocate();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/* ------------------------------------------------------------- events */
|
|
1503
|
+
|
|
1504
|
+
function handleLink(a) {
|
|
1505
|
+
var href = a.getAttribute('href') || '';
|
|
1506
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) { post('link', { href: href }); return; }
|
|
1507
|
+
var frag = href.indexOf('#') >= 0 ? href.split('#')[1] : '';
|
|
1508
|
+
var host = a.closest && a.closest('.__ch');
|
|
1509
|
+
var path = resolvePath(S.spine[host ? +host.getAttribute('data-spine') : S.cur].path, href);
|
|
1510
|
+
var idx = path ? spineIndexOfPath(path) : -1;
|
|
1511
|
+
if (idx < 0) return;
|
|
1512
|
+
queue = queue.then(function () { return gotoLocation({ spine: idx, fragment: frag, offset: 0 }); });
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function attachListeners(d) {
|
|
1516
|
+
d.addEventListener('click', function (e) {
|
|
1517
|
+
if (S.dragged) return;
|
|
1518
|
+
var sel = d.getSelection();
|
|
1519
|
+
if (sel && !sel.isCollapsed) return;
|
|
1520
|
+
var t = e.target, a = t.closest && t.closest('a'), m = t.closest && t.closest('mark[data-hl]');
|
|
1521
|
+
if (a) { e.preventDefault(); handleLink(a); return; }
|
|
1522
|
+
if (m && m.getAttribute('data-hl') !== '__hit') {
|
|
1523
|
+
post('highlightTap', { id: m.getAttribute('data-hl'), rect: rectOf(m) });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
var x = (e.clientX + S.G.mx) / S.G.W;
|
|
1527
|
+
var zone = S.G.scroll || (x >= 0.25 && x <= 0.75) ? 'center' : (x < 0.25 ? 'left' : 'right');
|
|
1528
|
+
if (zone === 'center') { clearHit(); post('tap', { zone: 'center' }); return; }
|
|
1529
|
+
var forward = (zone === 'right') !== S.rtl;
|
|
1530
|
+
queue = queue.then(forward ? next : prev);
|
|
1531
|
+
}, true);
|
|
1532
|
+
|
|
1533
|
+
var t0 = null;
|
|
1534
|
+
d.addEventListener('touchstart', function (e) {
|
|
1535
|
+
if (e.touches.length !== 1) { t0 = null; return; }
|
|
1536
|
+
var t = e.touches[0], el = d.scrollingElement || d.documentElement;
|
|
1537
|
+
t0 = { x: t.clientX, y: t.clientY, time: Date.now(), drag: false, dx: 0, dy: 0, base: currentX(), top: el.scrollTop };
|
|
1538
|
+
}, { passive: true });
|
|
1539
|
+
d.addEventListener('touchmove', function (e) {
|
|
1540
|
+
if (!t0) return;
|
|
1541
|
+
var sel = d.getSelection();
|
|
1542
|
+
if (sel && !sel.isCollapsed) { t0 = null; return; }
|
|
1543
|
+
var t = e.touches[0], dx = t.clientX - t0.x, dy = t.clientY - t0.y;
|
|
1544
|
+
t0.dx = dx; t0.dy = dy;
|
|
1545
|
+
if (S.G.scroll) return;
|
|
1546
|
+
if (!t0.drag) {
|
|
1547
|
+
if (Math.abs(dx) > 10 && Math.abs(dx) > Math.abs(dy) * 1.5) t0.drag = true; else return;
|
|
1548
|
+
}
|
|
1549
|
+
e.preventDefault();
|
|
1550
|
+
if (S.G.mode === 'slide') slideDrag(t0, dx);
|
|
1551
|
+
else if (S.G.mode === 'curl') curlDrag(t0, dx);
|
|
1552
|
+
}, { passive: false });
|
|
1553
|
+
d.addEventListener('touchend', function () {
|
|
1554
|
+
var tt = t0;
|
|
1555
|
+
t0 = null;
|
|
1556
|
+
if (!tt) return;
|
|
1557
|
+
var G = S.G;
|
|
1558
|
+
if (G.scroll) {
|
|
1559
|
+
// keep scrolling into the next/previous chapter when the edge is already reached
|
|
1560
|
+
var el = d.scrollingElement || d.documentElement;
|
|
1561
|
+
var still = Math.abs(el.scrollTop - tt.top) < 3;
|
|
1562
|
+
if (still && Math.abs(tt.dy) > 60) {
|
|
1563
|
+
if (tt.dy < 0 && el.scrollTop + G.pageH >= el.scrollHeight - 2) queue = queue.then(next);
|
|
1564
|
+
else if (tt.dy > 0 && el.scrollTop <= 1) queue = queue.then(prev);
|
|
1565
|
+
}
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (!tt.drag) return;
|
|
1569
|
+
if (tt.slide) {
|
|
1570
|
+
var ss = tt.slide;
|
|
1571
|
+
S.dragged = true;
|
|
1572
|
+
setTimeout(function () { S.dragged = false; }, 350);
|
|
1573
|
+
var sfast = Math.abs(tt.dx) / Math.max(1, Date.now() - tt.time) > 0.5 && Math.abs(tt.dx) > 30;
|
|
1574
|
+
var scommit = ss.p > 0.28 || sfast;
|
|
1575
|
+
if (scommit) post('pageTurn', { mode: 'slide' });
|
|
1576
|
+
queue = queue.then(async function () {
|
|
1577
|
+
await slideAnimate(ss, ss.p, scommit ? 1 : 0, scommit ? 140 + 280 * (1 - ss.p) : 120 + 220 * ss.p);
|
|
1578
|
+
slideEnd(ss, scommit);
|
|
1579
|
+
if (scommit) relocate();
|
|
1580
|
+
});
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (tt.curl) {
|
|
1584
|
+
var st = tt.curl;
|
|
1585
|
+
S.dragged = true;
|
|
1586
|
+
setTimeout(function () { S.dragged = false; }, 350);
|
|
1587
|
+
var fast = Math.abs(tt.dx) / Math.max(1, Date.now() - tt.time) > 0.5 && Math.abs(tt.dx) > 30;
|
|
1588
|
+
var commit = st.p > 0.3 || fast;
|
|
1589
|
+
if (commit) post('pageTurn', { mode: 'curl' });
|
|
1590
|
+
queue = queue.then(async function () {
|
|
1591
|
+
await curlAnimate(st, st.p, commit ? 1 : 0, commit ? 260 + 560 * (1 - st.p) : 200 + 300 * st.p);
|
|
1592
|
+
curlEnd(st, commit);
|
|
1593
|
+
if (commit) relocate();
|
|
1594
|
+
});
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
var dx = tt.dx, dt = Math.max(1, Date.now() - tt.time);
|
|
1598
|
+
var go = Math.abs(dx) > G.W * 0.2 || (Math.abs(dx) / dt > 0.5 && Math.abs(dx) > 30);
|
|
1599
|
+
S.dragged = true;
|
|
1600
|
+
setTimeout(function () { S.dragged = false; }, 350);
|
|
1601
|
+
if (!go) { if (G.mode === 'slide') showPage(S.page, true); return; }
|
|
1602
|
+
var forward = S.rtl ? dx > 0 : dx < 0;
|
|
1603
|
+
queue = queue.then(forward ? next : prev);
|
|
1604
|
+
}, { passive: true });
|
|
1605
|
+
|
|
1606
|
+
d.defaultView.addEventListener('scroll', debounce(function () {
|
|
1607
|
+
if (d !== S.doc || !S.G.scroll) return;
|
|
1608
|
+
if (checkSection()) return;
|
|
1609
|
+
var el = d.scrollingElement || d.documentElement;
|
|
1610
|
+
var p = clamp(Math.round((el.scrollTop - bodyTop()) / S.G.pageH), 0, S.pageCount - 1);
|
|
1611
|
+
if (p !== S.page) { S.page = p; autoLabels(); relocate(); }
|
|
1612
|
+
}, 120));
|
|
1613
|
+
|
|
1614
|
+
d.addEventListener('selectionchange', debounce(function () {
|
|
1615
|
+
if (d !== S.doc) return;
|
|
1616
|
+
var sel = d.getSelection();
|
|
1617
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
|
|
1618
|
+
if (S.sel) { S.sel = null; post('selectionClear'); }
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
var r = sel.getRangeAt(0);
|
|
1622
|
+
if (!rootEl().contains(r.startContainer) || !rootEl().contains(r.endContainer)) return;
|
|
1623
|
+
var a = boundaryOffset(r.startContainer, r.startOffset), b = boundaryOffset(r.endContainer, r.endOffset);
|
|
1624
|
+
if (b <= a) return;
|
|
1625
|
+
S.sel = { spine: S.cur, start: a, end: b, text: sel.toString() };
|
|
1626
|
+
post('selection', { spine: S.cur, start: a, end: b, text: S.sel.text, rect: rectOf(r) });
|
|
1627
|
+
}, 180));
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/* ------------------------------------------------------------ commands */
|
|
1631
|
+
|
|
1632
|
+
var queue = Promise.resolve();
|
|
1633
|
+
|
|
1634
|
+
async function handle(c) {
|
|
1635
|
+
switch (c.cmd) {
|
|
1636
|
+
case 'open':
|
|
1637
|
+
S.openId = (S.openId || 0) + 1;
|
|
1638
|
+
S.token++;
|
|
1639
|
+
S.cur = -1;
|
|
1640
|
+
S.page = 0;
|
|
1641
|
+
S.pageCount = 1;
|
|
1642
|
+
S.doc = null;
|
|
1643
|
+
S.win = null;
|
|
1644
|
+
S.segs = [];
|
|
1645
|
+
S.curText = '';
|
|
1646
|
+
S.pageStarts = {};
|
|
1647
|
+
S.texts = [];
|
|
1648
|
+
S.cum = [];
|
|
1649
|
+
S.totalLen = 0;
|
|
1650
|
+
S.indexed = false;
|
|
1651
|
+
S.sel = null;
|
|
1652
|
+
S.searchId = -1;
|
|
1653
|
+
applySettings(c.settings);
|
|
1654
|
+
S.highlights = c.highlights || [];
|
|
1655
|
+
await openBook(c.data);
|
|
1656
|
+
buildIndex();
|
|
1657
|
+
post('loaded', { spineCount: S.spine.length });
|
|
1658
|
+
await gotoLocation(c.location);
|
|
1659
|
+
break;
|
|
1660
|
+
case 'settings': applySettings(c.settings); break;
|
|
1661
|
+
case 'labels':
|
|
1662
|
+
S.labelsExternal = true;
|
|
1663
|
+
hdr.textContent = c.header || '';
|
|
1664
|
+
ftr.textContent = c.footer || '';
|
|
1665
|
+
break;
|
|
1666
|
+
case 'highlights':
|
|
1667
|
+
S.highlights = c.list || [];
|
|
1668
|
+
if (S.doc) { applyHighlightsDom(); }
|
|
1669
|
+
break;
|
|
1670
|
+
case 'next': await next(); break;
|
|
1671
|
+
case 'prev': await prev(); break;
|
|
1672
|
+
case 'goto': await gotoLocation(c.location); break;
|
|
1673
|
+
case 'showHit': if (S.doc) showHit(c.offset, c.length); break;
|
|
1674
|
+
case 'clearSelection':
|
|
1675
|
+
if (S.doc) S.doc.getSelection().removeAllRanges();
|
|
1676
|
+
S.sel = null;
|
|
1677
|
+
break;
|
|
1678
|
+
case 'search': search(c.query, c.id); break;
|
|
1679
|
+
case 'cancelSearch': S.searchId = c.id || -1; break;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
window.__cmd = function (str) {
|
|
1684
|
+
var c;
|
|
1685
|
+
try { c = JSON.parse(str); } catch (e) { return; }
|
|
1686
|
+
queue = queue.then(function () { return handle(c); }).catch(function (err) {
|
|
1687
|
+
post('error', { message: String((err && err.message) || err) });
|
|
1688
|
+
});
|
|
1689
|
+
};
|
|
1690
|
+
window.addEventListener('message', function (e) { if (typeof e.data === 'string') window.__cmd(e.data); });
|
|
1691
|
+
document.addEventListener('message', function (e) { if (typeof e.data === 'string') window.__cmd(e.data); });
|
|
1692
|
+
window.addEventListener('resize', debounce(function () { if (S.doc) relayout(); }, 150));
|
|
1693
|
+
window.__reader = S; // debugging aid
|
|
1694
|
+
|
|
1695
|
+
post('ready');
|
|
1696
|
+
})();
|