@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,1121 @@
|
|
|
1
|
+
import { mountEditor, } from './editor.js';
|
|
2
|
+
import { decodeImages, paintPage } from './index.js';
|
|
3
|
+
import { ensureWorkspaceStyles } from './workspace-styles.js';
|
|
4
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
5
|
+
const ICONS = {
|
|
6
|
+
rail: '<path d="M6.835 4c-.451.004-.82.012-1.137.038-.386.032-.659.085-.876.162l-.2.086c-.44.224-.807.564-1.063.982l-.103.184c-.126.247-.206.562-.248 1.076-.043.523-.043 1.19-.043 2.135v2.664c0 .944 0 1.612.043 2.135.042.515.122.829.248 1.076l.103.184c.256.418.624.758 1.063.982l.2.086c.217.077.49.13.876.162.316.026.685.034 1.136.038zm11.33 7.327c0 .922 0 1.654-.048 2.243-.043.522-.125.977-.305 1.395l-.082.177a4 4 0 0 1-1.473 1.593l-.276.155c-.465.237-.974.338-1.57.387-.59.048-1.322.048-2.244.048H7.833c-.922 0-1.654 0-2.243-.048-.522-.042-.977-.126-1.395-.305l-.176-.082a4 4 0 0 1-1.594-1.473l-.154-.275c-.238-.466-.34-.975-.388-1.572-.048-.589-.048-1.32-.048-2.243V8.663c0-.922 0-1.654.048-2.243.049-.597.15-1.106.388-1.571l.154-.276a4 4 0 0 1 1.594-1.472l.176-.083c.418-.18.873-.263 1.395-.305.589-.048 1.32-.048 2.243-.048h4.334c.922 0 1.654 0 2.243.048.597.049 1.106.15 1.571.388l.276.154a4 4 0 0 1 1.473 1.594l.082.176c.18.418.262.873.305 1.395.048.589.048 1.32.048 2.243zm-10 4.668h4.002c.944 0 1.612 0 2.135-.043.514-.042.829-.122 1.076-.248l.184-.103c.418-.256.758-.624.982-1.063l.086-.2c.077-.217.13-.49.162-.876.043-.523.043-1.19.043-2.135V8.663c0-.944 0-1.612-.043-2.135-.032-.386-.085-.659-.162-.876l-.086-.2a2.67 2.67 0 0 0-.982-1.063l-.184-.103c-.247-.126-.562-.206-1.076-.248-.523-.043-1.19-.043-2.135-.043H8.164L8.165 4z"/>',
|
|
7
|
+
undo: '<path d="M15.916 10.833a4.25 4.25 0 0 0-4.25-4.25H5.144l2.053 2.054.051.056a.75.75 0 0 1-1.054 1.055l-.058-.05-3.333-3.335a.75.75 0 0 1 0-1.06L6.136 1.97l.058-.052a.75.75 0 0 1 1.054 1.056l-.051.056-2.053 2.053h6.522a5.75 5.75 0 0 1 0 11.5H8.333a.75.75 0 0 1 0-1.5h3.333a4.25 4.25 0 0 0 4.25-4.25"/>',
|
|
8
|
+
redo: '<path d="M4.084 10.833a4.25 4.25 0 0 1 4.25-4.25h6.522l-2.053 2.054-.051.056a.75.75 0 0 0 1.054 1.055l.058-.05 3.333-3.335a.75.75 0 0 0 0-1.06L13.864 1.97l-.058-.052a.75.75 0 0 0-1.054 1.056l.051.056 2.053 2.053H8.334a5.75 5.75 0 0 0 0 11.5h3.333a.75.75 0 1 0 0-1.5H8.334a4.25 4.25 0 0 1-4.25-4.25"/>',
|
|
9
|
+
bold: '<path d="M6.33 10h4.17a3 3 0 0 0 0-6H6.33v12H11a3 3 0 0 0 0-6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
10
|
+
italic: '<path d="M8 4h6M6 16h6M12 4L8 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
11
|
+
underline: '<path d="M5.67 3.33v6a4.33 4.33 0 0 0 8.66 0v-6M4.67 17h10.66" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
12
|
+
strike: '<path d="M14 5.33c-.67-1.17-2.04-1.83-4-1.83-2.33 0-4 1.17-4 3 0 1.67 1.5 2.67 4 3.5s4 1.83 4 3.5c0 1.83-1.67 3-4 3-1.96 0-3.33-.66-4-1.83M4.33 10h11.34" fill="none" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
13
|
+
align: '<path d="M3 5h14M3 10h10M3 15h12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>',
|
|
14
|
+
zoomOut: '<path d="M4.5 10h11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>',
|
|
15
|
+
zoomIn: '<path d="M10 4.5v11M4.5 10h11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>',
|
|
16
|
+
fit: '<path d="M4.335 11a.665.665 0 0 1 1.33 0v3.335H9l.134.014a.665.665 0 0 1 0 1.302L9 15.665H5A.665.665 0 0 1 4.335 15zm10-2V5.665H11a.665.665 0 0 1 0-1.33h4l.134.014c.303.062.531.33.531.651v4a.665.665 0 1 1-1.33 0"/>',
|
|
17
|
+
search: '<circle cx="8.8" cy="8.8" r="5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M12.6 12.6l3.8 3.8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
|
|
18
|
+
paragraph: '<path d="M6.75 4.75h10M3.25 8.25h13.5M3.25 11.75h13.5M3.25 15.25h8.5"/>',
|
|
19
|
+
heading: '<path d="M5 4v12M15 4v12M5 10h10"/>',
|
|
20
|
+
table: '<rect x="3" y="4" width="14" height="12" rx="2"/><path d="M3 8h14M3 12h14M10 8v8"/>',
|
|
21
|
+
callout: '<rect x="3" y="3.5" width="14" height="13" rx="2.25"/><path d="M7 7v3M10.5 8h3M10.5 12h3"/><circle cx="7" cy="12.75" r="0.75" fill="currentColor" stroke="none"/>',
|
|
22
|
+
image: '<rect x="3" y="3" width="14" height="14" rx="2.25"/><circle cx="12.75" cy="7.25" r="1.5"/><path d="m3 13 3.5-3.5a1.4 1.4 0 0 1 2 0l7.1 7.1"/>',
|
|
23
|
+
pageBreak: '<path d="M4 7V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2M4 13v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2M3 10h2m4 0h2m4 0h2"/>',
|
|
24
|
+
chevronDown: '<path d="M15.78 7.404a.666.666 0 0 1 .94.942L10.766 14.3a1.08 1.08 0 0 1-1.53 0L3.28 8.346a.666.666 0 0 1 .942-.942L10 13.184z"/>',
|
|
25
|
+
check: '<path d="M14.445 5.038a.665.665 0 0 1 1.1.748l-6.14 9.03a.815.815 0 0 1-1.25.117L4.528 11.3a.665.665 0 0 1 .942-.94l3.184 3.192z"/>',
|
|
26
|
+
ask: '<path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="1.45" d="M10 16.83c4.004 0 7.25-3.058 7.25-6.83S14.004 3.17 10 3.17 2.75 6.228 2.75 10a6.55 6.55 0 0 0 1.484 4.141c-.147.781-.39 1.537-.678 2.287a13.5 13.5 0 0 0 2.877-.48A7.56 7.56 0 0 0 10 16.83Z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.45" d="M5.888 9C6.278 7.575 7.478 6.457 9 6.11"/>',
|
|
27
|
+
};
|
|
28
|
+
export function formatAskAssistantLabel(assistantName) {
|
|
29
|
+
const resolved = assistantName?.trim() || 'AI';
|
|
30
|
+
return /^[\x00-\x7f]/.test(resolved) ? `询问 ${resolved}` : `询问${resolved}`;
|
|
31
|
+
}
|
|
32
|
+
function makeSvgIcon(markup, size = 16) {
|
|
33
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
34
|
+
svg.setAttribute('width', String(size));
|
|
35
|
+
svg.setAttribute('height', String(size));
|
|
36
|
+
svg.setAttribute('viewBox', '0 0 20 20');
|
|
37
|
+
svg.setAttribute('fill', 'currentColor');
|
|
38
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
39
|
+
svg.innerHTML = markup;
|
|
40
|
+
return svg;
|
|
41
|
+
}
|
|
42
|
+
function el(tag, className, text) {
|
|
43
|
+
const node = document.createElement(tag);
|
|
44
|
+
if (className)
|
|
45
|
+
node.className = className;
|
|
46
|
+
if (text !== undefined)
|
|
47
|
+
node.textContent = text;
|
|
48
|
+
return node;
|
|
49
|
+
}
|
|
50
|
+
function mountToolbarScroller(toolbar, history, zoom, onScrollOrResize) {
|
|
51
|
+
const center = el('div', 'idocs-ws-tb-center');
|
|
52
|
+
const viewport = el('div', 'idocs-ws-toolbar-viewport');
|
|
53
|
+
const strip = el('div', 'idocs-ws-toolbar-strip');
|
|
54
|
+
const content = el('div', 'idocs-ws-toolbar-content');
|
|
55
|
+
const scrollButton = (direction, label) => {
|
|
56
|
+
const button = el('button', 'idocs-ws-icon-btn idocs-ws-toolbar-arrow');
|
|
57
|
+
button.type = 'button';
|
|
58
|
+
button.hidden = true;
|
|
59
|
+
button.title = label;
|
|
60
|
+
button.setAttribute('aria-label', label);
|
|
61
|
+
button.append(makeSvgIcon(ICONS.chevronDown, 14));
|
|
62
|
+
button.addEventListener('mousedown', (event) => event.preventDefault());
|
|
63
|
+
button.addEventListener('click', () => {
|
|
64
|
+
viewport.scrollBy({
|
|
65
|
+
left: direction * viewport.clientWidth * 0.75,
|
|
66
|
+
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return button;
|
|
70
|
+
};
|
|
71
|
+
const left = scrollButton(-1, '向左滚动格式工具栏');
|
|
72
|
+
const right = scrollButton(1, '向右滚动格式工具栏');
|
|
73
|
+
strip.append(content);
|
|
74
|
+
viewport.append(strip);
|
|
75
|
+
center.append(left, viewport, right);
|
|
76
|
+
toolbar.append(history, center, zoom);
|
|
77
|
+
const syncEdges = () => {
|
|
78
|
+
left.disabled = viewport.scrollLeft <= 1;
|
|
79
|
+
right.disabled = viewport.scrollLeft + viewport.clientWidth >= viewport.scrollWidth - 1;
|
|
80
|
+
onScrollOrResize();
|
|
81
|
+
};
|
|
82
|
+
let compact = false;
|
|
83
|
+
const resize = new ResizeObserver(() => {
|
|
84
|
+
const { paddingLeft, paddingRight, columnGap } = getComputedStyle(toolbar);
|
|
85
|
+
const innerWidth = toolbar.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
|
86
|
+
const available = innerWidth - history.offsetWidth - zoom.offsetWidth - 2 * parseFloat(columnGap);
|
|
87
|
+
const nextCompact = available < Math.min(144, content.offsetWidth);
|
|
88
|
+
if (nextCompact !== compact) {
|
|
89
|
+
compact = nextCompact;
|
|
90
|
+
if (compact) {
|
|
91
|
+
strip.prepend(history);
|
|
92
|
+
strip.append(zoom);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
toolbar.prepend(history);
|
|
96
|
+
toolbar.append(zoom);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const overflow = strip.offsetWidth > center.clientWidth;
|
|
100
|
+
left.hidden = !overflow;
|
|
101
|
+
right.hidden = !overflow;
|
|
102
|
+
syncEdges();
|
|
103
|
+
});
|
|
104
|
+
for (const element of [toolbar, content, history, zoom])
|
|
105
|
+
resize.observe(element);
|
|
106
|
+
viewport.addEventListener('scroll', syncEdges, { passive: true });
|
|
107
|
+
return {
|
|
108
|
+
content,
|
|
109
|
+
viewport,
|
|
110
|
+
destroy() {
|
|
111
|
+
resize.disconnect();
|
|
112
|
+
viewport.removeEventListener('scroll', syncEdges);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function headingLevelFromStyle(style) {
|
|
117
|
+
switch (style) {
|
|
118
|
+
case 'heading1':
|
|
119
|
+
return 1;
|
|
120
|
+
case 'heading2':
|
|
121
|
+
return 2;
|
|
122
|
+
case 'heading3':
|
|
123
|
+
return 3;
|
|
124
|
+
default:
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function extractOutline(doc) {
|
|
129
|
+
const entries = [];
|
|
130
|
+
let lastPath = '';
|
|
131
|
+
for (let pageIndex = 0; pageIndex < doc.pages.length; pageIndex += 1) {
|
|
132
|
+
for (const line of doc.pages[pageIndex].lines) {
|
|
133
|
+
if (line.story !== 'body')
|
|
134
|
+
continue;
|
|
135
|
+
const seg = line.sourceSegments?.find((s) => s.source.kind === 'editable');
|
|
136
|
+
if (!seg || seg.source.kind !== 'editable')
|
|
137
|
+
continue;
|
|
138
|
+
const level = headingLevelFromStyle(seg.source.blockStyle);
|
|
139
|
+
if (!level)
|
|
140
|
+
continue;
|
|
141
|
+
const fragment = line.sourceSegments
|
|
142
|
+
?.filter((s) => s.source.kind === 'editable')
|
|
143
|
+
.map((s) => line.text.slice(s.start, s.end))
|
|
144
|
+
.join('')
|
|
145
|
+
.trim();
|
|
146
|
+
const piece = fragment || line.text.trim();
|
|
147
|
+
if (entries.length > 0 && line.path === lastPath) {
|
|
148
|
+
const prev = entries[entries.length - 1];
|
|
149
|
+
entries[entries.length - 1] = {
|
|
150
|
+
...prev,
|
|
151
|
+
text: piece ? `${prev.text}${prev.text ? ' ' : ''}${piece}` : prev.text,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
lastPath = line.path;
|
|
156
|
+
entries.push({
|
|
157
|
+
path: line.path,
|
|
158
|
+
level,
|
|
159
|
+
text: piece || `标题 ${entries.length + 1}`,
|
|
160
|
+
page: pageIndex,
|
|
161
|
+
anchor: seg.source.stops[0]?.anchor,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
export function isTableCellPath(path) {
|
|
169
|
+
return /\.rows\.\d+\.cells\.\d+(?:\.|$)/.test(path);
|
|
170
|
+
}
|
|
171
|
+
function findEditableLineMeta(doc, page, flow) {
|
|
172
|
+
const targetPage = doc?.pages[page];
|
|
173
|
+
if (!targetPage)
|
|
174
|
+
return {};
|
|
175
|
+
for (const line of targetPage.lines) {
|
|
176
|
+
for (const seg of line.sourceSegments ?? []) {
|
|
177
|
+
if (seg.source.kind === 'editable' && seg.source.stops.some((stop) => stop.anchor.flow === flow)) {
|
|
178
|
+
return {
|
|
179
|
+
blockStyle: seg.source.blockStyle,
|
|
180
|
+
align: seg.source.align,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
export function resolveToolbarContext(input) {
|
|
188
|
+
if (input.readOnly) {
|
|
189
|
+
const processing = input.processing ?? Boolean(input.busyLabel);
|
|
190
|
+
return {
|
|
191
|
+
kind: 'readOnly',
|
|
192
|
+
processing,
|
|
193
|
+
label: input.busyLabel || (processing ? '正在修订文档' : '只读'),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (input.readOnlyTarget) {
|
|
197
|
+
return { kind: 'readOnlyTarget', target: input.readOnlyTarget };
|
|
198
|
+
}
|
|
199
|
+
if (input.selectedImage) {
|
|
200
|
+
return { kind: 'image', target: input.selectedImage };
|
|
201
|
+
}
|
|
202
|
+
if (input.selection) {
|
|
203
|
+
const { range } = input.selection.selection;
|
|
204
|
+
const collapsed = range.start.flow === range.end.flow && range.start.offset === range.end.offset;
|
|
205
|
+
const meta = findEditableLineMeta(input.document, input.selection.page, range.start.flow);
|
|
206
|
+
return {
|
|
207
|
+
kind: 'editable',
|
|
208
|
+
inTable: isTableCellPath(input.selection.path),
|
|
209
|
+
blockStyle: meta.blockStyle,
|
|
210
|
+
align: meta.align,
|
|
211
|
+
collapsed,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return { kind: 'empty' };
|
|
215
|
+
}
|
|
216
|
+
function sameImageList(a, b) {
|
|
217
|
+
if (!a || a.length !== b.length)
|
|
218
|
+
return false;
|
|
219
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
220
|
+
if (a[i].missing !== b[i].missing ||
|
|
221
|
+
a[i].format !== b[i].format ||
|
|
222
|
+
a[i].washout !== b[i].washout ||
|
|
223
|
+
a[i].bytes !== b[i].bytes) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
function toolbarRenderKey(ctx, tableModePreferred) {
|
|
230
|
+
switch (ctx.kind) {
|
|
231
|
+
case 'readOnly':
|
|
232
|
+
return `ro:${ctx.processing}:${ctx.label}`;
|
|
233
|
+
case 'editable':
|
|
234
|
+
if (ctx.inTable && tableModePreferred)
|
|
235
|
+
return 'table';
|
|
236
|
+
return `ed:${ctx.inTable}:${ctx.blockStyle ?? ''}:${ctx.align ?? ''}:${ctx.collapsed}`;
|
|
237
|
+
case 'empty':
|
|
238
|
+
return 'empty';
|
|
239
|
+
case 'readOnlyTarget':
|
|
240
|
+
case 'image':
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function mountEditorWorkspace(container, session, options = {}) {
|
|
245
|
+
ensureWorkspaceStyles();
|
|
246
|
+
let readOnly = options.readOnly === true;
|
|
247
|
+
let busyLabel = options.busyLabel;
|
|
248
|
+
let processing = readOnly && (options.processing ?? Boolean(options.busyLabel));
|
|
249
|
+
let railOpen = options.railOpen ?? false;
|
|
250
|
+
let railTab = 'pages';
|
|
251
|
+
let tableModePreferred = false;
|
|
252
|
+
let lastToolbarKey = '';
|
|
253
|
+
let lastOutlinePath;
|
|
254
|
+
let activeDoc;
|
|
255
|
+
let renderedRailDoc;
|
|
256
|
+
let renderedRailTab;
|
|
257
|
+
let thumbImages = [];
|
|
258
|
+
let lastThumbImages = [];
|
|
259
|
+
let destroyed = false;
|
|
260
|
+
const root = el('div', 'idocs-ws');
|
|
261
|
+
root.classList.toggle('is-readonly', readOnly);
|
|
262
|
+
root.classList.toggle('is-processing', processing);
|
|
263
|
+
const toolbar = el('nav', 'idocs-ws-toolbar');
|
|
264
|
+
toolbar.setAttribute('aria-label', '文档格式工具栏');
|
|
265
|
+
const tbLeft = el('div', 'idocs-ws-tb-left');
|
|
266
|
+
const tbRight = el('div', 'idocs-ws-tb-right');
|
|
267
|
+
let openToolbarMenu;
|
|
268
|
+
const closeToolbarMenu = () => {
|
|
269
|
+
if (!openToolbarMenu)
|
|
270
|
+
return;
|
|
271
|
+
openToolbarMenu.trigger.setAttribute('aria-expanded', 'false');
|
|
272
|
+
openToolbarMenu.menu.hidden = true;
|
|
273
|
+
openToolbarMenu = undefined;
|
|
274
|
+
};
|
|
275
|
+
const positionToolbarMenu = () => {
|
|
276
|
+
if (!openToolbarMenu)
|
|
277
|
+
return;
|
|
278
|
+
const { trigger, menu } = openToolbarMenu;
|
|
279
|
+
const anchor = trigger.getBoundingClientRect();
|
|
280
|
+
const visible = toolbarScroller.viewport.getBoundingClientRect();
|
|
281
|
+
if (anchor.right <= visible.left || anchor.left >= visible.right) {
|
|
282
|
+
closeToolbarMenu();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const frame = toolbar.getBoundingClientRect();
|
|
286
|
+
const gutter = 4;
|
|
287
|
+
const anchorLeft = Math.max(anchor.left, visible.left) - frame.left;
|
|
288
|
+
menu.style.left = `${Math.max(gutter, Math.min(anchorLeft, toolbar.clientWidth - menu.offsetWidth - gutter))}px`;
|
|
289
|
+
menu.style.top = `${anchor.bottom - frame.top + 6}px`;
|
|
290
|
+
};
|
|
291
|
+
const toolbarScroller = mountToolbarScroller(toolbar, tbLeft, tbRight, positionToolbarMenu);
|
|
292
|
+
const tbContent = toolbarScroller.content;
|
|
293
|
+
const toggleToolbarMenu = (trigger, menu, open) => {
|
|
294
|
+
closeAllPopovers();
|
|
295
|
+
if (!open)
|
|
296
|
+
return;
|
|
297
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
298
|
+
menu.hidden = false;
|
|
299
|
+
openToolbarMenu = { trigger, menu };
|
|
300
|
+
positionToolbarMenu();
|
|
301
|
+
};
|
|
302
|
+
const railToggleBtn = el('button', 'idocs-ws-icon-btn');
|
|
303
|
+
railToggleBtn.type = 'button';
|
|
304
|
+
const initialRailLabel = railOpen ? '收起页面列表' : '展开页面列表';
|
|
305
|
+
railToggleBtn.title = initialRailLabel;
|
|
306
|
+
railToggleBtn.setAttribute('aria-label', initialRailLabel);
|
|
307
|
+
railToggleBtn.setAttribute('aria-expanded', String(railOpen));
|
|
308
|
+
railToggleBtn.append(makeSvgIcon(ICONS.rail, 16));
|
|
309
|
+
const undoBtn = el('button', 'idocs-ws-icon-btn');
|
|
310
|
+
undoBtn.type = 'button';
|
|
311
|
+
undoBtn.title = '撤销';
|
|
312
|
+
undoBtn.setAttribute('aria-label', '撤销');
|
|
313
|
+
undoBtn.append(makeSvgIcon(ICONS.undo, 16));
|
|
314
|
+
const redoBtn = el('button', 'idocs-ws-icon-btn');
|
|
315
|
+
redoBtn.type = 'button';
|
|
316
|
+
redoBtn.title = '重做';
|
|
317
|
+
redoBtn.setAttribute('aria-label', '重做');
|
|
318
|
+
redoBtn.append(makeSvgIcon(ICONS.redo, 16));
|
|
319
|
+
tbLeft.append(railToggleBtn, undoBtn, redoBtn);
|
|
320
|
+
const zoomOutBtn = el('button', 'idocs-ws-icon-btn');
|
|
321
|
+
zoomOutBtn.type = 'button';
|
|
322
|
+
zoomOutBtn.title = '缩小';
|
|
323
|
+
zoomOutBtn.setAttribute('aria-label', '缩小');
|
|
324
|
+
zoomOutBtn.append(makeSvgIcon(ICONS.zoomOut));
|
|
325
|
+
const zoomLabel = el('span', 'idocs-ws-zoom-label', '100%');
|
|
326
|
+
const zoomInBtn = el('button', 'idocs-ws-icon-btn');
|
|
327
|
+
zoomInBtn.type = 'button';
|
|
328
|
+
zoomInBtn.title = '放大';
|
|
329
|
+
zoomInBtn.setAttribute('aria-label', '放大');
|
|
330
|
+
zoomInBtn.append(makeSvgIcon(ICONS.zoomIn));
|
|
331
|
+
const fitWidthBtn = el('button', 'idocs-ws-icon-btn');
|
|
332
|
+
fitWidthBtn.type = 'button';
|
|
333
|
+
fitWidthBtn.title = '适应宽度';
|
|
334
|
+
fitWidthBtn.setAttribute('aria-label', '适应宽度');
|
|
335
|
+
fitWidthBtn.append(makeSvgIcon(ICONS.fit, 18));
|
|
336
|
+
const findBtn = el('button', 'idocs-ws-icon-btn');
|
|
337
|
+
findBtn.type = 'button';
|
|
338
|
+
findBtn.title = '查找';
|
|
339
|
+
findBtn.setAttribute('aria-label', '查找');
|
|
340
|
+
findBtn.append(makeSvgIcon(ICONS.search));
|
|
341
|
+
const zoomDivider = el('span', 'idocs-ws-divider');
|
|
342
|
+
zoomDivider.setAttribute('aria-hidden', 'true');
|
|
343
|
+
tbRight.append(zoomOutBtn, zoomLabel, zoomInBtn, zoomDivider, fitWidthBtn, findBtn);
|
|
344
|
+
const body = el('div', 'idocs-ws-body');
|
|
345
|
+
body.classList.toggle('is-rail-collapsed', !railOpen);
|
|
346
|
+
const railSlot = el('div', 'idocs-ws-rail-slot');
|
|
347
|
+
const rail = el('aside', 'idocs-ws-rail');
|
|
348
|
+
if ('inert' in rail)
|
|
349
|
+
rail.inert = !railOpen;
|
|
350
|
+
rail.setAttribute('aria-hidden', String(!railOpen));
|
|
351
|
+
const railTabs = el('div', 'idocs-ws-rail-tabs');
|
|
352
|
+
railTabs.setAttribute('role', 'tablist');
|
|
353
|
+
const pagesTabBtn = el('button', 'idocs-ws-rail-tab', '页面');
|
|
354
|
+
pagesTabBtn.type = 'button';
|
|
355
|
+
pagesTabBtn.setAttribute('role', 'tab');
|
|
356
|
+
pagesTabBtn.setAttribute('aria-selected', 'true');
|
|
357
|
+
const outlineTabBtn = el('button', 'idocs-ws-rail-tab', '大纲');
|
|
358
|
+
outlineTabBtn.type = 'button';
|
|
359
|
+
outlineTabBtn.setAttribute('role', 'tab');
|
|
360
|
+
outlineTabBtn.setAttribute('aria-selected', 'false');
|
|
361
|
+
railTabs.append(pagesTabBtn, outlineTabBtn);
|
|
362
|
+
const railList = el('div', 'idocs-ws-rail-list');
|
|
363
|
+
rail.append(railTabs, railList);
|
|
364
|
+
const stage = el('div', 'idocs-ws-stage');
|
|
365
|
+
const editorHost = el('div', 'idocs-ws-editor-host');
|
|
366
|
+
const askLabel = formatAskAssistantLabel(options.assistantName);
|
|
367
|
+
const selPill = el('div', 'idocs-ws-sel-pill');
|
|
368
|
+
selPill.hidden = true;
|
|
369
|
+
const pillQuoteBtn = options.onQuoteSelection ? el('button', 'idocs-ws-sel-pill-btn') : null;
|
|
370
|
+
if (pillQuoteBtn) {
|
|
371
|
+
pillQuoteBtn.type = 'button';
|
|
372
|
+
const askSvg = makeSvgIcon(ICONS.ask, 16);
|
|
373
|
+
askSvg.classList.add('idocs-ws-sel-pill-icon');
|
|
374
|
+
pillQuoteBtn.append(askSvg, el('span', undefined, askLabel));
|
|
375
|
+
pillQuoteBtn.addEventListener('mousedown', (event) => event.preventDefault());
|
|
376
|
+
selPill.append(pillQuoteBtn);
|
|
377
|
+
}
|
|
378
|
+
const dock = el('nav', 'idocs-ws-dock');
|
|
379
|
+
dock.setAttribute('aria-label', '插入内容');
|
|
380
|
+
dock.hidden = readOnly;
|
|
381
|
+
const insertButtons = [];
|
|
382
|
+
const makeDockButton = (icon, label, activate, popup) => {
|
|
383
|
+
const container = el('span');
|
|
384
|
+
const className = 'idocs-ws-dock-btn';
|
|
385
|
+
const svg = makeSvgIcon(icon, 20);
|
|
386
|
+
const bind = (button) => {
|
|
387
|
+
if (!button)
|
|
388
|
+
return;
|
|
389
|
+
if (popup)
|
|
390
|
+
button.setAttribute('aria-expanded', String(!popup.hidden));
|
|
391
|
+
button.append(svg);
|
|
392
|
+
};
|
|
393
|
+
if (options.mountInsertButtons) {
|
|
394
|
+
insertButtons.push({ container, label, className, activate, bind });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const button = el('button', className);
|
|
398
|
+
button.type = 'button';
|
|
399
|
+
button.title = label;
|
|
400
|
+
button.setAttribute('aria-label', label);
|
|
401
|
+
button.addEventListener('click', activate);
|
|
402
|
+
bind(button);
|
|
403
|
+
container.append(button);
|
|
404
|
+
}
|
|
405
|
+
return container;
|
|
406
|
+
};
|
|
407
|
+
const tableAnchor = el('div', 'idocs-ws-popover-anchor');
|
|
408
|
+
const tablePopover = el('div', 'idocs-ws-popover is-up idocs-ws-table-popover');
|
|
409
|
+
tablePopover.hidden = true;
|
|
410
|
+
const tableGridHead = el('div', 'idocs-ws-table-grid-head', '3 × 3 表格');
|
|
411
|
+
const tableGrid = el('div', 'idocs-ws-table-grid');
|
|
412
|
+
const tableCells = [];
|
|
413
|
+
let hoverRows = 3;
|
|
414
|
+
let hoverCols = 3;
|
|
415
|
+
for (let r = 1; r <= 6; r += 1) {
|
|
416
|
+
for (let c = 1; c <= 6; c += 1) {
|
|
417
|
+
const cell = el('button', 'idocs-ws-table-cell');
|
|
418
|
+
cell.type = 'button';
|
|
419
|
+
cell.dataset.rows = String(r);
|
|
420
|
+
cell.dataset.cols = String(c);
|
|
421
|
+
cell.setAttribute('aria-label', `${r} 行 ${c} 列`);
|
|
422
|
+
tableCells.push(cell);
|
|
423
|
+
tableGrid.append(cell);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
tablePopover.append(tableGridHead, tableGrid);
|
|
427
|
+
tableAnchor.append(tablePopover);
|
|
428
|
+
stage.append(editorHost, selPill, dock);
|
|
429
|
+
railSlot.append(rail);
|
|
430
|
+
body.append(railSlot, stage);
|
|
431
|
+
root.append(toolbar, body);
|
|
432
|
+
container.append(root);
|
|
433
|
+
const paintTableGrid = (rows, cols) => {
|
|
434
|
+
hoverRows = rows;
|
|
435
|
+
hoverCols = cols;
|
|
436
|
+
tableGridHead.textContent = `${rows} × ${cols} 表格`;
|
|
437
|
+
for (const cell of tableCells) {
|
|
438
|
+
const r = Number(cell.dataset.rows);
|
|
439
|
+
const c = Number(cell.dataset.cols);
|
|
440
|
+
cell.classList.toggle('is-hot', r <= rows && c <= cols);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
paintTableGrid(3, 3);
|
|
444
|
+
const setTablePopoverOpen = (open) => {
|
|
445
|
+
tablePopover.hidden = !open;
|
|
446
|
+
tableAnchor.querySelector('.idocs-ws-dock-btn')?.setAttribute('aria-expanded', String(open));
|
|
447
|
+
};
|
|
448
|
+
const closeAllPopovers = () => {
|
|
449
|
+
setTablePopoverOpen(false);
|
|
450
|
+
closeToolbarMenu();
|
|
451
|
+
};
|
|
452
|
+
let view;
|
|
453
|
+
const syncZoom = () => {
|
|
454
|
+
zoomLabel.textContent = `${Math.round(view.zoom * 100)}%`;
|
|
455
|
+
};
|
|
456
|
+
const updatePill = (sel) => {
|
|
457
|
+
if (readOnly ||
|
|
458
|
+
view?.dragging ||
|
|
459
|
+
!sel ||
|
|
460
|
+
!sel.clientRect ||
|
|
461
|
+
!pillQuoteBtn ||
|
|
462
|
+
!sel.excerpt?.trim()) {
|
|
463
|
+
selPill.hidden = true;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
selPill.hidden = false;
|
|
467
|
+
const stageRect = stage.getBoundingClientRect();
|
|
468
|
+
const pillWidth = selPill.offsetWidth || 92;
|
|
469
|
+
const pillHeight = selPill.offsetHeight || 32;
|
|
470
|
+
const left = Math.max(8, Math.min(stageRect.width - pillWidth - 8, sel.clientRect.left - stageRect.left + sel.clientRect.width / 2 - pillWidth / 2));
|
|
471
|
+
const top = Math.max(8, sel.clientRect.top - stageRect.top - pillHeight - 8);
|
|
472
|
+
selPill.style.left = `${Math.round(left)}px`;
|
|
473
|
+
selPill.style.top = `${Math.round(top)}px`;
|
|
474
|
+
};
|
|
475
|
+
const renderToolbarCenter = () => {
|
|
476
|
+
const sel = view.selection();
|
|
477
|
+
const ctx = resolveToolbarContext({
|
|
478
|
+
readOnly,
|
|
479
|
+
processing,
|
|
480
|
+
busyLabel,
|
|
481
|
+
readOnlyTarget: view.readOnlyTarget(),
|
|
482
|
+
selectedImage: view.selectedImage(),
|
|
483
|
+
selection: sel,
|
|
484
|
+
document: activeDoc,
|
|
485
|
+
});
|
|
486
|
+
const nextKey = toolbarRenderKey(ctx, tableModePreferred);
|
|
487
|
+
if (nextKey !== null && nextKey === lastToolbarKey) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
lastToolbarKey = nextKey ?? '';
|
|
491
|
+
closeToolbarMenu();
|
|
492
|
+
tbContent.replaceChildren();
|
|
493
|
+
if (ctx.kind === 'readOnly') {
|
|
494
|
+
if (ctx.processing) {
|
|
495
|
+
const status = el('span', 'idocs-ws-processing-indicator');
|
|
496
|
+
status.setAttribute('role', 'status');
|
|
497
|
+
status.setAttribute('aria-label', ctx.label);
|
|
498
|
+
status.title = ctx.label;
|
|
499
|
+
const ring = el('span', 'idocs-ws-spinner');
|
|
500
|
+
ring.setAttribute('aria-hidden', 'true');
|
|
501
|
+
status.append(ring);
|
|
502
|
+
tbContent.append(status);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (ctx.kind === 'readOnlyTarget') {
|
|
507
|
+
const box = el('div', 'idocs-ws-ctx is-readonly-target');
|
|
508
|
+
box.append(el('span', 'idocs-ws-badge is-warn', '此内容暂不支持编辑'));
|
|
509
|
+
if (options.onQuoteSelection && ctx.target.excerpt?.trim()) {
|
|
510
|
+
const quoteBtn = el('button', 'idocs-ws-pill-btn');
|
|
511
|
+
quoteBtn.type = 'button';
|
|
512
|
+
quoteBtn.append(makeSvgIcon(ICONS.ask, 15), el('span', undefined, askLabel));
|
|
513
|
+
quoteBtn.addEventListener('click', () => {
|
|
514
|
+
const target = view.readOnlyTarget();
|
|
515
|
+
if (!target?.excerpt?.trim())
|
|
516
|
+
return;
|
|
517
|
+
options.onQuoteSelection?.({ text: target.excerpt, path: target.path, page: target.page });
|
|
518
|
+
});
|
|
519
|
+
box.append(quoteBtn);
|
|
520
|
+
}
|
|
521
|
+
const clearBtn = el('button', 'idocs-ws-pill-btn', '取消选中');
|
|
522
|
+
clearBtn.type = 'button';
|
|
523
|
+
clearBtn.addEventListener('click', () => {
|
|
524
|
+
view.clearReadOnlyTarget();
|
|
525
|
+
view.focus();
|
|
526
|
+
});
|
|
527
|
+
box.append(clearBtn);
|
|
528
|
+
tbContent.append(box);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (ctx.kind === 'image') {
|
|
532
|
+
const box = el('div', 'idocs-ws-ctx');
|
|
533
|
+
box.append(el('span', 'idocs-ws-badge', '插图'));
|
|
534
|
+
for (const ratio of [0.5, 0.75, 1]) {
|
|
535
|
+
const btn = el('button', 'idocs-ws-pill-btn', `宽度 ${Math.round(ratio * 100)}%`);
|
|
536
|
+
btn.type = 'button';
|
|
537
|
+
btn.addEventListener('click', () => {
|
|
538
|
+
void view.editImage(ctx.target.path, { widthRatio: ratio });
|
|
539
|
+
});
|
|
540
|
+
box.append(btn);
|
|
541
|
+
}
|
|
542
|
+
box.append(el('span', 'idocs-ws-divider'));
|
|
543
|
+
for (const [align, label] of [
|
|
544
|
+
['left', '左对齐'],
|
|
545
|
+
['center', '居中'],
|
|
546
|
+
['right', '右对齐'],
|
|
547
|
+
]) {
|
|
548
|
+
const btn = el('button', 'idocs-ws-pill-btn', label);
|
|
549
|
+
btn.type = 'button';
|
|
550
|
+
btn.addEventListener('click', () => {
|
|
551
|
+
void view.editImage(ctx.target.path, { align });
|
|
552
|
+
});
|
|
553
|
+
box.append(btn);
|
|
554
|
+
}
|
|
555
|
+
if (options.onPickImage) {
|
|
556
|
+
const replaceBtn = el('button', 'idocs-ws-pill-btn', '替换图片');
|
|
557
|
+
replaceBtn.type = 'button';
|
|
558
|
+
replaceBtn.addEventListener('click', async () => {
|
|
559
|
+
const picked = await options.onPickImage?.();
|
|
560
|
+
if (picked && !destroyed) {
|
|
561
|
+
await view.editImage(ctx.target.path, { src: picked.src, alt: picked.alt });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
box.append(replaceBtn);
|
|
565
|
+
}
|
|
566
|
+
const doneBtn = el('button', 'idocs-ws-pill-btn', '完成');
|
|
567
|
+
doneBtn.type = 'button';
|
|
568
|
+
doneBtn.addEventListener('click', () => view.clearSelectedImage());
|
|
569
|
+
box.append(doneBtn);
|
|
570
|
+
tbContent.append(box);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const inTable = ctx.kind === 'editable' && ctx.inTable;
|
|
574
|
+
const showTable = inTable && tableModePreferred;
|
|
575
|
+
const box = el('div', showTable ? 'idocs-ws-ctx is-table' : 'idocs-ws-ctx');
|
|
576
|
+
if (inTable) {
|
|
577
|
+
const seg = el('div', 'idocs-ws-seg');
|
|
578
|
+
const textTab = el('button', 'idocs-ws-seg-btn', '文字');
|
|
579
|
+
textTab.type = 'button';
|
|
580
|
+
textTab.setAttribute('aria-pressed', String(!showTable));
|
|
581
|
+
textTab.addEventListener('click', () => {
|
|
582
|
+
tableModePreferred = false;
|
|
583
|
+
renderToolbarCenter();
|
|
584
|
+
});
|
|
585
|
+
const tableTab = el('button', 'idocs-ws-seg-btn', '表格');
|
|
586
|
+
tableTab.type = 'button';
|
|
587
|
+
tableTab.setAttribute('aria-pressed', String(showTable));
|
|
588
|
+
tableTab.addEventListener('click', () => {
|
|
589
|
+
tableModePreferred = true;
|
|
590
|
+
renderToolbarCenter();
|
|
591
|
+
});
|
|
592
|
+
seg.append(textTab, tableTab);
|
|
593
|
+
box.append(seg, el('span', 'idocs-ws-divider'));
|
|
594
|
+
}
|
|
595
|
+
if (showTable) {
|
|
596
|
+
const tableActions = [
|
|
597
|
+
['insertRowAbove', '上方插入行'],
|
|
598
|
+
['insertRowBelow', '下方插入行'],
|
|
599
|
+
['insertColumnLeft', '左侧插入列'],
|
|
600
|
+
['insertColumnRight', '右侧插入列'],
|
|
601
|
+
['deleteRow', '删除行', true],
|
|
602
|
+
['deleteColumn', '删除列', true],
|
|
603
|
+
['toggleHeaderRow', '重复表头'],
|
|
604
|
+
];
|
|
605
|
+
for (const [action, label, danger] of tableActions) {
|
|
606
|
+
const btn = el('button', danger ? 'idocs-ws-pill-btn is-danger' : 'idocs-ws-pill-btn', label);
|
|
607
|
+
btn.type = 'button';
|
|
608
|
+
if (action === 'toggleHeaderRow')
|
|
609
|
+
btn.title = '开启或关闭跨页重复表头';
|
|
610
|
+
btn.addEventListener('click', () => {
|
|
611
|
+
void view.editTable(action);
|
|
612
|
+
});
|
|
613
|
+
box.append(btn);
|
|
614
|
+
}
|
|
615
|
+
tbContent.append(box);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const styleOptions = [
|
|
619
|
+
['body', '正文'],
|
|
620
|
+
['heading1', '一级标题'],
|
|
621
|
+
['heading2', '二级标题'],
|
|
622
|
+
['heading3', '三级标题'],
|
|
623
|
+
];
|
|
624
|
+
const activeStyle = ctx.kind === 'editable' ? ctx.blockStyle : undefined;
|
|
625
|
+
const selectedStyle = activeStyle ?? 'body';
|
|
626
|
+
const selectedLabel = styleOptions.find(([v]) => v === selectedStyle)?.[1] ?? '正文';
|
|
627
|
+
const styleTrigger = el('button', 'idocs-ws-select-trigger');
|
|
628
|
+
styleTrigger.type = 'button';
|
|
629
|
+
styleTrigger.setAttribute('role', 'combobox');
|
|
630
|
+
styleTrigger.setAttribute('aria-haspopup', 'listbox');
|
|
631
|
+
styleTrigger.setAttribute('aria-expanded', 'false');
|
|
632
|
+
styleTrigger.setAttribute('aria-label', '段落样式');
|
|
633
|
+
styleTrigger.disabled = ctx.kind !== 'editable' || activeStyle === undefined;
|
|
634
|
+
const styleValueSpan = el('span', 'idocs-ws-select-value', selectedLabel);
|
|
635
|
+
const chevronSvg = makeSvgIcon(ICONS.chevronDown, 14);
|
|
636
|
+
chevronSvg.classList.add('idocs-ws-select-chevron');
|
|
637
|
+
styleTrigger.append(styleValueSpan, chevronSvg);
|
|
638
|
+
styleTrigger.addEventListener('mousedown', (event) => event.preventDefault());
|
|
639
|
+
const stylePopover = el('div', 'idocs-ws-popover is-down idocs-ws-style-popover');
|
|
640
|
+
stylePopover.setAttribute('role', 'listbox');
|
|
641
|
+
stylePopover.setAttribute('aria-label', '段落样式');
|
|
642
|
+
stylePopover.hidden = true;
|
|
643
|
+
const styleItems = [];
|
|
644
|
+
let highlightedStyleIdx = Math.max(0, styleOptions.findIndex(([v]) => v === selectedStyle));
|
|
645
|
+
const syncStyleHighlight = (nextIdx) => {
|
|
646
|
+
highlightedStyleIdx = (nextIdx + styleItems.length) % styleItems.length;
|
|
647
|
+
styleItems.forEach((item, idx) => {
|
|
648
|
+
if (idx === highlightedStyleIdx)
|
|
649
|
+
item.dataset.highlighted = 'true';
|
|
650
|
+
else
|
|
651
|
+
delete item.dataset.highlighted;
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
for (let idx = 0; idx < styleOptions.length; idx += 1) {
|
|
655
|
+
const [val, label] = styleOptions[idx];
|
|
656
|
+
const isSelected = val === selectedStyle;
|
|
657
|
+
const item = el('button', isSelected ? 'idocs-ws-menu-item is-active' : 'idocs-ws-menu-item');
|
|
658
|
+
item.type = 'button';
|
|
659
|
+
item.setAttribute('role', 'option');
|
|
660
|
+
item.setAttribute('aria-selected', String(isSelected));
|
|
661
|
+
item.dataset.value = val;
|
|
662
|
+
item.append(el('span', 'idocs-ws-menu-label', label));
|
|
663
|
+
if (isSelected) {
|
|
664
|
+
const checkWrap = el('span', 'idocs-ws-menu-check');
|
|
665
|
+
checkWrap.setAttribute('aria-hidden', 'true');
|
|
666
|
+
checkWrap.append(makeSvgIcon(ICONS.check, 14));
|
|
667
|
+
item.append(checkWrap);
|
|
668
|
+
}
|
|
669
|
+
item.addEventListener('mousedown', (event) => event.preventDefault());
|
|
670
|
+
item.addEventListener('mouseenter', () => syncStyleHighlight(idx));
|
|
671
|
+
item.addEventListener('click', (event) => {
|
|
672
|
+
event.stopPropagation();
|
|
673
|
+
closeAllPopovers();
|
|
674
|
+
void view.setBlockStyle(val);
|
|
675
|
+
});
|
|
676
|
+
styleItems.push(item);
|
|
677
|
+
stylePopover.append(item);
|
|
678
|
+
}
|
|
679
|
+
const toggleStylePopover = (forceOpen) => {
|
|
680
|
+
if (styleTrigger.disabled)
|
|
681
|
+
return;
|
|
682
|
+
const shouldOpen = forceOpen ?? Boolean(stylePopover.hidden);
|
|
683
|
+
toggleToolbarMenu(styleTrigger, stylePopover, shouldOpen);
|
|
684
|
+
if (shouldOpen) {
|
|
685
|
+
syncStyleHighlight(Math.max(0, styleOptions.findIndex(([v]) => v === selectedStyle)));
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
styleTrigger.addEventListener('click', (event) => {
|
|
689
|
+
event.stopPropagation();
|
|
690
|
+
toggleStylePopover();
|
|
691
|
+
});
|
|
692
|
+
styleTrigger.addEventListener('keydown', (event) => {
|
|
693
|
+
if (event.key === 'ArrowDown') {
|
|
694
|
+
event.preventDefault();
|
|
695
|
+
if (stylePopover.hidden)
|
|
696
|
+
toggleStylePopover(true);
|
|
697
|
+
else
|
|
698
|
+
syncStyleHighlight(highlightedStyleIdx + 1);
|
|
699
|
+
}
|
|
700
|
+
else if (event.key === 'ArrowUp') {
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
if (stylePopover.hidden)
|
|
703
|
+
toggleStylePopover(true);
|
|
704
|
+
else
|
|
705
|
+
syncStyleHighlight(highlightedStyleIdx - 1);
|
|
706
|
+
}
|
|
707
|
+
else if ((event.key === 'Enter' || event.key === ' ') && !stylePopover.hidden) {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
const picked = styleOptions[highlightedStyleIdx]?.[0];
|
|
710
|
+
closeAllPopovers();
|
|
711
|
+
if (picked)
|
|
712
|
+
void view.setBlockStyle(picked);
|
|
713
|
+
}
|
|
714
|
+
else if (event.key === 'Escape' && !stylePopover.hidden) {
|
|
715
|
+
event.preventDefault();
|
|
716
|
+
event.stopPropagation();
|
|
717
|
+
closeAllPopovers();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
stylePopover.addEventListener('click', (event) => event.stopPropagation());
|
|
721
|
+
box.append(styleTrigger, stylePopover, el('span', 'idocs-ws-divider'));
|
|
722
|
+
const collapsed = ctx.kind !== 'editable' || ctx.collapsed;
|
|
723
|
+
const marks = [
|
|
724
|
+
['bold', '粗体', ICONS.bold],
|
|
725
|
+
['italic', '斜体', ICONS.italic],
|
|
726
|
+
['underline', '下划线', ICONS.underline],
|
|
727
|
+
['strike', '删除线', ICONS.strike],
|
|
728
|
+
];
|
|
729
|
+
for (const [mark, label, icon] of marks) {
|
|
730
|
+
const btn = el('button', 'idocs-ws-icon-btn');
|
|
731
|
+
btn.type = 'button';
|
|
732
|
+
btn.title = label;
|
|
733
|
+
btn.setAttribute('aria-label', label);
|
|
734
|
+
btn.disabled = collapsed;
|
|
735
|
+
btn.append(makeSvgIcon(icon));
|
|
736
|
+
btn.addEventListener('mousedown', (event) => event.preventDefault());
|
|
737
|
+
btn.addEventListener('click', () => {
|
|
738
|
+
void view.toggleMark(mark);
|
|
739
|
+
});
|
|
740
|
+
box.append(btn);
|
|
741
|
+
}
|
|
742
|
+
box.append(el('span', 'idocs-ws-divider'));
|
|
743
|
+
const alignBtn = el('button', 'idocs-ws-icon-btn');
|
|
744
|
+
alignBtn.type = 'button';
|
|
745
|
+
alignBtn.title = '对齐方式';
|
|
746
|
+
alignBtn.setAttribute('aria-label', '对齐方式');
|
|
747
|
+
alignBtn.setAttribute('aria-haspopup', 'menu');
|
|
748
|
+
alignBtn.setAttribute('aria-expanded', 'false');
|
|
749
|
+
alignBtn.disabled = ctx.kind !== 'editable';
|
|
750
|
+
alignBtn.append(makeSvgIcon(ICONS.align));
|
|
751
|
+
alignBtn.addEventListener('mousedown', (event) => event.preventDefault());
|
|
752
|
+
const alignPopover = el('div', 'idocs-ws-popover is-down idocs-ws-align-popover');
|
|
753
|
+
alignPopover.setAttribute('role', 'menu');
|
|
754
|
+
alignPopover.hidden = true;
|
|
755
|
+
const currentAlign = ctx.kind === 'editable' ? (ctx.align ?? 'left') : undefined;
|
|
756
|
+
for (const [align, label] of [
|
|
757
|
+
['left', '左对齐'],
|
|
758
|
+
['center', '居中对齐'],
|
|
759
|
+
['right', '右对齐'],
|
|
760
|
+
['justify', '两端对齐'],
|
|
761
|
+
]) {
|
|
762
|
+
const isSelected = currentAlign === align;
|
|
763
|
+
const item = el('button', isSelected ? 'idocs-ws-menu-item is-active' : 'idocs-ws-menu-item');
|
|
764
|
+
item.type = 'button';
|
|
765
|
+
item.setAttribute('role', 'menuitemradio');
|
|
766
|
+
item.setAttribute('aria-checked', String(isSelected));
|
|
767
|
+
item.append(el('span', 'idocs-ws-menu-label', label));
|
|
768
|
+
if (isSelected) {
|
|
769
|
+
const checkWrap = el('span', 'idocs-ws-menu-check');
|
|
770
|
+
checkWrap.setAttribute('aria-hidden', 'true');
|
|
771
|
+
checkWrap.append(makeSvgIcon(ICONS.check, 14));
|
|
772
|
+
item.append(checkWrap);
|
|
773
|
+
}
|
|
774
|
+
item.addEventListener('mousedown', (event) => event.preventDefault());
|
|
775
|
+
item.addEventListener('click', (event) => {
|
|
776
|
+
event.stopPropagation();
|
|
777
|
+
closeAllPopovers();
|
|
778
|
+
void view.setAlignment(align);
|
|
779
|
+
});
|
|
780
|
+
alignPopover.append(item);
|
|
781
|
+
}
|
|
782
|
+
alignBtn.addEventListener('click', (event) => {
|
|
783
|
+
event.stopPropagation();
|
|
784
|
+
toggleToolbarMenu(alignBtn, alignPopover, Boolean(alignPopover.hidden));
|
|
785
|
+
});
|
|
786
|
+
alignPopover.addEventListener('click', (event) => event.stopPropagation());
|
|
787
|
+
box.append(alignBtn, alignPopover);
|
|
788
|
+
tbContent.append(box);
|
|
789
|
+
};
|
|
790
|
+
const paintThumbnails = () => {
|
|
791
|
+
if (!railOpen || !activeDoc || railTab !== 'pages')
|
|
792
|
+
return;
|
|
793
|
+
const canvases = railList.querySelectorAll('.idocs-ws-thumb-canvas');
|
|
794
|
+
const dpr = Math.max(1, Math.min(2, globalThis.devicePixelRatio || 1));
|
|
795
|
+
canvases.forEach((canvas, idx) => {
|
|
796
|
+
const page = activeDoc?.pages[idx];
|
|
797
|
+
if (!page)
|
|
798
|
+
return;
|
|
799
|
+
const cssWidth = Math.max(120, Math.round(canvas.clientWidth || 164));
|
|
800
|
+
const scale = cssWidth / page.width;
|
|
801
|
+
const cssHeight = Math.max(40, Math.round(page.height * scale));
|
|
802
|
+
const pxPerPt = scale * dpr;
|
|
803
|
+
canvas.width = Math.max(1, Math.round(cssWidth * dpr));
|
|
804
|
+
canvas.height = Math.max(1, Math.round(cssHeight * dpr));
|
|
805
|
+
canvas.style.aspectRatio = `${page.width} / ${page.height}`;
|
|
806
|
+
const ctx = canvas.getContext('2d');
|
|
807
|
+
if (!ctx)
|
|
808
|
+
return;
|
|
809
|
+
paintPage(ctx, page, pxPerPt, thumbImages, activeDoc?.glyphOutlines ?? []);
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
const updateRailHighlight = (pageIndex) => {
|
|
813
|
+
const cards = railList.querySelectorAll('.idocs-ws-thumb-card');
|
|
814
|
+
cards.forEach((card, idx) => {
|
|
815
|
+
const active = idx === pageIndex;
|
|
816
|
+
card.classList.toggle('is-active', active);
|
|
817
|
+
if (active)
|
|
818
|
+
card.setAttribute('aria-current', 'page');
|
|
819
|
+
else
|
|
820
|
+
card.removeAttribute('aria-current');
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
const updateOutlineHighlight = (activePath) => {
|
|
824
|
+
if (railTab !== 'outline' || activePath === lastOutlinePath)
|
|
825
|
+
return;
|
|
826
|
+
lastOutlinePath = activePath;
|
|
827
|
+
const items = railList.querySelectorAll('.idocs-ws-outline-item');
|
|
828
|
+
items.forEach((item) => {
|
|
829
|
+
item.classList.toggle('is-active', Boolean(activePath && item.dataset.path === activePath));
|
|
830
|
+
});
|
|
831
|
+
};
|
|
832
|
+
const renderRail = () => {
|
|
833
|
+
if (!railOpen || (renderedRailDoc === activeDoc && renderedRailTab === railTab))
|
|
834
|
+
return;
|
|
835
|
+
renderedRailDoc = activeDoc;
|
|
836
|
+
renderedRailTab = railTab;
|
|
837
|
+
lastOutlinePath = undefined;
|
|
838
|
+
pagesTabBtn.setAttribute('aria-selected', String(railTab === 'pages'));
|
|
839
|
+
outlineTabBtn.setAttribute('aria-selected', String(railTab === 'outline'));
|
|
840
|
+
railList.classList.toggle('is-outline', railTab === 'outline');
|
|
841
|
+
railList.innerHTML = '';
|
|
842
|
+
if (railTab === 'pages') {
|
|
843
|
+
if (!sameImageList(lastThumbImages, activeDoc?.images ?? [])) {
|
|
844
|
+
const nextImages = activeDoc?.images ?? [];
|
|
845
|
+
lastThumbImages = nextImages;
|
|
846
|
+
for (const image of thumbImages)
|
|
847
|
+
image?.close();
|
|
848
|
+
thumbImages = [];
|
|
849
|
+
if (nextImages.length > 0) {
|
|
850
|
+
void decodeImages(nextImages).then((decoded) => {
|
|
851
|
+
if (destroyed || lastThumbImages !== nextImages) {
|
|
852
|
+
for (const image of decoded)
|
|
853
|
+
image?.close();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
thumbImages = decoded;
|
|
857
|
+
if (railOpen)
|
|
858
|
+
paintThumbnails();
|
|
859
|
+
else
|
|
860
|
+
renderedRailDoc = undefined;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const pages = activeDoc?.pages ?? [];
|
|
865
|
+
if (pages.length === 0) {
|
|
866
|
+
railList.append(el('div', 'idocs-ws-empty', '正在加载页面…'));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
pages.forEach((_, idx) => {
|
|
870
|
+
const card = el('button', 'idocs-ws-thumb-card');
|
|
871
|
+
card.type = 'button';
|
|
872
|
+
card.setAttribute('aria-label', `第 ${idx + 1} 页`);
|
|
873
|
+
if (idx === view.currentPage) {
|
|
874
|
+
card.classList.add('is-active');
|
|
875
|
+
card.setAttribute('aria-current', 'page');
|
|
876
|
+
}
|
|
877
|
+
const sheet = el('span', 'idocs-ws-thumb-sheet');
|
|
878
|
+
sheet.setAttribute('aria-hidden', 'true');
|
|
879
|
+
const canvas = el('canvas', 'idocs-ws-thumb-canvas');
|
|
880
|
+
sheet.append(canvas);
|
|
881
|
+
const label = el('span', 'idocs-ws-thumb-label', String(idx + 1));
|
|
882
|
+
label.setAttribute('aria-hidden', 'true');
|
|
883
|
+
card.append(label, sheet);
|
|
884
|
+
card.addEventListener('click', () => view.goToPage(idx, { behavior: 'smooth' }));
|
|
885
|
+
railList.append(card);
|
|
886
|
+
});
|
|
887
|
+
paintThumbnails();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const entries = activeDoc ? extractOutline(activeDoc) : [];
|
|
891
|
+
if (entries.length === 0) {
|
|
892
|
+
railList.append(el('div', 'idocs-ws-empty', '暂无标题,可将段落设为一级至三级标题'));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const activePath = view.selection()?.path;
|
|
896
|
+
for (const entry of entries) {
|
|
897
|
+
const item = el('button', `idocs-ws-outline-item level-${entry.level}`, entry.text);
|
|
898
|
+
item.type = 'button';
|
|
899
|
+
item.dataset.path = entry.path;
|
|
900
|
+
item.classList.toggle('is-active', activePath === entry.path);
|
|
901
|
+
item.title = entry.text;
|
|
902
|
+
item.addEventListener('click', () => {
|
|
903
|
+
view.goToPage(entry.page, { behavior: 'smooth' });
|
|
904
|
+
if (entry.anchor) {
|
|
905
|
+
void view.selectAnchors(entry.anchor);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
railList.append(item);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const syncHistoryButtons = (state) => {
|
|
912
|
+
undoBtn.disabled = readOnly || !state.canUndo;
|
|
913
|
+
redoBtn.disabled = readOnly || !state.canRedo;
|
|
914
|
+
};
|
|
915
|
+
const syncDocument = (doc) => {
|
|
916
|
+
if (doc === activeDoc)
|
|
917
|
+
return;
|
|
918
|
+
activeDoc = doc;
|
|
919
|
+
renderRail();
|
|
920
|
+
renderToolbarCenter();
|
|
921
|
+
};
|
|
922
|
+
view = mountEditor(editorHost, session, {
|
|
923
|
+
...options.view,
|
|
924
|
+
bottomInset: options.view?.bottomInset ?? 68,
|
|
925
|
+
readOnly,
|
|
926
|
+
onPageChange: (page, doc) => {
|
|
927
|
+
updateRailHighlight(page);
|
|
928
|
+
options.view?.onPageChange?.(page, doc);
|
|
929
|
+
},
|
|
930
|
+
onZoomChange: (next) => {
|
|
931
|
+
syncZoom();
|
|
932
|
+
if (!selPill.hidden)
|
|
933
|
+
updatePill(view.selection());
|
|
934
|
+
options.view?.onZoomChange?.(next);
|
|
935
|
+
},
|
|
936
|
+
onSelectionChange: (sel) => {
|
|
937
|
+
updatePill(sel);
|
|
938
|
+
updateOutlineHighlight(sel?.path);
|
|
939
|
+
renderToolbarCenter();
|
|
940
|
+
options.view?.onSelectionChange?.(sel);
|
|
941
|
+
},
|
|
942
|
+
onReadOnlyTarget: (target) => {
|
|
943
|
+
updatePill(null);
|
|
944
|
+
renderToolbarCenter();
|
|
945
|
+
options.view?.onReadOnlyTarget?.(target);
|
|
946
|
+
},
|
|
947
|
+
onImageSelect: (target) => {
|
|
948
|
+
updatePill(null);
|
|
949
|
+
renderToolbarCenter();
|
|
950
|
+
options.view?.onImageSelect?.(target);
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
const onSessionState = (state) => {
|
|
954
|
+
if (destroyed)
|
|
955
|
+
return;
|
|
956
|
+
syncHistoryButtons(state);
|
|
957
|
+
if (state.layout.kind === 'ready') {
|
|
958
|
+
syncDocument(state.layout.frame.document);
|
|
959
|
+
}
|
|
960
|
+
else if (state.layout.kind === 'pending' && !activeDoc && state.layout.previous) {
|
|
961
|
+
syncDocument(state.layout.previous.document);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
const unsubscribe = session.subscribe(onSessionState);
|
|
965
|
+
onSessionState(session.state());
|
|
966
|
+
syncZoom();
|
|
967
|
+
const onDocClick = (event) => {
|
|
968
|
+
if (!event.composedPath().includes(tableAnchor))
|
|
969
|
+
closeAllPopovers();
|
|
970
|
+
};
|
|
971
|
+
const onDocKeydown = (event) => {
|
|
972
|
+
if (event.key === 'Escape')
|
|
973
|
+
closeAllPopovers();
|
|
974
|
+
};
|
|
975
|
+
document.addEventListener('click', onDocClick);
|
|
976
|
+
document.addEventListener('keydown', onDocKeydown);
|
|
977
|
+
const applyRailOpen = (open) => {
|
|
978
|
+
railOpen = open;
|
|
979
|
+
body.classList.toggle('is-rail-collapsed', !open);
|
|
980
|
+
if ('inert' in rail)
|
|
981
|
+
rail.inert = !open;
|
|
982
|
+
rail.setAttribute('aria-hidden', String(!open));
|
|
983
|
+
const label = open ? '收起页面列表' : '展开页面列表';
|
|
984
|
+
railToggleBtn.title = label;
|
|
985
|
+
railToggleBtn.setAttribute('aria-label', label);
|
|
986
|
+
railToggleBtn.setAttribute('aria-expanded', String(open));
|
|
987
|
+
if (open) {
|
|
988
|
+
renderRail();
|
|
989
|
+
if (railTab === 'pages')
|
|
990
|
+
updateRailHighlight(view.currentPage);
|
|
991
|
+
else
|
|
992
|
+
updateOutlineHighlight(view.selection()?.path);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
editorHost
|
|
996
|
+
.querySelector('.idocs-preview-scroller')
|
|
997
|
+
?.addEventListener('scroll', () => {
|
|
998
|
+
if (!selPill.hidden)
|
|
999
|
+
updatePill(view.selection());
|
|
1000
|
+
}, { passive: true });
|
|
1001
|
+
railSlot.addEventListener('transitionend', () => updatePill(view.selection()));
|
|
1002
|
+
railToggleBtn.addEventListener('click', () => applyRailOpen(!railOpen));
|
|
1003
|
+
pagesTabBtn.addEventListener('click', () => {
|
|
1004
|
+
if (railTab === 'pages')
|
|
1005
|
+
return;
|
|
1006
|
+
railTab = 'pages';
|
|
1007
|
+
renderRail();
|
|
1008
|
+
});
|
|
1009
|
+
outlineTabBtn.addEventListener('click', () => {
|
|
1010
|
+
if (railTab === 'outline')
|
|
1011
|
+
return;
|
|
1012
|
+
railTab = 'outline';
|
|
1013
|
+
renderRail();
|
|
1014
|
+
});
|
|
1015
|
+
undoBtn.addEventListener('click', () => void view.undo());
|
|
1016
|
+
redoBtn.addEventListener('click', () => void view.redo());
|
|
1017
|
+
zoomOutBtn.addEventListener('click', () => {
|
|
1018
|
+
view.setZoom(Math.max(0.25, view.zoom - 0.15));
|
|
1019
|
+
syncZoom();
|
|
1020
|
+
});
|
|
1021
|
+
zoomInBtn.addEventListener('click', () => {
|
|
1022
|
+
view.setZoom(Math.min(4, view.zoom + 0.15));
|
|
1023
|
+
syncZoom();
|
|
1024
|
+
});
|
|
1025
|
+
fitWidthBtn.addEventListener('click', () => {
|
|
1026
|
+
view.setZoom(view.fitWidthZoom);
|
|
1027
|
+
syncZoom();
|
|
1028
|
+
});
|
|
1029
|
+
findBtn.addEventListener('click', () => view.find.open());
|
|
1030
|
+
pillQuoteBtn?.addEventListener('click', () => {
|
|
1031
|
+
const sel = view.selection();
|
|
1032
|
+
const text = sel?.excerpt;
|
|
1033
|
+
if (!sel || !text?.trim())
|
|
1034
|
+
return;
|
|
1035
|
+
options.onQuoteSelection?.({ text, path: sel.path, page: sel.page });
|
|
1036
|
+
});
|
|
1037
|
+
const insertAndFocus = async (block) => {
|
|
1038
|
+
const active = document.activeElement;
|
|
1039
|
+
const commit = await view.insertBlock(block);
|
|
1040
|
+
if (commit?.selection && !destroyed && !readOnly && document.activeElement === active)
|
|
1041
|
+
view.focus();
|
|
1042
|
+
};
|
|
1043
|
+
dock.append(makeDockButton(ICONS.paragraph, '段落', () => void insertAndFocus({ kind: 'paragraph' })), makeDockButton(ICONS.heading, '标题', () => void insertAndFocus({ kind: 'heading', level: 2 })));
|
|
1044
|
+
tableAnchor.prepend(makeDockButton(ICONS.table, '表格', () => {
|
|
1045
|
+
closeToolbarMenu();
|
|
1046
|
+
setTablePopoverOpen(Boolean(tablePopover.hidden));
|
|
1047
|
+
}, tablePopover));
|
|
1048
|
+
dock.append(tableAnchor, makeDockButton(ICONS.callout, '提示框', () => void insertAndFocus({ kind: 'callout', colors: options.calloutColors })));
|
|
1049
|
+
if (options.onPickImage) {
|
|
1050
|
+
dock.append(makeDockButton(ICONS.image, '图片', async () => {
|
|
1051
|
+
const picked = await options.onPickImage?.();
|
|
1052
|
+
if (picked && !destroyed) {
|
|
1053
|
+
await view.insertBlock({ kind: 'image', src: picked.src, alt: picked.alt });
|
|
1054
|
+
}
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
dock.append(makeDockButton(ICONS.pageBreak, '分页符', () => void view.insertBlock({ kind: 'pageBreak' })));
|
|
1058
|
+
const detachInsertButtons = options.mountInsertButtons?.(insertButtons);
|
|
1059
|
+
tableGrid.addEventListener('mouseover', (event) => {
|
|
1060
|
+
const cell = event.target?.closest('.idocs-ws-table-cell');
|
|
1061
|
+
if (!cell)
|
|
1062
|
+
return;
|
|
1063
|
+
paintTableGrid(Number(cell.dataset.rows), Number(cell.dataset.cols));
|
|
1064
|
+
});
|
|
1065
|
+
tableGrid.addEventListener('click', (event) => {
|
|
1066
|
+
const cell = event.target?.closest('.idocs-ws-table-cell');
|
|
1067
|
+
const rows = cell ? Number(cell.dataset.rows) : hoverRows;
|
|
1068
|
+
const cols = cell ? Number(cell.dataset.cols) : hoverCols;
|
|
1069
|
+
closeAllPopovers();
|
|
1070
|
+
void view.insertBlock({ kind: 'table', rows, cols });
|
|
1071
|
+
});
|
|
1072
|
+
return {
|
|
1073
|
+
view,
|
|
1074
|
+
get readOnly() {
|
|
1075
|
+
return readOnly;
|
|
1076
|
+
},
|
|
1077
|
+
get processing() {
|
|
1078
|
+
return processing;
|
|
1079
|
+
},
|
|
1080
|
+
setReadOnly(next, nextBusyLabel, nextProcessing) {
|
|
1081
|
+
if (destroyed)
|
|
1082
|
+
return;
|
|
1083
|
+
readOnly = next;
|
|
1084
|
+
busyLabel = nextBusyLabel;
|
|
1085
|
+
processing = next ? (nextProcessing ?? Boolean(nextBusyLabel)) : false;
|
|
1086
|
+
root.classList.toggle('is-readonly', readOnly);
|
|
1087
|
+
root.classList.toggle('is-processing', processing);
|
|
1088
|
+
view.setReadOnly(next);
|
|
1089
|
+
dock.hidden = next;
|
|
1090
|
+
if (next) {
|
|
1091
|
+
selPill.hidden = true;
|
|
1092
|
+
closeAllPopovers();
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
updatePill(view.selection());
|
|
1096
|
+
}
|
|
1097
|
+
syncHistoryButtons(session.state());
|
|
1098
|
+
renderToolbarCenter();
|
|
1099
|
+
},
|
|
1100
|
+
setRailOpen(open) {
|
|
1101
|
+
if (destroyed)
|
|
1102
|
+
return;
|
|
1103
|
+
applyRailOpen(open);
|
|
1104
|
+
},
|
|
1105
|
+
destroy() {
|
|
1106
|
+
if (destroyed)
|
|
1107
|
+
return;
|
|
1108
|
+
destroyed = true;
|
|
1109
|
+
toolbarScroller.destroy();
|
|
1110
|
+
document.removeEventListener('click', onDocClick);
|
|
1111
|
+
document.removeEventListener('keydown', onDocKeydown);
|
|
1112
|
+
unsubscribe();
|
|
1113
|
+
view.destroy();
|
|
1114
|
+
detachInsertButtons?.();
|
|
1115
|
+
for (const img of thumbImages)
|
|
1116
|
+
img?.close();
|
|
1117
|
+
thumbImages = [];
|
|
1118
|
+
root.remove();
|
|
1119
|
+
},
|
|
1120
|
+
};
|
|
1121
|
+
}
|