@aseity/idocs-preview 0.3.0 → 0.5.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/CHANGELOG.md +12 -0
- package/README.md +59 -3
- package/dist/core/display.d.ts +90 -0
- package/dist/core/editor.d.ts +180 -0
- package/dist/core/editor.js +533 -0
- package/dist/core/engine.js +132 -38
- package/dist/core/index.d.ts +6 -19
- package/dist/core/index.js +13 -17
- package/dist/core/protocol.d.ts +87 -0
- package/dist/core/protocol.js +13 -1
- package/dist/view/edit-selection.d.ts +82 -0
- package/dist/view/edit-selection.js +786 -0
- package/dist/view/editor.d.ts +78 -0
- package/dist/view/editor.js +1485 -0
- package/dist/view/find.d.ts +2 -1
- package/dist/view/find.js +7 -0
- package/dist/view/ime.d.ts +63 -0
- package/dist/view/ime.js +352 -0
- package/dist/view/index.d.ts +4 -0
- package/dist/view/index.js +3 -0
- package/dist/view/selection-layer.d.ts +1 -0
- package/dist/view/selection-layer.js +4 -4
- package/dist/view/workspace-styles.d.ts +1 -0
- package/dist/view/workspace-styles.js +939 -0
- package/dist/view/workspace.d.ts +77 -0
- package/dist/view/workspace.js +1121 -0
- package/package.json +8 -3
- package/wasm/idocs_wasm.d.ts +30 -0
- package/wasm/idocs_wasm.js +147 -0
- package/wasm/idocs_wasm_bg.wasm +0 -0
- package/wasm/idocs_wasm_bg.wasm.d.ts +13 -0
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
import { canvasFont, CANVAS_TEXT_LANGUAGE } from '../core/fonts.js';
|
|
2
|
+
import { EditDocumentIndex, orderedAnchors } from './edit-selection.js';
|
|
3
|
+
import { createFindBar } from './find-bar.js';
|
|
4
|
+
import { Finder } from './find.js';
|
|
5
|
+
import { createImeBridge } from './ime.js';
|
|
6
|
+
import { decodeImages, paintPage } from './index.js';
|
|
7
|
+
import { paintHighlightBoxes } from './selection-layer.js';
|
|
8
|
+
import { TextIndex, turnedToPage } from './selection.js';
|
|
9
|
+
import { anchorAt, canvasResolution, currentPage, fitWidthZoom, layoutPages, pagesToKeep, PX_PER_PT, revealScroll, scrollTopFor, } from './viewport.js';
|
|
10
|
+
const SPACING = { gap: 16, padding: 16 };
|
|
11
|
+
const SELECTION_HIGHLIGHT = 'rgba(37, 99, 235, 0.22)';
|
|
12
|
+
const MATCH_HIGHLIGHT = 'rgba(255, 200, 0, 0.4)';
|
|
13
|
+
const CURRENT_MATCH_HIGHLIGHT = 'rgba(255, 130, 0, 0.55)';
|
|
14
|
+
const READONLY_FILL = 'rgba(217, 119, 6, 0.16)';
|
|
15
|
+
const READONLY_STROKE = '#d97706';
|
|
16
|
+
const DEFAULT_TEXT_COLOR = '#000';
|
|
17
|
+
const DRAG_EDGE = 16;
|
|
18
|
+
function clampZoom(zoom) {
|
|
19
|
+
return Number.isFinite(zoom) && zoom > 0 ? Math.min(10, Math.max(0.1, zoom)) : 1;
|
|
20
|
+
}
|
|
21
|
+
function element(tag, className) {
|
|
22
|
+
const node = document.createElement(tag);
|
|
23
|
+
node.className = className;
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
function freeCanvas(canvas) {
|
|
27
|
+
canvas.width = 0;
|
|
28
|
+
canvas.height = 0;
|
|
29
|
+
canvas.remove();
|
|
30
|
+
}
|
|
31
|
+
function isCollapsedRange(range) {
|
|
32
|
+
return range.start.flow === range.end.flow && range.start.offset === range.end.offset;
|
|
33
|
+
}
|
|
34
|
+
function sameAnchorValue(a, b) {
|
|
35
|
+
return a.session === b.session && a.flow === b.flow && a.offset === b.offset && a.affinity === b.affinity;
|
|
36
|
+
}
|
|
37
|
+
function sameSelection(a, b) {
|
|
38
|
+
if (a === b)
|
|
39
|
+
return true;
|
|
40
|
+
if (!a || !b)
|
|
41
|
+
return false;
|
|
42
|
+
return (a.revision === b.revision &&
|
|
43
|
+
a.reversed === b.reversed &&
|
|
44
|
+
a.range.revision === b.range.revision &&
|
|
45
|
+
sameAnchorValue(a.range.start, b.range.start) &&
|
|
46
|
+
sameAnchorValue(a.range.end, b.range.end));
|
|
47
|
+
}
|
|
48
|
+
function activeHead(selection) {
|
|
49
|
+
return selection.reversed ? selection.range.start : selection.range.end;
|
|
50
|
+
}
|
|
51
|
+
function anchorPoint(selection) {
|
|
52
|
+
return selection.reversed ? selection.range.end : selection.range.start;
|
|
53
|
+
}
|
|
54
|
+
function sameImageList(a, b) {
|
|
55
|
+
if (!a || a.length !== b.length)
|
|
56
|
+
return false;
|
|
57
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
58
|
+
if (a[i].missing !== b[i].missing ||
|
|
59
|
+
a[i].format !== b[i].format ||
|
|
60
|
+
a[i].washout !== b[i].washout ||
|
|
61
|
+
a[i].bytes !== b[i].bytes) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
function boxPageBounds(box) {
|
|
68
|
+
if (!box.turn)
|
|
69
|
+
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom };
|
|
70
|
+
const [x1, y1] = turnedToPage(box.turn, box.left, box.top);
|
|
71
|
+
const [x2, y2] = turnedToPage(box.turn, box.right, box.bottom);
|
|
72
|
+
return {
|
|
73
|
+
left: Math.min(x1, x2),
|
|
74
|
+
right: Math.max(x1, x2),
|
|
75
|
+
top: Math.min(y1, y2),
|
|
76
|
+
bottom: Math.max(y1, y2),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function caretPageBounds(caret) {
|
|
80
|
+
if (!caret.turn)
|
|
81
|
+
return { left: caret.x, right: caret.x, top: caret.top, bottom: caret.bottom };
|
|
82
|
+
const [x1, y1] = turnedToPage(caret.turn, caret.x, caret.top);
|
|
83
|
+
const [x2, y2] = turnedToPage(caret.turn, caret.x, caret.bottom);
|
|
84
|
+
return {
|
|
85
|
+
left: Math.min(x1, x2),
|
|
86
|
+
right: Math.max(x1, x2),
|
|
87
|
+
top: Math.min(y1, y2),
|
|
88
|
+
bottom: Math.max(y1, y2),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function roundedPath(ctx, x, y, w, h, r) {
|
|
92
|
+
const radius = Math.min(r, w / 2, h / 2);
|
|
93
|
+
ctx.beginPath();
|
|
94
|
+
ctx.moveTo(x + radius, y);
|
|
95
|
+
ctx.lineTo(x + w - radius, y);
|
|
96
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
|
97
|
+
ctx.lineTo(x + w, y + h - radius);
|
|
98
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
|
99
|
+
ctx.lineTo(x + radius, y + h);
|
|
100
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
|
101
|
+
ctx.lineTo(x, y + radius);
|
|
102
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
103
|
+
ctx.closePath();
|
|
104
|
+
}
|
|
105
|
+
function paintReadOnlyBox(ctx, box) {
|
|
106
|
+
ctx.save();
|
|
107
|
+
if (box.turn)
|
|
108
|
+
ctx.transform(0, 1, -1, 0, box.turn[0], box.turn[1]);
|
|
109
|
+
const w = Math.max(4, box.right - box.left);
|
|
110
|
+
const h = Math.max(4, box.bottom - box.top);
|
|
111
|
+
roundedPath(ctx, box.left, box.top, w, h, 2);
|
|
112
|
+
ctx.fillStyle = READONLY_FILL;
|
|
113
|
+
ctx.fill();
|
|
114
|
+
ctx.strokeStyle = READONLY_STROKE;
|
|
115
|
+
ctx.lineWidth = 0.9;
|
|
116
|
+
ctx.setLineDash([3, 2]);
|
|
117
|
+
ctx.stroke();
|
|
118
|
+
ctx.restore();
|
|
119
|
+
}
|
|
120
|
+
function paintSelectedImageBox(ctx, box) {
|
|
121
|
+
ctx.save();
|
|
122
|
+
ctx.strokeStyle = '#0071e3';
|
|
123
|
+
ctx.lineWidth = 1.5;
|
|
124
|
+
ctx.strokeRect(box.x, box.y, Math.max(4, box.width), Math.max(4, box.height));
|
|
125
|
+
const handle = 5;
|
|
126
|
+
const corners = [
|
|
127
|
+
[box.x, box.y],
|
|
128
|
+
[box.x + box.width, box.y],
|
|
129
|
+
[box.x, box.y + box.height],
|
|
130
|
+
[box.x + box.width, box.y + box.height],
|
|
131
|
+
];
|
|
132
|
+
ctx.fillStyle = '#ffffff';
|
|
133
|
+
for (const [cx, cy] of corners) {
|
|
134
|
+
ctx.fillRect(cx - handle / 2, cy - handle / 2, handle, handle);
|
|
135
|
+
ctx.strokeRect(cx - handle / 2, cy - handle / 2, handle, handle);
|
|
136
|
+
}
|
|
137
|
+
ctx.restore();
|
|
138
|
+
}
|
|
139
|
+
function intersectRect(a, b) {
|
|
140
|
+
if (!a)
|
|
141
|
+
return b;
|
|
142
|
+
const left = Math.max(a.x, b.x);
|
|
143
|
+
const top = Math.max(a.y, b.y);
|
|
144
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
145
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
146
|
+
return {
|
|
147
|
+
x: left,
|
|
148
|
+
y: top,
|
|
149
|
+
width: Math.max(0, right - left),
|
|
150
|
+
height: Math.max(0, bottom - top),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function isSelectableBlockImagePath(path) {
|
|
154
|
+
return path.length > 0 && !path.includes('.text.');
|
|
155
|
+
}
|
|
156
|
+
function hitImageInItems(items, x, y, clip) {
|
|
157
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
158
|
+
const item = items[i];
|
|
159
|
+
if (item.kind === 'clip') {
|
|
160
|
+
const nextClip = intersectRect(clip, item.rect);
|
|
161
|
+
if (x < nextClip.x || x > nextClip.x + nextClip.width || y < nextClip.y || y > nextClip.y + nextClip.height) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const nested = hitImageInItems(item.items, x, y, nextClip);
|
|
165
|
+
if (nested)
|
|
166
|
+
return nested;
|
|
167
|
+
}
|
|
168
|
+
else if (item.kind === 'image' && isSelectableBlockImagePath(item.path)) {
|
|
169
|
+
const box = intersectRect(clip, item);
|
|
170
|
+
if (x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height) {
|
|
171
|
+
return { path: item.path, box };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function findImageBoxByPath(items, path, clip) {
|
|
178
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
179
|
+
const item = items[i];
|
|
180
|
+
if (item.kind === 'clip') {
|
|
181
|
+
const nested = findImageBoxByPath(item.items, path, intersectRect(clip, item.rect));
|
|
182
|
+
if (nested)
|
|
183
|
+
return nested;
|
|
184
|
+
}
|
|
185
|
+
else if (item.kind === 'image' && item.path === path) {
|
|
186
|
+
return intersectRect(clip, item);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function paintCaretBar(ctx, caret) {
|
|
192
|
+
ctx.save();
|
|
193
|
+
if (caret.turn)
|
|
194
|
+
ctx.transform(0, 1, -1, 0, caret.turn[0], caret.turn[1]);
|
|
195
|
+
ctx.fillStyle = caret.style?.color ?? DEFAULT_TEXT_COLOR;
|
|
196
|
+
ctx.fillRect(caret.x - 0.5, caret.top, 1, caret.height);
|
|
197
|
+
ctx.restore();
|
|
198
|
+
}
|
|
199
|
+
function paintImePreview(ctx, caret, text) {
|
|
200
|
+
ctx.save();
|
|
201
|
+
if (caret.turn)
|
|
202
|
+
ctx.transform(0, 1, -1, 0, caret.turn[0], caret.turn[1]);
|
|
203
|
+
ctx.font = caret.style ? canvasFont(caret.style) : `${caret.height}px system-ui, sans-serif`;
|
|
204
|
+
Object.assign(ctx, { lang: CANVAS_TEXT_LANGUAGE });
|
|
205
|
+
ctx.fontKerning = 'none';
|
|
206
|
+
ctx.textBaseline = 'alphabetic';
|
|
207
|
+
ctx.textAlign = 'left';
|
|
208
|
+
const textWidth = text ? ctx.measureText(text).width : 0;
|
|
209
|
+
const padX = 2;
|
|
210
|
+
const boxWidth = Math.max(4, textWidth + padX * 2);
|
|
211
|
+
const boxHeight = caret.height;
|
|
212
|
+
roundedPath(ctx, caret.x, caret.top, boxWidth, boxHeight, 2);
|
|
213
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.96)';
|
|
214
|
+
ctx.fill();
|
|
215
|
+
ctx.strokeStyle = 'rgba(37, 99, 235, 0.35)';
|
|
216
|
+
ctx.lineWidth = 0.75;
|
|
217
|
+
ctx.stroke();
|
|
218
|
+
if (text) {
|
|
219
|
+
ctx.fillStyle = caret.style?.color ?? DEFAULT_TEXT_COLOR;
|
|
220
|
+
ctx.fillText(text, caret.x + padX, caret.baseline);
|
|
221
|
+
ctx.beginPath();
|
|
222
|
+
ctx.moveTo(caret.x + padX, caret.baseline + 1.5);
|
|
223
|
+
ctx.lineTo(caret.x + padX + textWidth, caret.baseline + 1.5);
|
|
224
|
+
ctx.strokeStyle = caret.style?.color ?? DEFAULT_TEXT_COLOR;
|
|
225
|
+
ctx.lineWidth = 1;
|
|
226
|
+
ctx.setLineDash([2, 1.5]);
|
|
227
|
+
ctx.stroke();
|
|
228
|
+
ctx.setLineDash([]);
|
|
229
|
+
}
|
|
230
|
+
ctx.fillStyle = caret.style?.color ?? DEFAULT_TEXT_COLOR;
|
|
231
|
+
ctx.fillRect(caret.x + padX + textWidth - 0.5, caret.top, 1, boxHeight);
|
|
232
|
+
ctx.restore();
|
|
233
|
+
}
|
|
234
|
+
function detectMarkInRuns(doc, index, range, mark) {
|
|
235
|
+
const path = index.flowPath(range.start.flow);
|
|
236
|
+
if (!path)
|
|
237
|
+
return undefined;
|
|
238
|
+
let sawGlyph = false;
|
|
239
|
+
for (let p = 0; p < doc.pages.length; p += 1) {
|
|
240
|
+
const boxes = index.highlightsForRange(range, p);
|
|
241
|
+
if (boxes.length === 0)
|
|
242
|
+
continue;
|
|
243
|
+
for (const item of doc.pages[p].items) {
|
|
244
|
+
if (item.kind !== 'glyphs' || item.path !== path)
|
|
245
|
+
continue;
|
|
246
|
+
const run = item;
|
|
247
|
+
for (const box of boxes) {
|
|
248
|
+
if (run.baseline < box.top - 2 || run.baseline > box.bottom + 2)
|
|
249
|
+
continue;
|
|
250
|
+
const firstX = run.glyphs[0]?.x ?? 0;
|
|
251
|
+
const lastGlyph = run.glyphs[run.glyphs.length - 1];
|
|
252
|
+
const lastX = lastGlyph ? lastGlyph.x + lastGlyph.width : firstX;
|
|
253
|
+
if (lastX < box.left - 0.5 || firstX > box.right + 0.5)
|
|
254
|
+
continue;
|
|
255
|
+
sawGlyph = true;
|
|
256
|
+
if (!run[mark])
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return sawGlyph ? true : undefined;
|
|
262
|
+
}
|
|
263
|
+
export function mountEditor(container, session, options = {}) {
|
|
264
|
+
let zoom = clampZoom(options.zoom);
|
|
265
|
+
let isReadOnly = options.readOnly === true;
|
|
266
|
+
const bottomInset = Math.max(0, options.bottomInset ?? 0);
|
|
267
|
+
const effectiveBottomInset = () => (isReadOnly ? 0 : bottomInset);
|
|
268
|
+
let doc;
|
|
269
|
+
let docRevision;
|
|
270
|
+
let editIndex;
|
|
271
|
+
let layout = layoutPages([], zoom, SPACING);
|
|
272
|
+
let current = -1;
|
|
273
|
+
let frame = 0;
|
|
274
|
+
let destroyed = false;
|
|
275
|
+
let images = [];
|
|
276
|
+
let lastImages;
|
|
277
|
+
const slots = new Map();
|
|
278
|
+
let activeSelection = null;
|
|
279
|
+
let readOnlyTarget = null;
|
|
280
|
+
let selectedImageTarget = null;
|
|
281
|
+
let lastCaret = null;
|
|
282
|
+
let preferredX;
|
|
283
|
+
let drag;
|
|
284
|
+
let dragFrame = 0;
|
|
285
|
+
let lastDragPoint;
|
|
286
|
+
let autoscroll = 0;
|
|
287
|
+
let caretVisible = true;
|
|
288
|
+
let solidTimer;
|
|
289
|
+
let blinkTimer;
|
|
290
|
+
let pendingScrollReveal = false;
|
|
291
|
+
let readyWaiters = [];
|
|
292
|
+
const markToggleHints = new Map();
|
|
293
|
+
const view = element('div', 'idocs-editor-view');
|
|
294
|
+
Object.assign(view.style, { height: '100%', position: 'relative' });
|
|
295
|
+
const scroller = element('div', 'idocs-preview-scroller');
|
|
296
|
+
Object.assign(scroller.style, { height: '100%', overflow: 'auto', position: 'relative' });
|
|
297
|
+
scroller.tabIndex = 0;
|
|
298
|
+
const content = element('div', 'idocs-preview-pages');
|
|
299
|
+
Object.assign(content.style, { position: 'relative', userSelect: 'none', cursor: 'text' });
|
|
300
|
+
scroller.append(content);
|
|
301
|
+
view.append(scroller);
|
|
302
|
+
container.append(view);
|
|
303
|
+
const currentRevision = () => session.state().revision;
|
|
304
|
+
const resolveCaret = () => {
|
|
305
|
+
if (!activeSelection || !editIndex)
|
|
306
|
+
return lastCaret;
|
|
307
|
+
const box = editIndex.caretForAnchor(activeHead(activeSelection));
|
|
308
|
+
if (box) {
|
|
309
|
+
lastCaret = box;
|
|
310
|
+
return box;
|
|
311
|
+
}
|
|
312
|
+
return lastCaret;
|
|
313
|
+
};
|
|
314
|
+
const repaintOverlays = () => {
|
|
315
|
+
for (const [index, slot] of slots)
|
|
316
|
+
paintOverlay(index, slot.overlay, slot.pixelsPerPoint);
|
|
317
|
+
};
|
|
318
|
+
const clearBlinkTimers = () => {
|
|
319
|
+
if (solidTimer !== undefined) {
|
|
320
|
+
clearTimeout(solidTimer);
|
|
321
|
+
solidTimer = undefined;
|
|
322
|
+
}
|
|
323
|
+
if (blinkTimer !== undefined) {
|
|
324
|
+
clearInterval(blinkTimer);
|
|
325
|
+
blinkTimer = undefined;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const resetBlink = () => {
|
|
329
|
+
clearBlinkTimers();
|
|
330
|
+
caretVisible = true;
|
|
331
|
+
repaintOverlays();
|
|
332
|
+
if (destroyed || !ime.focused || isReadOnly)
|
|
333
|
+
return;
|
|
334
|
+
if (!activeSelection || !isCollapsedRange(activeSelection.range))
|
|
335
|
+
return;
|
|
336
|
+
solidTimer = setTimeout(() => {
|
|
337
|
+
solidTimer = undefined;
|
|
338
|
+
if (destroyed || !ime.focused)
|
|
339
|
+
return;
|
|
340
|
+
blinkTimer = setInterval(() => {
|
|
341
|
+
if (destroyed || !ime.focused) {
|
|
342
|
+
clearBlinkTimers();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
caretVisible = !caretVisible;
|
|
346
|
+
repaintOverlays();
|
|
347
|
+
}, 530);
|
|
348
|
+
}, 500);
|
|
349
|
+
};
|
|
350
|
+
const buildViewSelection = () => {
|
|
351
|
+
if (!activeSelection || !editIndex)
|
|
352
|
+
return null;
|
|
353
|
+
const range = activeSelection.range;
|
|
354
|
+
const index = editIndex;
|
|
355
|
+
const canQuote = range.revision === docRevision;
|
|
356
|
+
const path = editIndex.flowPath(range.start.flow) ?? '';
|
|
357
|
+
let page = 0;
|
|
358
|
+
let pageBounds = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
359
|
+
if (isCollapsedRange(range)) {
|
|
360
|
+
const caret = editIndex.caretForAnchor(range.start);
|
|
361
|
+
if (!caret)
|
|
362
|
+
return null;
|
|
363
|
+
page = caret.page;
|
|
364
|
+
pageBounds = caretPageBounds(caret);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const found = editIndex.selectionPageBounds(range);
|
|
368
|
+
if (!found) {
|
|
369
|
+
const caret = editIndex.caretForAnchor(activeHead(activeSelection));
|
|
370
|
+
if (!caret)
|
|
371
|
+
return null;
|
|
372
|
+
page = caret.page;
|
|
373
|
+
pageBounds = caretPageBounds(caret);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
page = found.page;
|
|
377
|
+
pageBounds = found.bounds;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let clientRect = null;
|
|
381
|
+
const placed = layout.pages[page];
|
|
382
|
+
if (placed && view.isConnected) {
|
|
383
|
+
const scrollerRect = scroller.getBoundingClientRect();
|
|
384
|
+
const scale = PX_PER_PT * zoom;
|
|
385
|
+
const contentWidth = Math.max(scroller.clientWidth, content.clientWidth, layout.width);
|
|
386
|
+
const pageLeft = contentWidth / 2 - placed.width / 2;
|
|
387
|
+
const left = scrollerRect.left + pageLeft - scroller.scrollLeft + pageBounds.left * scale;
|
|
388
|
+
const right = scrollerRect.left + pageLeft - scroller.scrollLeft + pageBounds.right * scale;
|
|
389
|
+
const top = scrollerRect.top + placed.top - scroller.scrollTop + pageBounds.top * scale;
|
|
390
|
+
const bottom = scrollerRect.top + placed.top - scroller.scrollTop + pageBounds.bottom * scale;
|
|
391
|
+
clientRect = {
|
|
392
|
+
left,
|
|
393
|
+
top,
|
|
394
|
+
right,
|
|
395
|
+
bottom,
|
|
396
|
+
width: right - left,
|
|
397
|
+
height: bottom - top,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
selection: activeSelection,
|
|
402
|
+
text: isCollapsedRange(range) ? '' : editIndex.selectedText(range),
|
|
403
|
+
get excerpt() {
|
|
404
|
+
return canQuote ? index.selectedText(range, 'quote') : '';
|
|
405
|
+
},
|
|
406
|
+
path,
|
|
407
|
+
page,
|
|
408
|
+
pageBounds,
|
|
409
|
+
clientRect,
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
const emitSelectionChange = () => {
|
|
413
|
+
options.onSelectionChange?.(buildViewSelection());
|
|
414
|
+
};
|
|
415
|
+
const setSelectionLocal = (next, reveal = false) => {
|
|
416
|
+
if (!reveal && sameSelection(activeSelection, next) && !readOnlyTarget && !selectedImageTarget) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
activeSelection = next;
|
|
420
|
+
if (next) {
|
|
421
|
+
if (readOnlyTarget) {
|
|
422
|
+
readOnlyTarget = null;
|
|
423
|
+
options.onReadOnlyTarget?.(null);
|
|
424
|
+
}
|
|
425
|
+
if (selectedImageTarget) {
|
|
426
|
+
selectedImageTarget = null;
|
|
427
|
+
options.onImageSelect?.(null);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const caret = next ? (editIndex?.caretForAnchor(activeHead(next)) ?? null) : null;
|
|
431
|
+
if (caret)
|
|
432
|
+
lastCaret = caret;
|
|
433
|
+
ime.position(caret);
|
|
434
|
+
resetBlink();
|
|
435
|
+
if (reveal)
|
|
436
|
+
revealCaret();
|
|
437
|
+
emitSelectionChange();
|
|
438
|
+
};
|
|
439
|
+
const setReadOnlyLocal = (target) => {
|
|
440
|
+
readOnlyTarget = target;
|
|
441
|
+
if (target) {
|
|
442
|
+
activeSelection = null;
|
|
443
|
+
lastCaret = null;
|
|
444
|
+
ime.position(null);
|
|
445
|
+
emitSelectionChange();
|
|
446
|
+
if (selectedImageTarget) {
|
|
447
|
+
selectedImageTarget = null;
|
|
448
|
+
options.onImageSelect?.(null);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
repaintOverlays();
|
|
452
|
+
options.onReadOnlyTarget?.(target);
|
|
453
|
+
};
|
|
454
|
+
const setSelectedImageLocal = (target) => {
|
|
455
|
+
selectedImageTarget = target;
|
|
456
|
+
if (target) {
|
|
457
|
+
activeSelection = null;
|
|
458
|
+
readOnlyTarget = null;
|
|
459
|
+
lastCaret = null;
|
|
460
|
+
ime.position(null);
|
|
461
|
+
emitSelectionChange();
|
|
462
|
+
options.onReadOnlyTarget?.(null);
|
|
463
|
+
}
|
|
464
|
+
repaintOverlays();
|
|
465
|
+
options.onImageSelect?.(target);
|
|
466
|
+
};
|
|
467
|
+
const revealCaret = () => {
|
|
468
|
+
const caret = resolveCaret();
|
|
469
|
+
if (!caret)
|
|
470
|
+
return;
|
|
471
|
+
const bounds = caretPageBounds(caret);
|
|
472
|
+
const { top, left } = revealScroll(layout, zoom, {
|
|
473
|
+
page: caret.page,
|
|
474
|
+
left: bounds.left - 4,
|
|
475
|
+
right: bounds.right + 4,
|
|
476
|
+
top: bounds.top - 4,
|
|
477
|
+
bottom: bounds.bottom + 4,
|
|
478
|
+
}, { top: scroller.scrollTop, left: scroller.scrollLeft }, { width: scroller.clientWidth, height: Math.max(40, scroller.clientHeight - effectiveBottomInset()) });
|
|
479
|
+
scroller.scrollTop = top;
|
|
480
|
+
scroller.scrollLeft = left;
|
|
481
|
+
ime.position(caret);
|
|
482
|
+
};
|
|
483
|
+
const waitForLatestIndex = () => {
|
|
484
|
+
const targetRev = currentRevision();
|
|
485
|
+
if (docRevision === targetRev && editIndex)
|
|
486
|
+
return Promise.resolve(editIndex);
|
|
487
|
+
if (destroyed)
|
|
488
|
+
return Promise.resolve(undefined);
|
|
489
|
+
return new Promise((resolve) => {
|
|
490
|
+
readyWaiters.push(resolve);
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
const flushWaiters = (index) => {
|
|
494
|
+
if (readyWaiters.length === 0)
|
|
495
|
+
return;
|
|
496
|
+
const waiters = readyWaiters;
|
|
497
|
+
readyWaiters = [];
|
|
498
|
+
for (const resolve of waiters)
|
|
499
|
+
resolve(index);
|
|
500
|
+
};
|
|
501
|
+
const makeSelectionFromAnchors = (revision, a, b) => {
|
|
502
|
+
const ordered = orderedAnchors(a, b);
|
|
503
|
+
return {
|
|
504
|
+
revision,
|
|
505
|
+
range: { revision, start: ordered.start, end: ordered.end },
|
|
506
|
+
reversed: ordered.reversed,
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
const applyRangeText = async (base, range, text) => {
|
|
510
|
+
if (destroyed || isReadOnly)
|
|
511
|
+
return null;
|
|
512
|
+
pendingScrollReveal = true;
|
|
513
|
+
const commit = await session.apply({
|
|
514
|
+
base,
|
|
515
|
+
edit: { kind: 'replaceText', range, text },
|
|
516
|
+
});
|
|
517
|
+
return commit;
|
|
518
|
+
};
|
|
519
|
+
const replaceSelection = async (text) => {
|
|
520
|
+
if (destroyed || isReadOnly || !activeSelection)
|
|
521
|
+
return null;
|
|
522
|
+
const base = currentRevision();
|
|
523
|
+
const range = activeSelection.range.revision === base
|
|
524
|
+
? activeSelection.range
|
|
525
|
+
: { revision: base, start: activeSelection.range.start, end: activeSelection.range.end };
|
|
526
|
+
preferredX = undefined;
|
|
527
|
+
return applyRangeText(base, range, text);
|
|
528
|
+
};
|
|
529
|
+
const performDeleteBackward = async () => {
|
|
530
|
+
if (destroyed || isReadOnly || !activeSelection)
|
|
531
|
+
return null;
|
|
532
|
+
preferredX = undefined;
|
|
533
|
+
if (!isCollapsedRange(activeSelection.range)) {
|
|
534
|
+
return replaceSelection('');
|
|
535
|
+
}
|
|
536
|
+
const index = await waitForLatestIndex();
|
|
537
|
+
if (destroyed || isReadOnly || !index || !activeSelection)
|
|
538
|
+
return null;
|
|
539
|
+
const base = currentRevision();
|
|
540
|
+
const anchor = activeSelection.range.start;
|
|
541
|
+
if (anchor.offset === 0) {
|
|
542
|
+
pendingScrollReveal = true;
|
|
543
|
+
return session.apply({ base, edit: { kind: 'mergeBackward', at: anchor } });
|
|
544
|
+
}
|
|
545
|
+
const prev = index.neighborStop(anchor, 'backward');
|
|
546
|
+
if (!prev) {
|
|
547
|
+
pendingScrollReveal = true;
|
|
548
|
+
return session.apply({ base, edit: { kind: 'mergeBackward', at: anchor } });
|
|
549
|
+
}
|
|
550
|
+
return applyRangeText(base, { revision: base, start: prev, end: anchor }, '');
|
|
551
|
+
};
|
|
552
|
+
const performDeleteForward = async () => {
|
|
553
|
+
if (destroyed || isReadOnly || !activeSelection)
|
|
554
|
+
return null;
|
|
555
|
+
preferredX = undefined;
|
|
556
|
+
if (!isCollapsedRange(activeSelection.range)) {
|
|
557
|
+
return replaceSelection('');
|
|
558
|
+
}
|
|
559
|
+
const index = await waitForLatestIndex();
|
|
560
|
+
if (destroyed || isReadOnly || !index || !activeSelection)
|
|
561
|
+
return null;
|
|
562
|
+
const base = currentRevision();
|
|
563
|
+
const anchor = activeSelection.range.start;
|
|
564
|
+
const next = index.neighborStop(anchor, 'forward');
|
|
565
|
+
if (!next)
|
|
566
|
+
return null;
|
|
567
|
+
return applyRangeText(base, { revision: base, start: anchor, end: next }, '');
|
|
568
|
+
};
|
|
569
|
+
const performSplitBlock = async () => {
|
|
570
|
+
if (destroyed || isReadOnly || !activeSelection)
|
|
571
|
+
return null;
|
|
572
|
+
preferredX = undefined;
|
|
573
|
+
pendingScrollReveal = true;
|
|
574
|
+
let base = currentRevision();
|
|
575
|
+
let splitAt = activeSelection.range.start;
|
|
576
|
+
if (!isCollapsedRange(activeSelection.range)) {
|
|
577
|
+
const range = activeSelection.range.revision === base
|
|
578
|
+
? activeSelection.range
|
|
579
|
+
: { revision: base, start: activeSelection.range.start, end: activeSelection.range.end };
|
|
580
|
+
const deleted = await session.apply({
|
|
581
|
+
base,
|
|
582
|
+
edit: { kind: 'replaceText', range, text: '' },
|
|
583
|
+
});
|
|
584
|
+
base = deleted.revision;
|
|
585
|
+
if (deleted.selection)
|
|
586
|
+
splitAt = deleted.selection.range.start;
|
|
587
|
+
}
|
|
588
|
+
return session.apply({ base, edit: { kind: 'splitBlock', at: splitAt } });
|
|
589
|
+
};
|
|
590
|
+
const performToggleMark = async (mark, enabled) => {
|
|
591
|
+
if (destroyed || isReadOnly || !activeSelection || isCollapsedRange(activeSelection.range))
|
|
592
|
+
return null;
|
|
593
|
+
const base = currentRevision();
|
|
594
|
+
const range = activeSelection.range.revision === base
|
|
595
|
+
? activeSelection.range
|
|
596
|
+
: { revision: base, start: activeSelection.range.start, end: activeSelection.range.end };
|
|
597
|
+
const hintKey = `${range.start.flow}:${range.start.offset}:${range.end.offset}:${mark}`;
|
|
598
|
+
let desired;
|
|
599
|
+
if (enabled !== undefined) {
|
|
600
|
+
desired = enabled;
|
|
601
|
+
}
|
|
602
|
+
else if (markToggleHints.has(hintKey)) {
|
|
603
|
+
desired = !markToggleHints.get(hintKey);
|
|
604
|
+
}
|
|
605
|
+
else if ((mark === 'bold' || mark === 'italic') && doc && editIndex) {
|
|
606
|
+
const detected = detectMarkInRuns(doc, editIndex, range, mark);
|
|
607
|
+
desired = detected === undefined ? true : !detected;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
desired = true;
|
|
611
|
+
}
|
|
612
|
+
const commit = await session.apply({
|
|
613
|
+
base,
|
|
614
|
+
edit: { kind: 'setMark', range, mark, enabled: desired },
|
|
615
|
+
});
|
|
616
|
+
if (enabled === undefined && commit.revision === base) {
|
|
617
|
+
const flipped = !desired;
|
|
618
|
+
const retry = await session.apply({
|
|
619
|
+
base,
|
|
620
|
+
edit: { kind: 'setMark', range, mark, enabled: flipped },
|
|
621
|
+
});
|
|
622
|
+
markToggleHints.set(hintKey, flipped);
|
|
623
|
+
return retry;
|
|
624
|
+
}
|
|
625
|
+
markToggleHints.set(hintKey, desired);
|
|
626
|
+
return commit;
|
|
627
|
+
};
|
|
628
|
+
const performUndo = async () => {
|
|
629
|
+
if (destroyed || isReadOnly || !session.state().canUndo)
|
|
630
|
+
return null;
|
|
631
|
+
preferredX = undefined;
|
|
632
|
+
pendingScrollReveal = true;
|
|
633
|
+
return session.undo({ base: currentRevision() });
|
|
634
|
+
};
|
|
635
|
+
const performRedo = async () => {
|
|
636
|
+
if (destroyed || isReadOnly || !session.state().canRedo)
|
|
637
|
+
return null;
|
|
638
|
+
preferredX = undefined;
|
|
639
|
+
pendingScrollReveal = true;
|
|
640
|
+
return session.redo({ base: currentRevision() });
|
|
641
|
+
};
|
|
642
|
+
const moveHorizontal = async (direction, granularity, extend) => {
|
|
643
|
+
const index = await waitForLatestIndex();
|
|
644
|
+
if (destroyed || !index || !activeSelection)
|
|
645
|
+
return;
|
|
646
|
+
preferredX = undefined;
|
|
647
|
+
const rev = currentRevision();
|
|
648
|
+
if (!extend && !isCollapsedRange(activeSelection.range)) {
|
|
649
|
+
const target = direction === 'backward' ? activeSelection.range.start : activeSelection.range.end;
|
|
650
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, target, target), true);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const head = activeHead(activeSelection);
|
|
654
|
+
const nextHead = index.moveHorizontal(head, direction, granularity);
|
|
655
|
+
if (extend) {
|
|
656
|
+
const fixed = anchorPoint(activeSelection);
|
|
657
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, fixed, nextHead), true);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, nextHead, nextHead), true);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
const moveVertical = async (direction, extend) => {
|
|
664
|
+
const index = await waitForLatestIndex();
|
|
665
|
+
if (destroyed || !index || !activeSelection)
|
|
666
|
+
return;
|
|
667
|
+
const rev = currentRevision();
|
|
668
|
+
const head = activeHead(activeSelection);
|
|
669
|
+
const moved = index.moveVertical(head, direction, preferredX);
|
|
670
|
+
preferredX = moved.preferredX;
|
|
671
|
+
if (extend) {
|
|
672
|
+
const fixed = anchorPoint(activeSelection);
|
|
673
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, fixed, moved.anchor), true);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, moved.anchor, moved.anchor), true);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const moveLineBoundary = async (edge, extend) => {
|
|
680
|
+
const index = await waitForLatestIndex();
|
|
681
|
+
if (destroyed || !index || !activeSelection)
|
|
682
|
+
return;
|
|
683
|
+
preferredX = undefined;
|
|
684
|
+
const rev = currentRevision();
|
|
685
|
+
const head = activeHead(activeSelection);
|
|
686
|
+
const nextHead = index.moveLineBoundary(head, edge);
|
|
687
|
+
if (extend) {
|
|
688
|
+
const fixed = anchorPoint(activeSelection);
|
|
689
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, fixed, nextHead), true);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, nextHead, nextHead), true);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const finder = new Finder(new TextIndex([]));
|
|
696
|
+
const find = createFindBar({
|
|
697
|
+
frame: view,
|
|
698
|
+
finder,
|
|
699
|
+
currentPage: () => current,
|
|
700
|
+
reveal: (bounds) => {
|
|
701
|
+
const { top, left } = revealScroll(layout, zoom, bounds, { top: scroller.scrollTop, left: scroller.scrollLeft }, { width: scroller.clientWidth, height: Math.max(40, scroller.clientHeight - effectiveBottomInset()) });
|
|
702
|
+
scroller.scrollTop = top;
|
|
703
|
+
scroller.scrollLeft = left;
|
|
704
|
+
},
|
|
705
|
+
repaint: repaintOverlays,
|
|
706
|
+
closed: () => {
|
|
707
|
+
ime.focus();
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
const ime = createImeBridge({
|
|
711
|
+
frame: view,
|
|
712
|
+
scroller,
|
|
713
|
+
content,
|
|
714
|
+
layout: () => layout,
|
|
715
|
+
zoom: () => zoom,
|
|
716
|
+
isFindOpen: () => find.isOpen,
|
|
717
|
+
openFind: () => find.open(),
|
|
718
|
+
closeFind: () => find.close(),
|
|
719
|
+
currentRevision,
|
|
720
|
+
currentSelection: () => activeSelection,
|
|
721
|
+
currentCaret: resolveCaret,
|
|
722
|
+
selectedText: () => {
|
|
723
|
+
if (!activeSelection || !editIndex || isCollapsedRange(activeSelection.range))
|
|
724
|
+
return '';
|
|
725
|
+
return editIndex.selectedText(activeSelection.range);
|
|
726
|
+
},
|
|
727
|
+
hasReadOnlyTarget: () => readOnlyTarget !== null,
|
|
728
|
+
clearReadOnlyTarget: () => setReadOnlyLocal(null),
|
|
729
|
+
collapseSelection: () => {
|
|
730
|
+
if (!activeSelection)
|
|
731
|
+
return;
|
|
732
|
+
const head = activeHead(activeSelection);
|
|
733
|
+
setSelectionLocal(makeSelectionFromAnchors(currentRevision(), head, head));
|
|
734
|
+
},
|
|
735
|
+
selectAllInFlow: () => {
|
|
736
|
+
if (!editIndex)
|
|
737
|
+
return;
|
|
738
|
+
const flow = activeSelection?.range.start.flow ?? editIndex.firstFlow();
|
|
739
|
+
if (!flow)
|
|
740
|
+
return;
|
|
741
|
+
const bounds = editIndex.flowBounds(flow);
|
|
742
|
+
if (!bounds)
|
|
743
|
+
return;
|
|
744
|
+
setSelectionLocal(makeSelectionFromAnchors(currentRevision(), bounds.start, bounds.end));
|
|
745
|
+
},
|
|
746
|
+
moveHorizontal,
|
|
747
|
+
moveVertical,
|
|
748
|
+
moveLineBoundary,
|
|
749
|
+
replaceSelection,
|
|
750
|
+
applyRangeText,
|
|
751
|
+
deleteBackward: performDeleteBackward,
|
|
752
|
+
deleteForward: performDeleteForward,
|
|
753
|
+
splitBlock: performSplitBlock,
|
|
754
|
+
toggleMark: performToggleMark,
|
|
755
|
+
undo: performUndo,
|
|
756
|
+
redo: performRedo,
|
|
757
|
+
onFocusChange: (focused) => {
|
|
758
|
+
if (focused) {
|
|
759
|
+
if (!activeSelection && !readOnlyTarget && editIndex) {
|
|
760
|
+
const flow = editIndex.firstFlow();
|
|
761
|
+
const bounds = flow !== undefined ? editIndex.flowBounds(flow) : undefined;
|
|
762
|
+
if (bounds) {
|
|
763
|
+
setSelectionLocal(makeSelectionFromAnchors(currentRevision(), bounds.start, bounds.start));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
resetBlink();
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
clearBlinkTimers();
|
|
771
|
+
caretVisible = false;
|
|
772
|
+
repaintOverlays();
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
onModeChange: (mode) => {
|
|
776
|
+
if (mode.kind === 'composing') {
|
|
777
|
+
clearBlinkTimers();
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
resetBlink();
|
|
781
|
+
}
|
|
782
|
+
repaintOverlays();
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
const paintOverlay = (index, overlay, pixelsPerPoint) => {
|
|
786
|
+
const ctx = overlay.getContext('2d');
|
|
787
|
+
if (!ctx)
|
|
788
|
+
return;
|
|
789
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
790
|
+
ctx.clearRect(0, 0, overlay.width, overlay.height);
|
|
791
|
+
ctx.setTransform(pixelsPerPoint, 0, 0, pixelsPerPoint, 0, 0);
|
|
792
|
+
const marks = finder.marks(index);
|
|
793
|
+
paintHighlightBoxes(ctx, marks.matches, MATCH_HIGHLIGHT);
|
|
794
|
+
paintHighlightBoxes(ctx, marks.current, CURRENT_MATCH_HIGHLIGHT);
|
|
795
|
+
if (activeSelection && editIndex && !isCollapsedRange(activeSelection.range)) {
|
|
796
|
+
const boxes = editIndex.highlightsForRange(activeSelection.range, index);
|
|
797
|
+
paintHighlightBoxes(ctx, boxes, SELECTION_HIGHLIGHT);
|
|
798
|
+
}
|
|
799
|
+
if (readOnlyTarget && readOnlyTarget.page === index) {
|
|
800
|
+
paintReadOnlyBox(ctx, readOnlyTarget.box);
|
|
801
|
+
}
|
|
802
|
+
if (selectedImageTarget && selectedImageTarget.page === index) {
|
|
803
|
+
paintSelectedImageBox(ctx, selectedImageTarget.box);
|
|
804
|
+
}
|
|
805
|
+
if (ime.mode.kind === 'composing' && !isReadOnly) {
|
|
806
|
+
if (ime.mode.caret.page === index) {
|
|
807
|
+
paintImePreview(ctx, ime.mode.caret, ime.mode.text);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else if (!isReadOnly && ime.focused && activeSelection && isCollapsedRange(activeSelection.range) && caretVisible) {
|
|
811
|
+
const caret = resolveCaret();
|
|
812
|
+
if (caret && caret.page === index) {
|
|
813
|
+
paintCaretBar(ctx, caret);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
const sizeContent = () => {
|
|
818
|
+
content.style.width = `max(100%, ${layout.width}px)`;
|
|
819
|
+
content.style.height = `${layout.height + effectiveBottomInset()}px`;
|
|
820
|
+
};
|
|
821
|
+
const paintSlot = (index, slot) => {
|
|
822
|
+
if (!doc)
|
|
823
|
+
return;
|
|
824
|
+
const placed = layout.pages[index];
|
|
825
|
+
const pageData = doc.pages[index];
|
|
826
|
+
if (!placed || !pageData)
|
|
827
|
+
return;
|
|
828
|
+
Object.assign(slot.box.style, {
|
|
829
|
+
top: `${placed.top}px`,
|
|
830
|
+
left: `calc(50% - ${placed.width / 2}px)`,
|
|
831
|
+
width: `${placed.width}px`,
|
|
832
|
+
height: `${placed.height}px`,
|
|
833
|
+
});
|
|
834
|
+
slot.box.dataset.page = String(pageData.number);
|
|
835
|
+
const res = canvasResolution(placed.width, placed.height, globalThis.devicePixelRatio || 1, options.maxCanvasPixels);
|
|
836
|
+
const { canvas, overlay } = slot;
|
|
837
|
+
canvas.width = res.width;
|
|
838
|
+
canvas.height = res.height;
|
|
839
|
+
canvas.style.width = `${res.width / res.scale}px`;
|
|
840
|
+
canvas.style.height = `${res.height / res.scale}px`;
|
|
841
|
+
const ctx = canvas.getContext('2d');
|
|
842
|
+
slot.pixelsPerPoint = PX_PER_PT * zoom * res.scale;
|
|
843
|
+
if (ctx)
|
|
844
|
+
paintPage(ctx, pageData, slot.pixelsPerPoint, images, doc.glyphOutlines);
|
|
845
|
+
overlay.width = res.width;
|
|
846
|
+
overlay.height = res.height;
|
|
847
|
+
overlay.style.width = canvas.style.width;
|
|
848
|
+
overlay.style.height = canvas.style.height;
|
|
849
|
+
paintOverlay(index, overlay, slot.pixelsPerPoint);
|
|
850
|
+
};
|
|
851
|
+
const mountSlot = (index) => {
|
|
852
|
+
const placed = layout.pages[index];
|
|
853
|
+
const box = element('div', 'idocs-preview-page');
|
|
854
|
+
Object.assign(box.style, {
|
|
855
|
+
position: 'absolute',
|
|
856
|
+
top: `${placed.top}px`,
|
|
857
|
+
left: `calc(50% - ${placed.width / 2}px)`,
|
|
858
|
+
width: `${placed.width}px`,
|
|
859
|
+
height: `${placed.height}px`,
|
|
860
|
+
overflow: 'hidden',
|
|
861
|
+
background: '#fff',
|
|
862
|
+
});
|
|
863
|
+
const canvas = element('canvas', 'idocs-preview-content');
|
|
864
|
+
canvas.style.display = 'block';
|
|
865
|
+
const overlay = element('canvas', 'idocs-preview-selection');
|
|
866
|
+
Object.assign(overlay.style, { position: 'absolute', left: '0', top: '0', pointerEvents: 'none' });
|
|
867
|
+
box.append(canvas, overlay);
|
|
868
|
+
const slot = { box, canvas, overlay, pixelsPerPoint: 1 };
|
|
869
|
+
paintSlot(index, slot);
|
|
870
|
+
content.append(box);
|
|
871
|
+
return slot;
|
|
872
|
+
};
|
|
873
|
+
const releaseSlot = (index) => {
|
|
874
|
+
const slot = slots.get(index);
|
|
875
|
+
if (!slot)
|
|
876
|
+
return;
|
|
877
|
+
freeCanvas(slot.canvas);
|
|
878
|
+
freeCanvas(slot.overlay);
|
|
879
|
+
slot.box.remove();
|
|
880
|
+
slots.delete(index);
|
|
881
|
+
};
|
|
882
|
+
const releaseAll = () => {
|
|
883
|
+
for (const index of [...slots.keys()])
|
|
884
|
+
releaseSlot(index);
|
|
885
|
+
};
|
|
886
|
+
const updateViewport = (repaintExisting = false) => {
|
|
887
|
+
frame = 0;
|
|
888
|
+
if (destroyed || !doc)
|
|
889
|
+
return;
|
|
890
|
+
const { scrollTop, clientHeight } = scroller;
|
|
891
|
+
const keep = pagesToKeep(layout, scrollTop, clientHeight);
|
|
892
|
+
for (const index of [...slots.keys()]) {
|
|
893
|
+
if (index < keep.first || index > keep.last)
|
|
894
|
+
releaseSlot(index);
|
|
895
|
+
}
|
|
896
|
+
for (let index = keep.first; index <= keep.last; index += 1) {
|
|
897
|
+
const existing = slots.get(index);
|
|
898
|
+
if (existing) {
|
|
899
|
+
if (repaintExisting)
|
|
900
|
+
paintSlot(index, existing);
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
slots.set(index, mountSlot(index));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const now = currentPage(layout, scrollTop, clientHeight);
|
|
907
|
+
if (now !== current) {
|
|
908
|
+
current = now;
|
|
909
|
+
if (now >= 0)
|
|
910
|
+
options.onPageChange?.(now, doc);
|
|
911
|
+
}
|
|
912
|
+
ime.position(resolveCaret());
|
|
913
|
+
};
|
|
914
|
+
const scheduleViewport = () => {
|
|
915
|
+
if (!frame && !destroyed)
|
|
916
|
+
frame = requestAnimationFrame(() => updateViewport(false));
|
|
917
|
+
};
|
|
918
|
+
const zoomAround = (next, x, y) => {
|
|
919
|
+
next = clampZoom(next);
|
|
920
|
+
if (next === zoom || !doc)
|
|
921
|
+
return false;
|
|
922
|
+
const anchor = anchorAt(layout, scroller.scrollTop + y);
|
|
923
|
+
const across = (scroller.scrollLeft + x) / Math.max(content.offsetWidth, 1);
|
|
924
|
+
zoom = next;
|
|
925
|
+
layout = layoutPages(doc.pages, zoom, SPACING);
|
|
926
|
+
releaseAll();
|
|
927
|
+
sizeContent();
|
|
928
|
+
scroller.scrollTop = scrollTopFor(layout, anchor) - y;
|
|
929
|
+
scroller.scrollLeft = across * content.offsetWidth - x;
|
|
930
|
+
updateViewport(false);
|
|
931
|
+
return true;
|
|
932
|
+
};
|
|
933
|
+
const pointAt = (clientX, clientY) => {
|
|
934
|
+
const { pages } = layout;
|
|
935
|
+
if (pages.length === 0)
|
|
936
|
+
return undefined;
|
|
937
|
+
const rect = content.getBoundingClientRect();
|
|
938
|
+
const y = clientY - rect.top;
|
|
939
|
+
let index = pages.findIndex((p) => y < p.top + p.height);
|
|
940
|
+
if (index < 0)
|
|
941
|
+
index = pages.length - 1;
|
|
942
|
+
if (index > 0 && y < pages[index].top) {
|
|
943
|
+
const above = pages[index - 1];
|
|
944
|
+
if (y - (above.top + above.height) < pages[index].top - y)
|
|
945
|
+
index -= 1;
|
|
946
|
+
}
|
|
947
|
+
const placed = pages[index];
|
|
948
|
+
const left = rect.width / 2 - placed.width / 2;
|
|
949
|
+
const scale = PX_PER_PT * zoom;
|
|
950
|
+
return { page: index, x: (clientX - rect.left - left) / scale, y: (y - placed.top) / scale };
|
|
951
|
+
};
|
|
952
|
+
const startDrag = (state, event) => {
|
|
953
|
+
drag = state;
|
|
954
|
+
lastDragPoint = { x: event.clientX, y: event.clientY };
|
|
955
|
+
addEventListener('mousemove', onMouseMove);
|
|
956
|
+
addEventListener('mouseup', onMouseUp);
|
|
957
|
+
addEventListener('blur', onMouseUp);
|
|
958
|
+
};
|
|
959
|
+
const followDrag = () => {
|
|
960
|
+
if (!drag || !lastDragPoint || !editIndex)
|
|
961
|
+
return;
|
|
962
|
+
const point = pointAt(lastDragPoint.x, lastDragPoint.y);
|
|
963
|
+
if (!point)
|
|
964
|
+
return;
|
|
965
|
+
const rev = currentRevision();
|
|
966
|
+
if (drag.mode === 'word') {
|
|
967
|
+
const range = editIndex.wordRangeInFlow(drag.flow, point);
|
|
968
|
+
if (!range)
|
|
969
|
+
return;
|
|
970
|
+
if (range.start.offset < drag.originStart.offset) {
|
|
971
|
+
setSelectionLocal({
|
|
972
|
+
revision: rev,
|
|
973
|
+
range: { revision: rev, start: range.start, end: drag.originEnd },
|
|
974
|
+
reversed: true,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
const end = range.end.offset > drag.originEnd.offset ? range.end : drag.originEnd;
|
|
979
|
+
setSelectionLocal({
|
|
980
|
+
revision: rev,
|
|
981
|
+
range: { revision: rev, start: drag.originStart, end },
|
|
982
|
+
reversed: false,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const targetAnchor = editIndex.locatePointInFlow(drag.flow, point);
|
|
988
|
+
if (targetAnchor) {
|
|
989
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, drag.originStart, targetAnchor));
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const stepAutoscroll = () => {
|
|
993
|
+
autoscroll = 0;
|
|
994
|
+
if (!drag || !lastDragPoint)
|
|
995
|
+
return;
|
|
996
|
+
const rect = scroller.getBoundingClientRect();
|
|
997
|
+
const top = rect.top + DRAG_EDGE;
|
|
998
|
+
const bottom = rect.bottom - DRAG_EDGE;
|
|
999
|
+
const out = lastDragPoint.y < top ? lastDragPoint.y - top : lastDragPoint.y > bottom ? lastDragPoint.y - bottom : 0;
|
|
1000
|
+
if (out === 0)
|
|
1001
|
+
return;
|
|
1002
|
+
scroller.scrollTop += Math.max(-60, Math.min(60, out / 2));
|
|
1003
|
+
autoscroll = requestAnimationFrame(stepAutoscroll);
|
|
1004
|
+
};
|
|
1005
|
+
const onMouseDown = (event) => {
|
|
1006
|
+
if (event.button !== 0 || !editIndex)
|
|
1007
|
+
return;
|
|
1008
|
+
const point = pointAt(event.clientX, event.clientY);
|
|
1009
|
+
if (!point)
|
|
1010
|
+
return;
|
|
1011
|
+
event.preventDefault();
|
|
1012
|
+
ime.focus();
|
|
1013
|
+
preferredX = undefined;
|
|
1014
|
+
const rev = currentRevision();
|
|
1015
|
+
const clicks = event.detail || 1;
|
|
1016
|
+
if (event.shiftKey && activeSelection) {
|
|
1017
|
+
const fixed = anchorPoint(activeSelection);
|
|
1018
|
+
const targetAnchor = editIndex.locatePointInFlow(fixed.flow, point);
|
|
1019
|
+
if (targetAnchor) {
|
|
1020
|
+
startDrag({
|
|
1021
|
+
mode: 'character',
|
|
1022
|
+
flow: fixed.flow,
|
|
1023
|
+
originStart: fixed,
|
|
1024
|
+
originEnd: fixed,
|
|
1025
|
+
}, event);
|
|
1026
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, fixed, targetAnchor));
|
|
1027
|
+
}
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (clicks >= 3) {
|
|
1031
|
+
const range = editIndex.flowRangeAt(point);
|
|
1032
|
+
if (range) {
|
|
1033
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, range.start, range.end));
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (clicks === 2) {
|
|
1038
|
+
const range = editIndex.wordRangeAt(point);
|
|
1039
|
+
if (range) {
|
|
1040
|
+
startDrag({
|
|
1041
|
+
mode: 'word',
|
|
1042
|
+
flow: range.start.flow,
|
|
1043
|
+
originStart: range.start,
|
|
1044
|
+
originEnd: range.end,
|
|
1045
|
+
}, event);
|
|
1046
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, range.start, range.end));
|
|
1047
|
+
}
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (clicks === 1 && doc) {
|
|
1051
|
+
const pageItems = doc.pages[point.page]?.items;
|
|
1052
|
+
const imgHit = pageItems ? hitImageInItems(pageItems, point.x, point.y) : null;
|
|
1053
|
+
if (imgHit) {
|
|
1054
|
+
setSelectedImageLocal({ page: point.page, path: imgHit.path, box: imgHit.box });
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const hit = editIndex.locatePoint(point);
|
|
1059
|
+
if (hit.kind === 'editable') {
|
|
1060
|
+
startDrag({
|
|
1061
|
+
mode: 'character',
|
|
1062
|
+
flow: hit.anchor.flow,
|
|
1063
|
+
originStart: hit.anchor,
|
|
1064
|
+
originEnd: hit.anchor,
|
|
1065
|
+
}, event);
|
|
1066
|
+
setSelectionLocal(makeSelectionFromAnchors(rev, hit.anchor, hit.anchor));
|
|
1067
|
+
}
|
|
1068
|
+
else if (hit.kind === 'readOnly') {
|
|
1069
|
+
setReadOnlyLocal(hit.target);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
setReadOnlyLocal(null);
|
|
1073
|
+
if (selectedImageTarget)
|
|
1074
|
+
setSelectedImageLocal(null);
|
|
1075
|
+
setSelectionLocal(null);
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
const onMouseMove = (event) => {
|
|
1079
|
+
if (!drag)
|
|
1080
|
+
return;
|
|
1081
|
+
lastDragPoint = { x: event.clientX, y: event.clientY };
|
|
1082
|
+
if (!dragFrame) {
|
|
1083
|
+
dragFrame = requestAnimationFrame(() => {
|
|
1084
|
+
dragFrame = 0;
|
|
1085
|
+
followDrag();
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (!autoscroll)
|
|
1089
|
+
stepAutoscroll();
|
|
1090
|
+
};
|
|
1091
|
+
const onMouseUp = () => {
|
|
1092
|
+
removeEventListener('mousemove', onMouseMove);
|
|
1093
|
+
removeEventListener('mouseup', onMouseUp);
|
|
1094
|
+
removeEventListener('blur', onMouseUp);
|
|
1095
|
+
if (dragFrame) {
|
|
1096
|
+
cancelAnimationFrame(dragFrame);
|
|
1097
|
+
dragFrame = 0;
|
|
1098
|
+
followDrag();
|
|
1099
|
+
}
|
|
1100
|
+
if (!drag)
|
|
1101
|
+
return;
|
|
1102
|
+
drag = undefined;
|
|
1103
|
+
if (autoscroll) {
|
|
1104
|
+
cancelAnimationFrame(autoscroll);
|
|
1105
|
+
autoscroll = 0;
|
|
1106
|
+
}
|
|
1107
|
+
emitSelectionChange();
|
|
1108
|
+
};
|
|
1109
|
+
const onScroll = () => {
|
|
1110
|
+
followDrag();
|
|
1111
|
+
scheduleViewport();
|
|
1112
|
+
ime.position(resolveCaret());
|
|
1113
|
+
};
|
|
1114
|
+
const onViewKeyDown = (event) => {
|
|
1115
|
+
if (event.defaultPrevented || event.isComposing)
|
|
1116
|
+
return;
|
|
1117
|
+
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === 'f') {
|
|
1118
|
+
event.preventDefault();
|
|
1119
|
+
find.open();
|
|
1120
|
+
}
|
|
1121
|
+
else if (event.key === 'Escape' && find.isOpen) {
|
|
1122
|
+
event.preventDefault();
|
|
1123
|
+
find.close();
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
let wheelFrame = 0;
|
|
1127
|
+
let wheelDelta = 0;
|
|
1128
|
+
let wheelPoint = { x: 0, y: 0 };
|
|
1129
|
+
const onWheel = (event) => {
|
|
1130
|
+
if (!event.ctrlKey)
|
|
1131
|
+
return;
|
|
1132
|
+
event.preventDefault();
|
|
1133
|
+
const unit = event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
|
1134
|
+
? 16
|
|
1135
|
+
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
|
1136
|
+
? scroller.clientHeight
|
|
1137
|
+
: 1;
|
|
1138
|
+
wheelDelta += event.deltaY * unit;
|
|
1139
|
+
const box = scroller.getBoundingClientRect();
|
|
1140
|
+
wheelPoint = { x: event.clientX - box.left, y: event.clientY - box.top };
|
|
1141
|
+
if (wheelFrame)
|
|
1142
|
+
return;
|
|
1143
|
+
wheelFrame = requestAnimationFrame(() => {
|
|
1144
|
+
wheelFrame = 0;
|
|
1145
|
+
const delta = wheelDelta;
|
|
1146
|
+
wheelDelta = 0;
|
|
1147
|
+
if (!destroyed && zoomAround(zoom * Math.exp(-delta / 500), wheelPoint.x, wheelPoint.y)) {
|
|
1148
|
+
options.onZoomChange?.(zoom);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
};
|
|
1152
|
+
let resolution;
|
|
1153
|
+
const watchResolution = () => {
|
|
1154
|
+
resolution?.removeEventListener('change', onResolution);
|
|
1155
|
+
resolution = globalThis.matchMedia?.(`(resolution: ${globalThis.devicePixelRatio || 1}dppx)`);
|
|
1156
|
+
resolution?.addEventListener('change', onResolution);
|
|
1157
|
+
};
|
|
1158
|
+
function onResolution() {
|
|
1159
|
+
watchResolution();
|
|
1160
|
+
releaseAll();
|
|
1161
|
+
updateViewport(false);
|
|
1162
|
+
}
|
|
1163
|
+
const applyFrameDocument = (nextDoc, nextRev, nextSelection) => {
|
|
1164
|
+
if (doc === nextDoc)
|
|
1165
|
+
return;
|
|
1166
|
+
const prevDoc = doc;
|
|
1167
|
+
const prevCount = prevDoc?.pageCount ?? 0;
|
|
1168
|
+
doc = nextDoc;
|
|
1169
|
+
docRevision = nextRev;
|
|
1170
|
+
editIndex = new EditDocumentIndex(nextDoc.pages);
|
|
1171
|
+
const findInput = view.querySelector('.idocs-preview-find input');
|
|
1172
|
+
finder.setIndex(new TextIndex(nextDoc.pages), find.isOpen ? findInput?.value : undefined, Math.max(current, 0));
|
|
1173
|
+
layout = layoutPages(nextDoc.pages, zoom, SPACING);
|
|
1174
|
+
sizeContent();
|
|
1175
|
+
if (!sameImageList(lastImages, nextDoc.images)) {
|
|
1176
|
+
lastImages = nextDoc.images;
|
|
1177
|
+
for (const bitmap of images)
|
|
1178
|
+
bitmap?.close();
|
|
1179
|
+
images = [];
|
|
1180
|
+
if (nextDoc.images.length > 0) {
|
|
1181
|
+
const expectedDoc = nextDoc;
|
|
1182
|
+
void decodeImages(nextDoc.images).then((decoded) => {
|
|
1183
|
+
if (destroyed || doc !== expectedDoc) {
|
|
1184
|
+
for (const bitmap of decoded)
|
|
1185
|
+
bitmap?.close();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
images = decoded;
|
|
1189
|
+
updateViewport(true);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
let selectionChanged = false;
|
|
1194
|
+
if (readOnlyTarget) {
|
|
1195
|
+
readOnlyTarget = null;
|
|
1196
|
+
options.onReadOnlyTarget?.(null);
|
|
1197
|
+
}
|
|
1198
|
+
if (nextSelection && nextSelection.revision === nextRev) {
|
|
1199
|
+
activeSelection = nextSelection;
|
|
1200
|
+
selectionChanged = true;
|
|
1201
|
+
}
|
|
1202
|
+
else if (activeSelection && activeSelection.revision !== nextRev) {
|
|
1203
|
+
const startCaret = editIndex.caretForAnchor(activeSelection.range.start);
|
|
1204
|
+
if (startCaret) {
|
|
1205
|
+
activeSelection = {
|
|
1206
|
+
revision: nextRev,
|
|
1207
|
+
range: {
|
|
1208
|
+
revision: nextRev,
|
|
1209
|
+
start: activeSelection.range.start,
|
|
1210
|
+
end: activeSelection.range.end,
|
|
1211
|
+
},
|
|
1212
|
+
reversed: activeSelection.reversed,
|
|
1213
|
+
};
|
|
1214
|
+
selectionChanged = true;
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
activeSelection = null;
|
|
1218
|
+
selectionChanged = true;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (selectedImageTarget) {
|
|
1222
|
+
let nextTarget = null;
|
|
1223
|
+
for (let p = 0; p < nextDoc.pages.length; p += 1) {
|
|
1224
|
+
const box = findImageBoxByPath(nextDoc.pages[p].items, selectedImageTarget.path);
|
|
1225
|
+
if (box) {
|
|
1226
|
+
nextTarget = { page: p, path: selectedImageTarget.path, box };
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
selectedImageTarget = nextTarget;
|
|
1231
|
+
options.onImageSelect?.(nextTarget);
|
|
1232
|
+
}
|
|
1233
|
+
const caret = activeSelection ? (editIndex.caretForAnchor(activeHead(activeSelection)) ?? null) : null;
|
|
1234
|
+
if (caret)
|
|
1235
|
+
lastCaret = caret;
|
|
1236
|
+
updateViewport(true);
|
|
1237
|
+
if (prevCount === 0 && nextDoc.pageCount > 0) {
|
|
1238
|
+
current = currentPage(layout, scroller.scrollTop, scroller.clientHeight);
|
|
1239
|
+
if (current >= 0)
|
|
1240
|
+
options.onPageChange?.(current, nextDoc);
|
|
1241
|
+
}
|
|
1242
|
+
if (pendingScrollReveal) {
|
|
1243
|
+
pendingScrollReveal = false;
|
|
1244
|
+
revealCaret();
|
|
1245
|
+
}
|
|
1246
|
+
if (selectionChanged) {
|
|
1247
|
+
emitSelectionChange();
|
|
1248
|
+
}
|
|
1249
|
+
if (ime.focused)
|
|
1250
|
+
resetBlink();
|
|
1251
|
+
flushWaiters(editIndex);
|
|
1252
|
+
};
|
|
1253
|
+
const adoptState = (state) => {
|
|
1254
|
+
if (destroyed)
|
|
1255
|
+
return;
|
|
1256
|
+
if (state.layout.kind === 'ready') {
|
|
1257
|
+
applyFrameDocument(state.layout.frame.document, state.layout.frame.revision, state.selection);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (state.layout.kind === 'pending') {
|
|
1261
|
+
if (!doc && state.layout.previous) {
|
|
1262
|
+
applyFrameDocument(state.layout.previous.document, state.layout.previous.revision, state.selection);
|
|
1263
|
+
}
|
|
1264
|
+
else if (state.selection && state.selection.revision === state.revision) {
|
|
1265
|
+
activeSelection = state.selection;
|
|
1266
|
+
readOnlyTarget = null;
|
|
1267
|
+
}
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (state.layout.kind === 'failed') {
|
|
1271
|
+
if (!doc && state.layout.previous) {
|
|
1272
|
+
applyFrameDocument(state.layout.previous.document, state.layout.previous.revision, state.selection);
|
|
1273
|
+
}
|
|
1274
|
+
flushWaiters(undefined);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const resize = new ResizeObserver(scheduleViewport);
|
|
1278
|
+
resize.observe(scroller);
|
|
1279
|
+
watchResolution();
|
|
1280
|
+
content.addEventListener('mousedown', onMouseDown);
|
|
1281
|
+
scroller.addEventListener('scroll', onScroll, { passive: true });
|
|
1282
|
+
scroller.addEventListener('wheel', onWheel, { passive: false });
|
|
1283
|
+
view.addEventListener('keydown', onViewKeyDown);
|
|
1284
|
+
const unsubscribe = session.subscribe(adoptState);
|
|
1285
|
+
adoptState(session.state());
|
|
1286
|
+
return {
|
|
1287
|
+
get canvases() {
|
|
1288
|
+
return [...slots.entries()].sort((a, b) => a[0] - b[0]).map(([, slot]) => slot.canvas);
|
|
1289
|
+
},
|
|
1290
|
+
get zoom() {
|
|
1291
|
+
return zoom;
|
|
1292
|
+
},
|
|
1293
|
+
get currentPage() {
|
|
1294
|
+
return current;
|
|
1295
|
+
},
|
|
1296
|
+
get fitWidthZoom() {
|
|
1297
|
+
return doc ? fitWidthZoom(doc.pages, scroller.clientWidth, SPACING) : 1;
|
|
1298
|
+
},
|
|
1299
|
+
find,
|
|
1300
|
+
get focused() {
|
|
1301
|
+
return ime.focused;
|
|
1302
|
+
},
|
|
1303
|
+
get readOnly() {
|
|
1304
|
+
return isReadOnly;
|
|
1305
|
+
},
|
|
1306
|
+
get dragging() {
|
|
1307
|
+
return drag !== undefined;
|
|
1308
|
+
},
|
|
1309
|
+
selection: buildViewSelection,
|
|
1310
|
+
readOnlyTarget: () => readOnlyTarget,
|
|
1311
|
+
clearReadOnlyTarget() {
|
|
1312
|
+
if (readOnlyTarget)
|
|
1313
|
+
setReadOnlyLocal(null);
|
|
1314
|
+
},
|
|
1315
|
+
selectedImage: () => selectedImageTarget,
|
|
1316
|
+
clearSelectedImage() {
|
|
1317
|
+
if (selectedImageTarget)
|
|
1318
|
+
setSelectedImageLocal(null);
|
|
1319
|
+
},
|
|
1320
|
+
composingText: () => (ime.mode.kind === 'composing' ? ime.mode.text : null),
|
|
1321
|
+
focus() {
|
|
1322
|
+
ime.focus();
|
|
1323
|
+
},
|
|
1324
|
+
setReadOnly(next) {
|
|
1325
|
+
if (destroyed || isReadOnly === next)
|
|
1326
|
+
return;
|
|
1327
|
+
isReadOnly = next;
|
|
1328
|
+
sizeContent();
|
|
1329
|
+
if (isReadOnly) {
|
|
1330
|
+
clearBlinkTimers();
|
|
1331
|
+
caretVisible = false;
|
|
1332
|
+
}
|
|
1333
|
+
else if (ime.focused) {
|
|
1334
|
+
resetBlink();
|
|
1335
|
+
}
|
|
1336
|
+
repaintOverlays();
|
|
1337
|
+
},
|
|
1338
|
+
setZoom(next) {
|
|
1339
|
+
if (destroyed)
|
|
1340
|
+
return;
|
|
1341
|
+
zoomAround(next, scroller.clientWidth / 2, 0);
|
|
1342
|
+
},
|
|
1343
|
+
goToPage(index, scrollOptions) {
|
|
1344
|
+
if (destroyed || layout.pages.length === 0)
|
|
1345
|
+
return;
|
|
1346
|
+
const page = Math.min(Math.max(0, Math.trunc(index)), layout.pages.length - 1);
|
|
1347
|
+
scroller.scrollTo({
|
|
1348
|
+
top: scrollTopFor(layout, { page, fraction: 0, above: SPACING.padding }),
|
|
1349
|
+
behavior: scrollOptions?.behavior,
|
|
1350
|
+
});
|
|
1351
|
+
},
|
|
1352
|
+
selectAnchors(start, end = start, reversed = false) {
|
|
1353
|
+
return ime.enqueue(async () => {
|
|
1354
|
+
if (destroyed)
|
|
1355
|
+
return;
|
|
1356
|
+
const rev = currentRevision();
|
|
1357
|
+
const ordered = orderedAnchors(start, end);
|
|
1358
|
+
const range = await session.range({ revision: rev, start: ordered.start, end: ordered.end });
|
|
1359
|
+
if (destroyed)
|
|
1360
|
+
return;
|
|
1361
|
+
const isReversed = start.offset === end.offset ? false : reversed || ordered.reversed;
|
|
1362
|
+
setSelectionLocal({ revision: range.revision, range, reversed: isReversed }, true);
|
|
1363
|
+
});
|
|
1364
|
+
},
|
|
1365
|
+
insertText(text) {
|
|
1366
|
+
return ime.enqueue(() => replaceSelection(text));
|
|
1367
|
+
},
|
|
1368
|
+
toggleMark(mark, enabled) {
|
|
1369
|
+
return ime.enqueue(() => performToggleMark(mark, enabled));
|
|
1370
|
+
},
|
|
1371
|
+
splitBlock() {
|
|
1372
|
+
return ime.enqueue(() => performSplitBlock());
|
|
1373
|
+
},
|
|
1374
|
+
setBlockStyle(style) {
|
|
1375
|
+
return ime.enqueue(async () => {
|
|
1376
|
+
if (destroyed || isReadOnly || !activeSelection)
|
|
1377
|
+
return null;
|
|
1378
|
+
pendingScrollReveal = true;
|
|
1379
|
+
return session.apply({
|
|
1380
|
+
base: currentRevision(),
|
|
1381
|
+
edit: { kind: 'setBlockStyle', at: activeSelection.range.start, style },
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
setAlignment(align) {
|
|
1386
|
+
return ime.enqueue(async () => {
|
|
1387
|
+
if (destroyed || isReadOnly || !activeSelection)
|
|
1388
|
+
return null;
|
|
1389
|
+
pendingScrollReveal = true;
|
|
1390
|
+
return session.apply({
|
|
1391
|
+
base: currentRevision(),
|
|
1392
|
+
edit: { kind: 'setAlignment', at: activeSelection.range.start, align },
|
|
1393
|
+
});
|
|
1394
|
+
});
|
|
1395
|
+
},
|
|
1396
|
+
insertBlock(block, after) {
|
|
1397
|
+
return ime.enqueue(async () => {
|
|
1398
|
+
if (destroyed || isReadOnly)
|
|
1399
|
+
return null;
|
|
1400
|
+
pendingScrollReveal = true;
|
|
1401
|
+
return session.apply({
|
|
1402
|
+
base: currentRevision(),
|
|
1403
|
+
edit: {
|
|
1404
|
+
kind: 'insertBlock',
|
|
1405
|
+
after: after ?? activeSelection?.range.start,
|
|
1406
|
+
block,
|
|
1407
|
+
},
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
},
|
|
1411
|
+
editTable(action, at) {
|
|
1412
|
+
return ime.enqueue(async () => {
|
|
1413
|
+
if (destroyed || isReadOnly)
|
|
1414
|
+
return null;
|
|
1415
|
+
const target = at ?? activeSelection?.range.start;
|
|
1416
|
+
if (!target)
|
|
1417
|
+
return null;
|
|
1418
|
+
pendingScrollReveal = true;
|
|
1419
|
+
return session.apply({
|
|
1420
|
+
base: currentRevision(),
|
|
1421
|
+
edit: { kind: 'editTable', at: target, action },
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
},
|
|
1425
|
+
editImage(path, patch) {
|
|
1426
|
+
return ime.enqueue(async () => {
|
|
1427
|
+
if (destroyed || isReadOnly)
|
|
1428
|
+
return null;
|
|
1429
|
+
return session.apply({
|
|
1430
|
+
base: currentRevision(),
|
|
1431
|
+
edit: {
|
|
1432
|
+
kind: 'editImage',
|
|
1433
|
+
path,
|
|
1434
|
+
widthRatio: patch.widthRatio,
|
|
1435
|
+
align: patch.align,
|
|
1436
|
+
src: patch.src,
|
|
1437
|
+
alt: patch.alt,
|
|
1438
|
+
},
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
},
|
|
1442
|
+
undo() {
|
|
1443
|
+
return ime.enqueue(() => performUndo());
|
|
1444
|
+
},
|
|
1445
|
+
redo() {
|
|
1446
|
+
return ime.enqueue(() => performRedo());
|
|
1447
|
+
},
|
|
1448
|
+
destroy() {
|
|
1449
|
+
if (destroyed)
|
|
1450
|
+
return;
|
|
1451
|
+
destroyed = true;
|
|
1452
|
+
clearBlinkTimers();
|
|
1453
|
+
if (frame)
|
|
1454
|
+
cancelAnimationFrame(frame);
|
|
1455
|
+
frame = 0;
|
|
1456
|
+
if (dragFrame)
|
|
1457
|
+
cancelAnimationFrame(dragFrame);
|
|
1458
|
+
dragFrame = 0;
|
|
1459
|
+
if (wheelFrame)
|
|
1460
|
+
cancelAnimationFrame(wheelFrame);
|
|
1461
|
+
wheelFrame = 0;
|
|
1462
|
+
if (autoscroll)
|
|
1463
|
+
cancelAnimationFrame(autoscroll);
|
|
1464
|
+
autoscroll = 0;
|
|
1465
|
+
flushWaiters(undefined);
|
|
1466
|
+
unsubscribe();
|
|
1467
|
+
content.removeEventListener('mousedown', onMouseDown);
|
|
1468
|
+
removeEventListener('mousemove', onMouseMove);
|
|
1469
|
+
removeEventListener('mouseup', onMouseUp);
|
|
1470
|
+
removeEventListener('blur', onMouseUp);
|
|
1471
|
+
scroller.removeEventListener('scroll', onScroll);
|
|
1472
|
+
scroller.removeEventListener('wheel', onWheel);
|
|
1473
|
+
view.removeEventListener('keydown', onViewKeyDown);
|
|
1474
|
+
resize.disconnect();
|
|
1475
|
+
resolution?.removeEventListener('change', onResolution);
|
|
1476
|
+
find.destroy();
|
|
1477
|
+
ime.destroy();
|
|
1478
|
+
releaseAll();
|
|
1479
|
+
for (const bitmap of images)
|
|
1480
|
+
bitmap?.close();
|
|
1481
|
+
images = [];
|
|
1482
|
+
view.remove();
|
|
1483
|
+
},
|
|
1484
|
+
};
|
|
1485
|
+
}
|