@aseity/idocs-preview 0.2.0 → 0.4.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.
@@ -0,0 +1,1028 @@
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="M3.5 5.5h13M3.5 10h13M3.5 14.5h9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
19
+ heading: '<path d="M4.5 4.5v11M12.5 4.5v11M4.5 10h8M15.5 11.5v4M14 12.8l1.5-1.3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
20
+ table: '<rect x="3" y="4" width="14" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M3 8.5h14M3 12.5h14M8.5 8.5V16M13 8.5V16" fill="none" stroke="currentColor" stroke-width="1.3"/>',
21
+ callout: '<rect x="3" y="4" width="14" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M6.5 7.5v5M9.5 8.2h4.5M9.5 11.8h3.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
22
+ image: '<rect x="3" y="4" width="14" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="7.2" cy="8" r="1.2" fill="currentColor"/><path d="M4.2 14l3.6-3.4 2.8 2.6 2.2-1.8 3 2.6" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>',
23
+ pageBreak: '<path d="M3.5 5h13M3.5 15h13M4.5 10h2.5M9 10h2M13 10h2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
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 headingLevelFromStyle(style) {
51
+ switch (style) {
52
+ case 'heading1':
53
+ return 1;
54
+ case 'heading2':
55
+ return 2;
56
+ case 'heading3':
57
+ return 3;
58
+ default:
59
+ return null;
60
+ }
61
+ }
62
+ export function extractOutline(doc) {
63
+ const entries = [];
64
+ let lastPath = '';
65
+ for (let pageIndex = 0; pageIndex < doc.pages.length; pageIndex += 1) {
66
+ for (const line of doc.pages[pageIndex].lines) {
67
+ if (line.story !== 'body')
68
+ continue;
69
+ const seg = line.sourceSegments?.find((s) => s.source.kind === 'editable');
70
+ if (!seg || seg.source.kind !== 'editable')
71
+ continue;
72
+ const level = headingLevelFromStyle(seg.source.blockStyle);
73
+ if (!level)
74
+ continue;
75
+ const fragment = line.sourceSegments
76
+ ?.filter((s) => s.source.kind === 'editable')
77
+ .map((s) => line.text.slice(s.start, s.end))
78
+ .join('')
79
+ .trim();
80
+ const piece = fragment || line.text.trim();
81
+ if (entries.length > 0 && line.path === lastPath) {
82
+ const prev = entries[entries.length - 1];
83
+ entries[entries.length - 1] = {
84
+ ...prev,
85
+ text: piece ? `${prev.text}${prev.text ? ' ' : ''}${piece}` : prev.text,
86
+ };
87
+ }
88
+ else {
89
+ lastPath = line.path;
90
+ entries.push({
91
+ path: line.path,
92
+ level,
93
+ text: piece || `标题 ${entries.length + 1}`,
94
+ page: pageIndex,
95
+ anchor: seg.source.stops[0]?.anchor,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ return entries;
101
+ }
102
+ function readOnlyMeta(reason) {
103
+ switch (reason) {
104
+ case 'generated':
105
+ return { badge: '自动生成', message: '此处编号或目录由系统自动生成,无法直接修改' };
106
+ case 'aliasShared':
107
+ return { badge: '共享节点', message: '此内容与文档其他位置共享,暂不支持直接改写' };
108
+ case 'repeatedHeader':
109
+ return { badge: '重复表头', message: '此为跨页重复表头,请点击首页表格第一行进行编辑' };
110
+ case 'unsupportedStory':
111
+ return { badge: '页眉页脚', message: '当前仅支持编辑正文区域,页眉、页脚与脚注为只读' };
112
+ case 'protectedInline':
113
+ return { badge: '保护内容', message: '包含公式或交叉引用等保护元素,请在其前后输入' };
114
+ case 'unsupportedStructure':
115
+ return { badge: '只读结构', message: '此区域结构暂不支持直接编辑' };
116
+ }
117
+ }
118
+ export function isTableCellPath(path) {
119
+ return /\.rows\.\d+\.cells\.\d+(?:\.|$)/.test(path);
120
+ }
121
+ function findEditableLineMeta(doc, page, flow) {
122
+ const targetPage = doc?.pages[page];
123
+ if (!targetPage)
124
+ return {};
125
+ for (const line of targetPage.lines) {
126
+ for (const seg of line.sourceSegments ?? []) {
127
+ if (seg.source.kind === 'editable' && seg.source.stops.some((stop) => stop.anchor.flow === flow)) {
128
+ return {
129
+ blockStyle: seg.source.blockStyle,
130
+ align: seg.source.align,
131
+ };
132
+ }
133
+ }
134
+ }
135
+ return {};
136
+ }
137
+ export function resolveToolbarContext(input) {
138
+ if (input.readOnly) {
139
+ const processing = input.processing ?? Boolean(input.busyLabel);
140
+ return {
141
+ kind: 'readOnly',
142
+ processing,
143
+ label: input.busyLabel || (processing ? '正在修订文档' : '只读'),
144
+ };
145
+ }
146
+ if (input.readOnlyTarget) {
147
+ const meta = readOnlyMeta(input.readOnlyTarget.reason);
148
+ return { kind: 'readOnlyTarget', target: input.readOnlyTarget, ...meta };
149
+ }
150
+ if (input.selectedImage) {
151
+ return { kind: 'image', target: input.selectedImage };
152
+ }
153
+ if (input.selection) {
154
+ const { range } = input.selection.selection;
155
+ const collapsed = range.start.flow === range.end.flow && range.start.offset === range.end.offset;
156
+ const meta = findEditableLineMeta(input.document, input.selection.page, range.start.flow);
157
+ return {
158
+ kind: 'editable',
159
+ inTable: isTableCellPath(input.selection.path),
160
+ blockStyle: meta.blockStyle,
161
+ align: meta.align,
162
+ collapsed,
163
+ };
164
+ }
165
+ return { kind: 'empty' };
166
+ }
167
+ function sameImageList(a, b) {
168
+ if (!a || a.length !== b.length)
169
+ return false;
170
+ for (let i = 0; i < a.length; i += 1) {
171
+ if (a[i].missing !== b[i].missing ||
172
+ a[i].format !== b[i].format ||
173
+ a[i].washout !== b[i].washout ||
174
+ a[i].bytes !== b[i].bytes) {
175
+ return false;
176
+ }
177
+ }
178
+ return true;
179
+ }
180
+ function readOnlyQuoteText(doc, target) {
181
+ const page = doc?.pages[target.page];
182
+ if (!page)
183
+ return '';
184
+ const matching = page.lines
185
+ .filter((l) => (target.path ? l.path === target.path : l.story !== 'body'))
186
+ .map((l) => l.text.trim())
187
+ .filter(Boolean);
188
+ if (matching.length > 0)
189
+ return matching.join(' ');
190
+ return `第 ${target.page + 1} 页只读区域`;
191
+ }
192
+ function toolbarRenderKey(ctx, tableModePreferred) {
193
+ switch (ctx.kind) {
194
+ case 'readOnly':
195
+ return `ro:${ctx.processing}:${ctx.label}`;
196
+ case 'editable':
197
+ return `ed:${ctx.inTable}:${ctx.inTable && tableModePreferred}:${ctx.blockStyle ?? ''}:${ctx.align ?? ''}:${ctx.collapsed}`;
198
+ case 'empty':
199
+ return 'empty';
200
+ case 'readOnlyTarget':
201
+ case 'image':
202
+ return null;
203
+ }
204
+ }
205
+ export function mountEditorWorkspace(container, session, options = {}) {
206
+ ensureWorkspaceStyles();
207
+ let readOnly = options.readOnly === true;
208
+ let busyLabel = options.busyLabel;
209
+ let processing = readOnly && (options.processing ?? Boolean(options.busyLabel));
210
+ let railOpen = options.railOpen ?? false;
211
+ let railTab = 'pages';
212
+ let tableModePreferred = false;
213
+ let lastToolbarKey = '';
214
+ let lastOutlinePath;
215
+ let activeDoc;
216
+ let thumbImages = [];
217
+ let lastThumbImages;
218
+ let destroyed = false;
219
+ const root = el('div', 'idocs-ws');
220
+ root.classList.toggle('is-readonly', readOnly);
221
+ root.classList.toggle('is-processing', processing);
222
+ const toolbar = el('nav', 'idocs-ws-toolbar');
223
+ toolbar.setAttribute('aria-label', '文档格式工具栏');
224
+ const tbLeft = el('div', 'idocs-ws-tb-left');
225
+ const tbCenter = el('div', 'idocs-ws-tb-center');
226
+ const tbRight = el('div', 'idocs-ws-tb-right');
227
+ toolbar.append(tbLeft, tbCenter, tbRight);
228
+ const railToggleBtn = el('button', 'idocs-ws-icon-btn');
229
+ railToggleBtn.type = 'button';
230
+ const initialRailLabel = railOpen ? '收起页面列表' : '展开页面列表';
231
+ railToggleBtn.title = initialRailLabel;
232
+ railToggleBtn.setAttribute('aria-label', initialRailLabel);
233
+ railToggleBtn.setAttribute('aria-expanded', String(railOpen));
234
+ railToggleBtn.append(makeSvgIcon(ICONS.rail, 16));
235
+ const undoBtn = el('button', 'idocs-ws-icon-btn');
236
+ undoBtn.type = 'button';
237
+ undoBtn.title = '撤销';
238
+ undoBtn.setAttribute('aria-label', '撤销');
239
+ undoBtn.append(makeSvgIcon(ICONS.undo, 16));
240
+ const redoBtn = el('button', 'idocs-ws-icon-btn');
241
+ redoBtn.type = 'button';
242
+ redoBtn.title = '重做';
243
+ redoBtn.setAttribute('aria-label', '重做');
244
+ redoBtn.append(makeSvgIcon(ICONS.redo, 16));
245
+ tbLeft.append(railToggleBtn, undoBtn, redoBtn);
246
+ const zoomOutBtn = el('button', 'idocs-ws-icon-btn');
247
+ zoomOutBtn.type = 'button';
248
+ zoomOutBtn.title = '缩小';
249
+ zoomOutBtn.setAttribute('aria-label', '缩小');
250
+ zoomOutBtn.append(makeSvgIcon(ICONS.zoomOut));
251
+ const zoomLabel = el('span', 'idocs-ws-zoom-label', '100%');
252
+ const zoomInBtn = el('button', 'idocs-ws-icon-btn');
253
+ zoomInBtn.type = 'button';
254
+ zoomInBtn.title = '放大';
255
+ zoomInBtn.setAttribute('aria-label', '放大');
256
+ zoomInBtn.append(makeSvgIcon(ICONS.zoomIn));
257
+ const fitWidthBtn = el('button', 'idocs-ws-icon-btn');
258
+ fitWidthBtn.type = 'button';
259
+ fitWidthBtn.title = '适应宽度';
260
+ fitWidthBtn.setAttribute('aria-label', '适应宽度');
261
+ fitWidthBtn.append(makeSvgIcon(ICONS.fit, 18));
262
+ const findBtn = el('button', 'idocs-ws-icon-btn');
263
+ findBtn.type = 'button';
264
+ findBtn.title = '查找';
265
+ findBtn.setAttribute('aria-label', '查找');
266
+ findBtn.append(makeSvgIcon(ICONS.search));
267
+ const zoomDivider = el('span', 'idocs-ws-divider');
268
+ zoomDivider.setAttribute('aria-hidden', 'true');
269
+ tbRight.append(zoomOutBtn, zoomLabel, zoomInBtn, zoomDivider, fitWidthBtn, findBtn);
270
+ const body = el('div', 'idocs-ws-body');
271
+ body.classList.toggle('is-rail-collapsed', !railOpen);
272
+ const railSlot = el('div', 'idocs-ws-rail-slot');
273
+ const rail = el('aside', 'idocs-ws-rail');
274
+ if ('inert' in rail)
275
+ rail.inert = !railOpen;
276
+ rail.setAttribute('aria-hidden', String(!railOpen));
277
+ const railTabs = el('div', 'idocs-ws-rail-tabs');
278
+ railTabs.setAttribute('role', 'tablist');
279
+ const pagesTabBtn = el('button', 'idocs-ws-rail-tab', '页面');
280
+ pagesTabBtn.type = 'button';
281
+ pagesTabBtn.setAttribute('role', 'tab');
282
+ pagesTabBtn.setAttribute('aria-selected', 'true');
283
+ const outlineTabBtn = el('button', 'idocs-ws-rail-tab', '大纲');
284
+ outlineTabBtn.type = 'button';
285
+ outlineTabBtn.setAttribute('role', 'tab');
286
+ outlineTabBtn.setAttribute('aria-selected', 'false');
287
+ railTabs.append(pagesTabBtn, outlineTabBtn);
288
+ const railList = el('div', 'idocs-ws-rail-list');
289
+ rail.append(railTabs, railList);
290
+ const stage = el('div', 'idocs-ws-stage');
291
+ const editorHost = el('div', 'idocs-ws-editor-host');
292
+ const askLabel = formatAskAssistantLabel(options.assistantName);
293
+ const selPill = el('div', 'idocs-ws-sel-pill');
294
+ selPill.hidden = true;
295
+ const pillQuoteBtn = options.onQuoteSelection ? el('button', 'idocs-ws-sel-pill-btn') : null;
296
+ if (pillQuoteBtn) {
297
+ pillQuoteBtn.type = 'button';
298
+ const askSvg = makeSvgIcon(ICONS.ask, 16);
299
+ askSvg.classList.add('idocs-ws-sel-pill-icon');
300
+ pillQuoteBtn.append(askSvg, el('span', undefined, askLabel));
301
+ pillQuoteBtn.addEventListener('mousedown', (event) => event.preventDefault());
302
+ selPill.append(pillQuoteBtn);
303
+ }
304
+ const dock = el('nav', 'idocs-ws-dock');
305
+ dock.setAttribute('aria-label', '插入内容');
306
+ dock.hidden = readOnly;
307
+ const makeDockBtn = (icon, label) => {
308
+ const btn = el('button', 'idocs-ws-dock-btn');
309
+ btn.type = 'button';
310
+ btn.append(makeSvgIcon(icon), el('span', undefined, label));
311
+ return btn;
312
+ };
313
+ const dockParaBtn = makeDockBtn(ICONS.paragraph, '段落');
314
+ const dockHeadingBtn = makeDockBtn(ICONS.heading, '标题');
315
+ const tableAnchor = el('div', 'idocs-ws-popover-anchor');
316
+ const dockTableBtn = makeDockBtn(ICONS.table, '表格');
317
+ dockTableBtn.setAttribute('aria-expanded', 'false');
318
+ const tablePopover = el('div', 'idocs-ws-popover is-up idocs-ws-table-popover');
319
+ tablePopover.hidden = true;
320
+ const tableGridHead = el('div', 'idocs-ws-table-grid-head', '3 × 3 表格');
321
+ const tableGrid = el('div', 'idocs-ws-table-grid');
322
+ const tableCells = [];
323
+ let hoverRows = 3;
324
+ let hoverCols = 3;
325
+ for (let r = 1; r <= 6; r += 1) {
326
+ for (let c = 1; c <= 6; c += 1) {
327
+ const cell = el('button', 'idocs-ws-table-cell');
328
+ cell.type = 'button';
329
+ cell.dataset.rows = String(r);
330
+ cell.dataset.cols = String(c);
331
+ cell.setAttribute('aria-label', `${r} 行 ${c} 列`);
332
+ tableCells.push(cell);
333
+ tableGrid.append(cell);
334
+ }
335
+ }
336
+ tablePopover.append(tableGridHead, tableGrid);
337
+ tableAnchor.append(dockTableBtn, tablePopover);
338
+ const dockCalloutBtn = makeDockBtn(ICONS.callout, '提示框');
339
+ const dockImageBtn = options.onPickImage ? makeDockBtn(ICONS.image, '图片') : null;
340
+ const dockPageBreakBtn = makeDockBtn(ICONS.pageBreak, '分页符');
341
+ dock.append(dockParaBtn, dockHeadingBtn, tableAnchor, dockCalloutBtn);
342
+ if (dockImageBtn)
343
+ dock.append(dockImageBtn);
344
+ dock.append(dockPageBreakBtn);
345
+ stage.append(editorHost, selPill, dock);
346
+ railSlot.append(rail);
347
+ body.append(railSlot, stage);
348
+ root.append(toolbar, body);
349
+ container.append(root);
350
+ const paintTableGrid = (rows, cols) => {
351
+ hoverRows = rows;
352
+ hoverCols = cols;
353
+ tableGridHead.textContent = `${rows} × ${cols} 表格`;
354
+ for (const cell of tableCells) {
355
+ const r = Number(cell.dataset.rows);
356
+ const c = Number(cell.dataset.cols);
357
+ cell.classList.toggle('is-hot', r <= rows && c <= cols);
358
+ }
359
+ };
360
+ paintTableGrid(3, 3);
361
+ const closeAllPopovers = () => {
362
+ tablePopover.hidden = true;
363
+ dockTableBtn.setAttribute('aria-expanded', 'false');
364
+ tbCenter.querySelectorAll('.idocs-ws-popover').forEach((menu) => {
365
+ menu.hidden = true;
366
+ });
367
+ tbCenter.querySelectorAll('[aria-expanded="true"]').forEach((trigger) => {
368
+ trigger.setAttribute('aria-expanded', 'false');
369
+ });
370
+ };
371
+ let view;
372
+ const syncZoom = () => {
373
+ zoomLabel.textContent = `${Math.round(view.zoom * 100)}%`;
374
+ };
375
+ const updatePill = (sel) => {
376
+ if (readOnly ||
377
+ view?.dragging ||
378
+ !sel ||
379
+ !sel.clientRect ||
380
+ (sel.selection.range.start.flow === sel.selection.range.end.flow &&
381
+ sel.selection.range.start.offset === sel.selection.range.end.offset) ||
382
+ !pillQuoteBtn) {
383
+ selPill.hidden = true;
384
+ return;
385
+ }
386
+ selPill.hidden = false;
387
+ const stageRect = stage.getBoundingClientRect();
388
+ const pillWidth = selPill.offsetWidth || 92;
389
+ const pillHeight = selPill.offsetHeight || 32;
390
+ const left = Math.max(8, Math.min(stageRect.width - pillWidth - 8, sel.clientRect.left - stageRect.left + sel.clientRect.width / 2 - pillWidth / 2));
391
+ const top = Math.max(8, sel.clientRect.top - stageRect.top - pillHeight - 8);
392
+ selPill.style.left = `${Math.round(left)}px`;
393
+ selPill.style.top = `${Math.round(top)}px`;
394
+ };
395
+ const renderToolbarCenter = () => {
396
+ const sel = view.selection();
397
+ const ctx = resolveToolbarContext({
398
+ readOnly,
399
+ processing,
400
+ busyLabel,
401
+ readOnlyTarget: view.readOnlyTarget(),
402
+ selectedImage: view.selectedImage(),
403
+ selection: sel,
404
+ document: activeDoc,
405
+ });
406
+ const nextKey = toolbarRenderKey(ctx, tableModePreferred);
407
+ if (nextKey !== null && nextKey === lastToolbarKey) {
408
+ return;
409
+ }
410
+ lastToolbarKey = nextKey ?? '';
411
+ tbCenter.innerHTML = '';
412
+ if (ctx.kind === 'readOnly') {
413
+ if (ctx.processing) {
414
+ const status = el('span', 'idocs-ws-processing-indicator');
415
+ status.setAttribute('role', 'status');
416
+ status.setAttribute('aria-label', ctx.label);
417
+ status.title = ctx.label;
418
+ const ring = el('span', 'idocs-ws-spinner');
419
+ ring.setAttribute('aria-hidden', 'true');
420
+ status.append(ring);
421
+ tbCenter.append(status);
422
+ }
423
+ return;
424
+ }
425
+ if (ctx.kind === 'readOnlyTarget') {
426
+ const box = el('div', 'idocs-ws-ctx');
427
+ box.append(el('span', 'idocs-ws-badge is-warn', ctx.badge));
428
+ box.append(el('span', 'idocs-ws-note', ctx.message));
429
+ if (options.onQuoteSelection && activeDoc) {
430
+ const quoteBtn = el('button', 'idocs-ws-pill-btn');
431
+ quoteBtn.type = 'button';
432
+ quoteBtn.append(makeSvgIcon(ICONS.ask, 15), el('span', undefined, askLabel));
433
+ quoteBtn.addEventListener('click', () => {
434
+ const text = readOnlyQuoteText(activeDoc, ctx.target);
435
+ options.onQuoteSelection?.({ text, path: ctx.target.path ?? '', page: ctx.target.page });
436
+ });
437
+ box.append(quoteBtn);
438
+ }
439
+ const clearBtn = el('button', 'idocs-ws-pill-btn', '取消选中');
440
+ clearBtn.type = 'button';
441
+ clearBtn.addEventListener('click', () => {
442
+ view.clearReadOnlyTarget();
443
+ view.focus();
444
+ });
445
+ box.append(clearBtn);
446
+ tbCenter.append(box);
447
+ return;
448
+ }
449
+ if (ctx.kind === 'image') {
450
+ const box = el('div', 'idocs-ws-ctx');
451
+ box.append(el('span', 'idocs-ws-badge', '插图'));
452
+ for (const ratio of [0.5, 0.75, 1]) {
453
+ const btn = el('button', 'idocs-ws-pill-btn', `宽度 ${Math.round(ratio * 100)}%`);
454
+ btn.type = 'button';
455
+ btn.addEventListener('click', () => {
456
+ void view.editImage(ctx.target.path, { widthRatio: ratio });
457
+ });
458
+ box.append(btn);
459
+ }
460
+ box.append(el('span', 'idocs-ws-divider'));
461
+ for (const [align, label] of [
462
+ ['left', '左对齐'],
463
+ ['center', '居中'],
464
+ ['right', '右对齐'],
465
+ ]) {
466
+ const btn = el('button', 'idocs-ws-pill-btn', label);
467
+ btn.type = 'button';
468
+ btn.addEventListener('click', () => {
469
+ void view.editImage(ctx.target.path, { align });
470
+ });
471
+ box.append(btn);
472
+ }
473
+ if (options.onPickImage) {
474
+ const replaceBtn = el('button', 'idocs-ws-pill-btn', '替换图片');
475
+ replaceBtn.type = 'button';
476
+ replaceBtn.addEventListener('click', async () => {
477
+ const picked = await options.onPickImage?.();
478
+ if (picked && !destroyed) {
479
+ await view.editImage(ctx.target.path, { src: picked.src, alt: picked.alt });
480
+ }
481
+ });
482
+ box.append(replaceBtn);
483
+ }
484
+ const doneBtn = el('button', 'idocs-ws-pill-btn', '完成');
485
+ doneBtn.type = 'button';
486
+ doneBtn.addEventListener('click', () => view.clearSelectedImage());
487
+ box.append(doneBtn);
488
+ tbCenter.append(box);
489
+ return;
490
+ }
491
+ const box = el('div', 'idocs-ws-ctx');
492
+ const inTable = ctx.kind === 'editable' && ctx.inTable;
493
+ const showTable = inTable && tableModePreferred;
494
+ if (inTable) {
495
+ const seg = el('div', 'idocs-ws-seg');
496
+ const textTab = el('button', 'idocs-ws-seg-btn', '文字');
497
+ textTab.type = 'button';
498
+ textTab.setAttribute('aria-pressed', String(!showTable));
499
+ textTab.addEventListener('click', () => {
500
+ tableModePreferred = false;
501
+ renderToolbarCenter();
502
+ });
503
+ const tableTab = el('button', 'idocs-ws-seg-btn', '表格');
504
+ tableTab.type = 'button';
505
+ tableTab.setAttribute('aria-pressed', String(showTable));
506
+ tableTab.addEventListener('click', () => {
507
+ tableModePreferred = true;
508
+ renderToolbarCenter();
509
+ });
510
+ seg.append(textTab, tableTab);
511
+ box.append(seg, el('span', 'idocs-ws-divider'));
512
+ }
513
+ if (showTable) {
514
+ const tableActions = [
515
+ ['insertRowAbove', '上插行'],
516
+ ['insertRowBelow', '下插行'],
517
+ ['insertColumnLeft', '左插列'],
518
+ ['insertColumnRight', '右插列'],
519
+ ['deleteRow', '删行', true],
520
+ ['deleteColumn', '删列', true],
521
+ ['toggleHeaderRow', '表头行'],
522
+ ];
523
+ for (const [action, label, danger] of tableActions) {
524
+ const btn = el('button', danger ? 'idocs-ws-pill-btn is-danger' : 'idocs-ws-pill-btn', label);
525
+ btn.type = 'button';
526
+ btn.addEventListener('click', () => {
527
+ void view.editTable(action);
528
+ });
529
+ box.append(btn);
530
+ }
531
+ tbCenter.append(box);
532
+ return;
533
+ }
534
+ const styleOptions = [
535
+ ['body', '正文'],
536
+ ['heading1', '一级标题'],
537
+ ['heading2', '二级标题'],
538
+ ['heading3', '三级标题'],
539
+ ];
540
+ const activeStyle = ctx.kind === 'editable' ? ctx.blockStyle : undefined;
541
+ const selectedStyle = activeStyle ?? 'body';
542
+ const selectedLabel = styleOptions.find(([v]) => v === selectedStyle)?.[1] ?? '正文';
543
+ const styleAnchor = el('div', 'idocs-ws-popover-anchor');
544
+ const styleTrigger = el('button', 'idocs-ws-select-trigger');
545
+ styleTrigger.type = 'button';
546
+ styleTrigger.setAttribute('role', 'combobox');
547
+ styleTrigger.setAttribute('aria-haspopup', 'listbox');
548
+ styleTrigger.setAttribute('aria-expanded', 'false');
549
+ styleTrigger.setAttribute('aria-label', '段落样式');
550
+ styleTrigger.disabled = ctx.kind !== 'editable' || activeStyle === undefined;
551
+ const styleValueSpan = el('span', 'idocs-ws-select-value', selectedLabel);
552
+ const chevronSvg = makeSvgIcon(ICONS.chevronDown, 14);
553
+ chevronSvg.classList.add('idocs-ws-select-chevron');
554
+ styleTrigger.append(styleValueSpan, chevronSvg);
555
+ styleTrigger.addEventListener('mousedown', (event) => event.preventDefault());
556
+ const stylePopover = el('div', 'idocs-ws-popover is-down idocs-ws-style-popover');
557
+ stylePopover.setAttribute('role', 'listbox');
558
+ stylePopover.setAttribute('aria-label', '段落样式');
559
+ stylePopover.hidden = true;
560
+ const styleItems = [];
561
+ let highlightedStyleIdx = Math.max(0, styleOptions.findIndex(([v]) => v === selectedStyle));
562
+ const syncStyleHighlight = (nextIdx) => {
563
+ highlightedStyleIdx = (nextIdx + styleItems.length) % styleItems.length;
564
+ styleItems.forEach((item, idx) => {
565
+ if (idx === highlightedStyleIdx)
566
+ item.dataset.highlighted = 'true';
567
+ else
568
+ delete item.dataset.highlighted;
569
+ });
570
+ };
571
+ for (let idx = 0; idx < styleOptions.length; idx += 1) {
572
+ const [val, label] = styleOptions[idx];
573
+ const isSelected = val === selectedStyle;
574
+ const item = el('button', isSelected ? 'idocs-ws-menu-item is-active' : 'idocs-ws-menu-item');
575
+ item.type = 'button';
576
+ item.setAttribute('role', 'option');
577
+ item.setAttribute('aria-selected', String(isSelected));
578
+ item.dataset.value = val;
579
+ item.append(el('span', 'idocs-ws-menu-label', label));
580
+ if (isSelected) {
581
+ const checkWrap = el('span', 'idocs-ws-menu-check');
582
+ checkWrap.setAttribute('aria-hidden', 'true');
583
+ checkWrap.append(makeSvgIcon(ICONS.check, 14));
584
+ item.append(checkWrap);
585
+ }
586
+ item.addEventListener('mousedown', (event) => event.preventDefault());
587
+ item.addEventListener('mouseenter', () => syncStyleHighlight(idx));
588
+ item.addEventListener('click', (event) => {
589
+ event.stopPropagation();
590
+ closeAllPopovers();
591
+ void view.setBlockStyle(val);
592
+ });
593
+ styleItems.push(item);
594
+ stylePopover.append(item);
595
+ }
596
+ const toggleStylePopover = (forceOpen) => {
597
+ if (styleTrigger.disabled)
598
+ return;
599
+ const shouldOpen = forceOpen ?? stylePopover.hidden;
600
+ closeAllPopovers();
601
+ if (shouldOpen) {
602
+ stylePopover.hidden = false;
603
+ styleTrigger.setAttribute('aria-expanded', 'true');
604
+ syncStyleHighlight(Math.max(0, styleOptions.findIndex(([v]) => v === selectedStyle)));
605
+ }
606
+ };
607
+ styleTrigger.addEventListener('click', (event) => {
608
+ event.stopPropagation();
609
+ toggleStylePopover();
610
+ });
611
+ styleTrigger.addEventListener('keydown', (event) => {
612
+ if (event.key === 'ArrowDown') {
613
+ event.preventDefault();
614
+ if (stylePopover.hidden)
615
+ toggleStylePopover(true);
616
+ else
617
+ syncStyleHighlight(highlightedStyleIdx + 1);
618
+ }
619
+ else if (event.key === 'ArrowUp') {
620
+ event.preventDefault();
621
+ if (stylePopover.hidden)
622
+ toggleStylePopover(true);
623
+ else
624
+ syncStyleHighlight(highlightedStyleIdx - 1);
625
+ }
626
+ else if ((event.key === 'Enter' || event.key === ' ') && !stylePopover.hidden) {
627
+ event.preventDefault();
628
+ const picked = styleOptions[highlightedStyleIdx]?.[0];
629
+ closeAllPopovers();
630
+ if (picked)
631
+ void view.setBlockStyle(picked);
632
+ }
633
+ else if (event.key === 'Escape' && !stylePopover.hidden) {
634
+ event.preventDefault();
635
+ event.stopPropagation();
636
+ closeAllPopovers();
637
+ }
638
+ });
639
+ stylePopover.addEventListener('click', (event) => event.stopPropagation());
640
+ styleAnchor.append(styleTrigger, stylePopover);
641
+ box.append(styleAnchor, el('span', 'idocs-ws-divider'));
642
+ const collapsed = ctx.kind !== 'editable' || ctx.collapsed;
643
+ const marks = [
644
+ ['bold', '粗体', ICONS.bold],
645
+ ['italic', '斜体', ICONS.italic],
646
+ ['underline', '下划线', ICONS.underline],
647
+ ['strike', '删除线', ICONS.strike],
648
+ ];
649
+ for (const [mark, label, icon] of marks) {
650
+ const btn = el('button', 'idocs-ws-icon-btn');
651
+ btn.type = 'button';
652
+ btn.title = label;
653
+ btn.setAttribute('aria-label', label);
654
+ btn.disabled = collapsed;
655
+ btn.append(makeSvgIcon(icon));
656
+ btn.addEventListener('mousedown', (event) => event.preventDefault());
657
+ btn.addEventListener('click', () => {
658
+ void view.toggleMark(mark);
659
+ });
660
+ box.append(btn);
661
+ }
662
+ box.append(el('span', 'idocs-ws-divider'));
663
+ const alignAnchor = el('div', 'idocs-ws-popover-anchor');
664
+ const alignBtn = el('button', 'idocs-ws-icon-btn');
665
+ alignBtn.type = 'button';
666
+ alignBtn.title = '对齐方式';
667
+ alignBtn.setAttribute('aria-label', '对齐方式');
668
+ alignBtn.setAttribute('aria-haspopup', 'menu');
669
+ alignBtn.setAttribute('aria-expanded', 'false');
670
+ alignBtn.disabled = ctx.kind !== 'editable';
671
+ alignBtn.append(makeSvgIcon(ICONS.align));
672
+ alignBtn.addEventListener('mousedown', (event) => event.preventDefault());
673
+ const alignPopover = el('div', 'idocs-ws-popover is-down idocs-ws-align-popover');
674
+ alignPopover.setAttribute('role', 'menu');
675
+ alignPopover.hidden = true;
676
+ const currentAlign = ctx.kind === 'editable' ? (ctx.align ?? 'left') : undefined;
677
+ for (const [align, label] of [
678
+ ['left', '左对齐'],
679
+ ['center', '居中对齐'],
680
+ ['right', '右对齐'],
681
+ ['justify', '两端对齐'],
682
+ ]) {
683
+ const isSelected = currentAlign === align;
684
+ const item = el('button', isSelected ? 'idocs-ws-menu-item is-active' : 'idocs-ws-menu-item');
685
+ item.type = 'button';
686
+ item.setAttribute('role', 'menuitemradio');
687
+ item.setAttribute('aria-checked', String(isSelected));
688
+ item.append(el('span', 'idocs-ws-menu-label', label));
689
+ if (isSelected) {
690
+ const checkWrap = el('span', 'idocs-ws-menu-check');
691
+ checkWrap.setAttribute('aria-hidden', 'true');
692
+ checkWrap.append(makeSvgIcon(ICONS.check, 14));
693
+ item.append(checkWrap);
694
+ }
695
+ item.addEventListener('mousedown', (event) => event.preventDefault());
696
+ item.addEventListener('click', (event) => {
697
+ event.stopPropagation();
698
+ closeAllPopovers();
699
+ void view.setAlignment(align);
700
+ });
701
+ alignPopover.append(item);
702
+ }
703
+ alignBtn.addEventListener('click', (event) => {
704
+ event.stopPropagation();
705
+ const nextHidden = !alignPopover.hidden;
706
+ closeAllPopovers();
707
+ alignPopover.hidden = nextHidden;
708
+ alignBtn.setAttribute('aria-expanded', String(!nextHidden));
709
+ });
710
+ alignPopover.addEventListener('click', (event) => event.stopPropagation());
711
+ alignAnchor.append(alignBtn, alignPopover);
712
+ box.append(alignAnchor);
713
+ tbCenter.append(box);
714
+ };
715
+ const paintThumbnails = () => {
716
+ if (!activeDoc || railTab !== 'pages')
717
+ return;
718
+ const canvases = railList.querySelectorAll('.idocs-ws-thumb-canvas');
719
+ const dpr = Math.max(1, Math.min(2, globalThis.devicePixelRatio || 1));
720
+ canvases.forEach((canvas, idx) => {
721
+ const page = activeDoc?.pages[idx];
722
+ if (!page)
723
+ return;
724
+ const cssWidth = Math.max(120, Math.round(canvas.clientWidth || 164));
725
+ const scale = cssWidth / page.width;
726
+ const cssHeight = Math.max(40, Math.round(page.height * scale));
727
+ const pxPerPt = scale * dpr;
728
+ canvas.width = Math.max(1, Math.round(cssWidth * dpr));
729
+ canvas.height = Math.max(1, Math.round(cssHeight * dpr));
730
+ canvas.style.aspectRatio = `${page.width} / ${page.height}`;
731
+ const ctx = canvas.getContext('2d');
732
+ if (!ctx)
733
+ return;
734
+ paintPage(ctx, page, pxPerPt, thumbImages, activeDoc?.glyphOutlines ?? []);
735
+ });
736
+ };
737
+ const updateRailHighlight = (pageIndex) => {
738
+ const cards = railList.querySelectorAll('.idocs-ws-thumb-card');
739
+ cards.forEach((card, idx) => {
740
+ const active = idx === pageIndex;
741
+ card.classList.toggle('is-active', active);
742
+ if (active)
743
+ card.setAttribute('aria-current', 'page');
744
+ else
745
+ card.removeAttribute('aria-current');
746
+ });
747
+ };
748
+ const updateOutlineHighlight = (activePath) => {
749
+ if (railTab !== 'outline' || activePath === lastOutlinePath)
750
+ return;
751
+ lastOutlinePath = activePath;
752
+ const items = railList.querySelectorAll('.idocs-ws-outline-item');
753
+ items.forEach((item) => {
754
+ item.classList.toggle('is-active', Boolean(activePath && item.dataset.path === activePath));
755
+ });
756
+ };
757
+ const renderRail = () => {
758
+ lastOutlinePath = undefined;
759
+ pagesTabBtn.setAttribute('aria-selected', String(railTab === 'pages'));
760
+ outlineTabBtn.setAttribute('aria-selected', String(railTab === 'outline'));
761
+ railList.classList.toggle('is-outline', railTab === 'outline');
762
+ railList.innerHTML = '';
763
+ if (railTab === 'pages') {
764
+ const pages = activeDoc?.pages ?? [];
765
+ if (pages.length === 0) {
766
+ railList.append(el('div', 'idocs-ws-empty', '正在加载页面…'));
767
+ return;
768
+ }
769
+ pages.forEach((_, idx) => {
770
+ const card = el('button', 'idocs-ws-thumb-card');
771
+ card.type = 'button';
772
+ card.setAttribute('aria-label', `第 ${idx + 1} 页`);
773
+ if (idx === view.currentPage) {
774
+ card.classList.add('is-active');
775
+ card.setAttribute('aria-current', 'page');
776
+ }
777
+ const sheet = el('span', 'idocs-ws-thumb-sheet');
778
+ sheet.setAttribute('aria-hidden', 'true');
779
+ const canvas = el('canvas', 'idocs-ws-thumb-canvas');
780
+ sheet.append(canvas);
781
+ const label = el('span', 'idocs-ws-thumb-label', String(idx + 1));
782
+ label.setAttribute('aria-hidden', 'true');
783
+ card.append(sheet, label);
784
+ card.addEventListener('click', () => view.goToPage(idx, { behavior: 'smooth' }));
785
+ railList.append(card);
786
+ });
787
+ paintThumbnails();
788
+ return;
789
+ }
790
+ const entries = activeDoc ? extractOutline(activeDoc) : [];
791
+ if (entries.length === 0) {
792
+ railList.append(el('div', 'idocs-ws-empty', '暂无标题,可将段落设为一级至三级标题'));
793
+ return;
794
+ }
795
+ const activePath = view.selection()?.path;
796
+ for (const entry of entries) {
797
+ const item = el('button', `idocs-ws-outline-item level-${entry.level}`, entry.text);
798
+ item.type = 'button';
799
+ item.dataset.path = entry.path;
800
+ item.classList.toggle('is-active', activePath === entry.path);
801
+ item.title = entry.text;
802
+ item.addEventListener('click', () => {
803
+ view.goToPage(entry.page, { behavior: 'smooth' });
804
+ if (entry.anchor) {
805
+ void view.selectAnchors(entry.anchor);
806
+ }
807
+ });
808
+ railList.append(item);
809
+ }
810
+ };
811
+ const syncHistoryButtons = (state) => {
812
+ undoBtn.disabled = readOnly || !state.canUndo;
813
+ redoBtn.disabled = readOnly || !state.canRedo;
814
+ };
815
+ const syncDocument = (doc) => {
816
+ activeDoc = doc;
817
+ if (!sameImageList(lastThumbImages, doc.images)) {
818
+ lastThumbImages = doc.images;
819
+ for (const img of thumbImages)
820
+ img?.close();
821
+ thumbImages = [];
822
+ if (doc.images.length > 0) {
823
+ const expected = doc;
824
+ void decodeImages(doc.images).then((decoded) => {
825
+ if (destroyed || activeDoc !== expected) {
826
+ for (const img of decoded)
827
+ img?.close();
828
+ return;
829
+ }
830
+ thumbImages = decoded;
831
+ paintThumbnails();
832
+ });
833
+ }
834
+ }
835
+ renderRail();
836
+ renderToolbarCenter();
837
+ };
838
+ view = mountEditor(editorHost, session, {
839
+ ...options.view,
840
+ bottomInset: options.view?.bottomInset ?? 68,
841
+ readOnly,
842
+ onPageChange: (page, doc) => {
843
+ updateRailHighlight(page);
844
+ options.view?.onPageChange?.(page, doc);
845
+ },
846
+ onZoomChange: (next) => {
847
+ syncZoom();
848
+ if (!selPill.hidden)
849
+ updatePill(view.selection());
850
+ options.view?.onZoomChange?.(next);
851
+ },
852
+ onSelectionChange: (sel) => {
853
+ updatePill(sel);
854
+ updateOutlineHighlight(sel?.path);
855
+ renderToolbarCenter();
856
+ options.view?.onSelectionChange?.(sel);
857
+ },
858
+ onReadOnlyTarget: (target) => {
859
+ updatePill(null);
860
+ renderToolbarCenter();
861
+ options.view?.onReadOnlyTarget?.(target);
862
+ },
863
+ onImageSelect: (target) => {
864
+ updatePill(null);
865
+ renderToolbarCenter();
866
+ options.view?.onImageSelect?.(target);
867
+ },
868
+ });
869
+ const onSessionState = (state) => {
870
+ if (destroyed)
871
+ return;
872
+ syncHistoryButtons(state);
873
+ if (state.layout.kind === 'ready') {
874
+ syncDocument(state.layout.frame.document);
875
+ }
876
+ else if (state.layout.kind === 'pending' && !activeDoc && state.layout.previous) {
877
+ syncDocument(state.layout.previous.document);
878
+ }
879
+ };
880
+ const unsubscribe = session.subscribe(onSessionState);
881
+ onSessionState(session.state());
882
+ syncZoom();
883
+ const onDocClick = () => closeAllPopovers();
884
+ const onDocKeydown = (event) => {
885
+ if (event.key === 'Escape')
886
+ closeAllPopovers();
887
+ };
888
+ document.addEventListener('click', onDocClick);
889
+ document.addEventListener('keydown', onDocKeydown);
890
+ const applyRailOpen = (open) => {
891
+ railOpen = open;
892
+ body.classList.toggle('is-rail-collapsed', !open);
893
+ if ('inert' in rail)
894
+ rail.inert = !open;
895
+ rail.setAttribute('aria-hidden', String(!open));
896
+ const label = open ? '收起页面列表' : '展开页面列表';
897
+ railToggleBtn.title = label;
898
+ railToggleBtn.setAttribute('aria-label', label);
899
+ railToggleBtn.setAttribute('aria-expanded', String(open));
900
+ };
901
+ editorHost
902
+ .querySelector('.idocs-preview-scroller')
903
+ ?.addEventListener('scroll', () => {
904
+ if (!selPill.hidden)
905
+ updatePill(view.selection());
906
+ }, { passive: true });
907
+ railSlot.addEventListener('transitionend', () => updatePill(view.selection()));
908
+ railToggleBtn.addEventListener('click', () => applyRailOpen(!railOpen));
909
+ pagesTabBtn.addEventListener('click', () => {
910
+ if (railTab === 'pages')
911
+ return;
912
+ railTab = 'pages';
913
+ renderRail();
914
+ });
915
+ outlineTabBtn.addEventListener('click', () => {
916
+ if (railTab === 'outline')
917
+ return;
918
+ railTab = 'outline';
919
+ renderRail();
920
+ });
921
+ undoBtn.addEventListener('click', () => void view.undo());
922
+ redoBtn.addEventListener('click', () => void view.redo());
923
+ zoomOutBtn.addEventListener('click', () => {
924
+ view.setZoom(Math.max(0.25, view.zoom - 0.15));
925
+ syncZoom();
926
+ });
927
+ zoomInBtn.addEventListener('click', () => {
928
+ view.setZoom(Math.min(4, view.zoom + 0.15));
929
+ syncZoom();
930
+ });
931
+ fitWidthBtn.addEventListener('click', () => {
932
+ view.setZoom(view.fitWidthZoom);
933
+ syncZoom();
934
+ });
935
+ findBtn.addEventListener('click', () => view.find.open());
936
+ pillQuoteBtn?.addEventListener('click', () => {
937
+ const sel = view.selection();
938
+ if (!sel || !sel.text)
939
+ return;
940
+ options.onQuoteSelection?.({ text: sel.text, path: sel.path, page: sel.page });
941
+ });
942
+ dockParaBtn.addEventListener('click', () => {
943
+ void view.insertBlock({ kind: 'paragraph' });
944
+ });
945
+ dockHeadingBtn.addEventListener('click', () => {
946
+ void view.insertBlock({ kind: 'heading', level: 2 });
947
+ });
948
+ dockCalloutBtn.addEventListener('click', () => {
949
+ void view.insertBlock({ kind: 'callout' });
950
+ });
951
+ dockPageBreakBtn.addEventListener('click', () => {
952
+ void view.insertBlock({ kind: 'pageBreak' });
953
+ });
954
+ dockImageBtn?.addEventListener('click', async () => {
955
+ const picked = await options.onPickImage?.();
956
+ if (picked && !destroyed) {
957
+ await view.insertBlock({ kind: 'image', src: picked.src, alt: picked.alt });
958
+ }
959
+ });
960
+ dockTableBtn.addEventListener('click', (event) => {
961
+ event.stopPropagation();
962
+ const nextHidden = !tablePopover.hidden;
963
+ closeAllPopovers();
964
+ tablePopover.hidden = nextHidden;
965
+ dockTableBtn.setAttribute('aria-expanded', String(!nextHidden));
966
+ });
967
+ tablePopover.addEventListener('click', (event) => event.stopPropagation());
968
+ tableGrid.addEventListener('mouseover', (event) => {
969
+ const cell = event.target?.closest('.idocs-ws-table-cell');
970
+ if (!cell)
971
+ return;
972
+ paintTableGrid(Number(cell.dataset.rows), Number(cell.dataset.cols));
973
+ });
974
+ tableGrid.addEventListener('click', (event) => {
975
+ const cell = event.target?.closest('.idocs-ws-table-cell');
976
+ const rows = cell ? Number(cell.dataset.rows) : hoverRows;
977
+ const cols = cell ? Number(cell.dataset.cols) : hoverCols;
978
+ closeAllPopovers();
979
+ void view.insertBlock({ kind: 'table', rows, cols });
980
+ });
981
+ return {
982
+ view,
983
+ get readOnly() {
984
+ return readOnly;
985
+ },
986
+ get processing() {
987
+ return processing;
988
+ },
989
+ setReadOnly(next, nextBusyLabel, nextProcessing) {
990
+ if (destroyed)
991
+ return;
992
+ readOnly = next;
993
+ busyLabel = nextBusyLabel;
994
+ processing = next ? (nextProcessing ?? Boolean(nextBusyLabel)) : false;
995
+ root.classList.toggle('is-readonly', readOnly);
996
+ root.classList.toggle('is-processing', processing);
997
+ view.setReadOnly(next);
998
+ dock.hidden = next;
999
+ if (next) {
1000
+ selPill.hidden = true;
1001
+ closeAllPopovers();
1002
+ }
1003
+ else {
1004
+ updatePill(view.selection());
1005
+ }
1006
+ syncHistoryButtons(session.state());
1007
+ renderToolbarCenter();
1008
+ },
1009
+ setRailOpen(open) {
1010
+ if (destroyed)
1011
+ return;
1012
+ applyRailOpen(open);
1013
+ },
1014
+ destroy() {
1015
+ if (destroyed)
1016
+ return;
1017
+ destroyed = true;
1018
+ document.removeEventListener('click', onDocClick);
1019
+ document.removeEventListener('keydown', onDocKeydown);
1020
+ unsubscribe();
1021
+ view.destroy();
1022
+ for (const img of thumbImages)
1023
+ img?.close();
1024
+ thumbImages = [];
1025
+ root.remove();
1026
+ },
1027
+ };
1028
+ }