@aseity/idocs-preview 0.4.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 +6 -0
- package/README.md +23 -3
- package/dist/core/display.d.ts +23 -0
- package/dist/core/editor.d.ts +8 -0
- package/dist/core/editor.js +27 -21
- package/dist/core/index.d.ts +1 -1
- package/dist/view/edit-selection.d.ts +4 -2
- package/dist/view/edit-selection.js +58 -113
- package/dist/view/editor.d.ts +1 -0
- package/dist/view/editor.js +23 -15
- package/dist/view/ime.js +5 -5
- package/dist/view/index.d.ts +1 -1
- package/dist/view/workspace-styles.js +110 -50
- package/dist/view/workspace.d.ts +10 -3
- package/dist/view/workspace.js +244 -151
- package/package.json +7 -2
- package/wasm/idocs_wasm_bg.wasm +0 -0
package/dist/view/workspace.js
CHANGED
|
@@ -15,12 +15,12 @@ const ICONS = {
|
|
|
15
15
|
zoomIn: '<path d="M10 4.5v11M4.5 10h11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>',
|
|
16
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
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="
|
|
19
|
-
heading: '<path d="
|
|
20
|
-
table: '<rect x="3" y="4" width="14" height="12" rx="2"
|
|
21
|
-
callout: '<rect x="3" y="
|
|
22
|
-
image: '<rect x="3" y="
|
|
23
|
-
pageBreak: '<path d="
|
|
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
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
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
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"/>',
|
|
@@ -47,6 +47,72 @@ function el(tag, className, text) {
|
|
|
47
47
|
node.textContent = text;
|
|
48
48
|
return node;
|
|
49
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
|
+
}
|
|
50
116
|
function headingLevelFromStyle(style) {
|
|
51
117
|
switch (style) {
|
|
52
118
|
case 'heading1':
|
|
@@ -99,22 +165,6 @@ export function extractOutline(doc) {
|
|
|
99
165
|
}
|
|
100
166
|
return entries;
|
|
101
167
|
}
|
|
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
168
|
export function isTableCellPath(path) {
|
|
119
169
|
return /\.rows\.\d+\.cells\.\d+(?:\.|$)/.test(path);
|
|
120
170
|
}
|
|
@@ -144,8 +194,7 @@ export function resolveToolbarContext(input) {
|
|
|
144
194
|
};
|
|
145
195
|
}
|
|
146
196
|
if (input.readOnlyTarget) {
|
|
147
|
-
|
|
148
|
-
return { kind: 'readOnlyTarget', target: input.readOnlyTarget, ...meta };
|
|
197
|
+
return { kind: 'readOnlyTarget', target: input.readOnlyTarget };
|
|
149
198
|
}
|
|
150
199
|
if (input.selectedImage) {
|
|
151
200
|
return { kind: 'image', target: input.selectedImage };
|
|
@@ -177,24 +226,14 @@ function sameImageList(a, b) {
|
|
|
177
226
|
}
|
|
178
227
|
return true;
|
|
179
228
|
}
|
|
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
229
|
function toolbarRenderKey(ctx, tableModePreferred) {
|
|
193
230
|
switch (ctx.kind) {
|
|
194
231
|
case 'readOnly':
|
|
195
232
|
return `ro:${ctx.processing}:${ctx.label}`;
|
|
196
233
|
case 'editable':
|
|
197
|
-
|
|
234
|
+
if (ctx.inTable && tableModePreferred)
|
|
235
|
+
return 'table';
|
|
236
|
+
return `ed:${ctx.inTable}:${ctx.blockStyle ?? ''}:${ctx.align ?? ''}:${ctx.collapsed}`;
|
|
198
237
|
case 'empty':
|
|
199
238
|
return 'empty';
|
|
200
239
|
case 'readOnlyTarget':
|
|
@@ -213,8 +252,10 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
213
252
|
let lastToolbarKey = '';
|
|
214
253
|
let lastOutlinePath;
|
|
215
254
|
let activeDoc;
|
|
255
|
+
let renderedRailDoc;
|
|
256
|
+
let renderedRailTab;
|
|
216
257
|
let thumbImages = [];
|
|
217
|
-
let lastThumbImages;
|
|
258
|
+
let lastThumbImages = [];
|
|
218
259
|
let destroyed = false;
|
|
219
260
|
const root = el('div', 'idocs-ws');
|
|
220
261
|
root.classList.toggle('is-readonly', readOnly);
|
|
@@ -222,9 +263,42 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
222
263
|
const toolbar = el('nav', 'idocs-ws-toolbar');
|
|
223
264
|
toolbar.setAttribute('aria-label', '文档格式工具栏');
|
|
224
265
|
const tbLeft = el('div', 'idocs-ws-tb-left');
|
|
225
|
-
const tbCenter = el('div', 'idocs-ws-tb-center');
|
|
226
266
|
const tbRight = el('div', 'idocs-ws-tb-right');
|
|
227
|
-
|
|
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
|
+
};
|
|
228
302
|
const railToggleBtn = el('button', 'idocs-ws-icon-btn');
|
|
229
303
|
railToggleBtn.type = 'button';
|
|
230
304
|
const initialRailLabel = railOpen ? '收起页面列表' : '展开页面列表';
|
|
@@ -304,17 +378,33 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
304
378
|
const dock = el('nav', 'idocs-ws-dock');
|
|
305
379
|
dock.setAttribute('aria-label', '插入内容');
|
|
306
380
|
dock.hidden = readOnly;
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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;
|
|
312
406
|
};
|
|
313
|
-
const dockParaBtn = makeDockBtn(ICONS.paragraph, '段落');
|
|
314
|
-
const dockHeadingBtn = makeDockBtn(ICONS.heading, '标题');
|
|
315
407
|
const tableAnchor = el('div', 'idocs-ws-popover-anchor');
|
|
316
|
-
const dockTableBtn = makeDockBtn(ICONS.table, '表格');
|
|
317
|
-
dockTableBtn.setAttribute('aria-expanded', 'false');
|
|
318
408
|
const tablePopover = el('div', 'idocs-ws-popover is-up idocs-ws-table-popover');
|
|
319
409
|
tablePopover.hidden = true;
|
|
320
410
|
const tableGridHead = el('div', 'idocs-ws-table-grid-head', '3 × 3 表格');
|
|
@@ -334,14 +424,7 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
334
424
|
}
|
|
335
425
|
}
|
|
336
426
|
tablePopover.append(tableGridHead, tableGrid);
|
|
337
|
-
tableAnchor.append(
|
|
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);
|
|
427
|
+
tableAnchor.append(tablePopover);
|
|
345
428
|
stage.append(editorHost, selPill, dock);
|
|
346
429
|
railSlot.append(rail);
|
|
347
430
|
body.append(railSlot, stage);
|
|
@@ -358,15 +441,13 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
358
441
|
}
|
|
359
442
|
};
|
|
360
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
|
+
};
|
|
361
448
|
const closeAllPopovers = () => {
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
});
|
|
449
|
+
setTablePopoverOpen(false);
|
|
450
|
+
closeToolbarMenu();
|
|
370
451
|
};
|
|
371
452
|
let view;
|
|
372
453
|
const syncZoom = () => {
|
|
@@ -377,9 +458,8 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
377
458
|
view?.dragging ||
|
|
378
459
|
!sel ||
|
|
379
460
|
!sel.clientRect ||
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
!pillQuoteBtn) {
|
|
461
|
+
!pillQuoteBtn ||
|
|
462
|
+
!sel.excerpt?.trim()) {
|
|
383
463
|
selPill.hidden = true;
|
|
384
464
|
return;
|
|
385
465
|
}
|
|
@@ -408,7 +488,8 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
408
488
|
return;
|
|
409
489
|
}
|
|
410
490
|
lastToolbarKey = nextKey ?? '';
|
|
411
|
-
|
|
491
|
+
closeToolbarMenu();
|
|
492
|
+
tbContent.replaceChildren();
|
|
412
493
|
if (ctx.kind === 'readOnly') {
|
|
413
494
|
if (ctx.processing) {
|
|
414
495
|
const status = el('span', 'idocs-ws-processing-indicator');
|
|
@@ -418,21 +499,22 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
418
499
|
const ring = el('span', 'idocs-ws-spinner');
|
|
419
500
|
ring.setAttribute('aria-hidden', 'true');
|
|
420
501
|
status.append(ring);
|
|
421
|
-
|
|
502
|
+
tbContent.append(status);
|
|
422
503
|
}
|
|
423
504
|
return;
|
|
424
505
|
}
|
|
425
506
|
if (ctx.kind === 'readOnlyTarget') {
|
|
426
|
-
const box = el('div', 'idocs-ws-ctx');
|
|
427
|
-
box.append(el('span', 'idocs-ws-badge is-warn',
|
|
428
|
-
|
|
429
|
-
if (options.onQuoteSelection && activeDoc) {
|
|
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()) {
|
|
430
510
|
const quoteBtn = el('button', 'idocs-ws-pill-btn');
|
|
431
511
|
quoteBtn.type = 'button';
|
|
432
512
|
quoteBtn.append(makeSvgIcon(ICONS.ask, 15), el('span', undefined, askLabel));
|
|
433
513
|
quoteBtn.addEventListener('click', () => {
|
|
434
|
-
const
|
|
435
|
-
|
|
514
|
+
const target = view.readOnlyTarget();
|
|
515
|
+
if (!target?.excerpt?.trim())
|
|
516
|
+
return;
|
|
517
|
+
options.onQuoteSelection?.({ text: target.excerpt, path: target.path, page: target.page });
|
|
436
518
|
});
|
|
437
519
|
box.append(quoteBtn);
|
|
438
520
|
}
|
|
@@ -443,7 +525,7 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
443
525
|
view.focus();
|
|
444
526
|
});
|
|
445
527
|
box.append(clearBtn);
|
|
446
|
-
|
|
528
|
+
tbContent.append(box);
|
|
447
529
|
return;
|
|
448
530
|
}
|
|
449
531
|
if (ctx.kind === 'image') {
|
|
@@ -485,12 +567,12 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
485
567
|
doneBtn.type = 'button';
|
|
486
568
|
doneBtn.addEventListener('click', () => view.clearSelectedImage());
|
|
487
569
|
box.append(doneBtn);
|
|
488
|
-
|
|
570
|
+
tbContent.append(box);
|
|
489
571
|
return;
|
|
490
572
|
}
|
|
491
|
-
const box = el('div', 'idocs-ws-ctx');
|
|
492
573
|
const inTable = ctx.kind === 'editable' && ctx.inTable;
|
|
493
574
|
const showTable = inTable && tableModePreferred;
|
|
575
|
+
const box = el('div', showTable ? 'idocs-ws-ctx is-table' : 'idocs-ws-ctx');
|
|
494
576
|
if (inTable) {
|
|
495
577
|
const seg = el('div', 'idocs-ws-seg');
|
|
496
578
|
const textTab = el('button', 'idocs-ws-seg-btn', '文字');
|
|
@@ -512,23 +594,25 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
512
594
|
}
|
|
513
595
|
if (showTable) {
|
|
514
596
|
const tableActions = [
|
|
515
|
-
['insertRowAbove', '
|
|
516
|
-
['insertRowBelow', '
|
|
517
|
-
['insertColumnLeft', '
|
|
518
|
-
['insertColumnRight', '
|
|
519
|
-
['deleteRow', '
|
|
520
|
-
['deleteColumn', '
|
|
521
|
-
['toggleHeaderRow', '
|
|
597
|
+
['insertRowAbove', '上方插入行'],
|
|
598
|
+
['insertRowBelow', '下方插入行'],
|
|
599
|
+
['insertColumnLeft', '左侧插入列'],
|
|
600
|
+
['insertColumnRight', '右侧插入列'],
|
|
601
|
+
['deleteRow', '删除行', true],
|
|
602
|
+
['deleteColumn', '删除列', true],
|
|
603
|
+
['toggleHeaderRow', '重复表头'],
|
|
522
604
|
];
|
|
523
605
|
for (const [action, label, danger] of tableActions) {
|
|
524
606
|
const btn = el('button', danger ? 'idocs-ws-pill-btn is-danger' : 'idocs-ws-pill-btn', label);
|
|
525
607
|
btn.type = 'button';
|
|
608
|
+
if (action === 'toggleHeaderRow')
|
|
609
|
+
btn.title = '开启或关闭跨页重复表头';
|
|
526
610
|
btn.addEventListener('click', () => {
|
|
527
611
|
void view.editTable(action);
|
|
528
612
|
});
|
|
529
613
|
box.append(btn);
|
|
530
614
|
}
|
|
531
|
-
|
|
615
|
+
tbContent.append(box);
|
|
532
616
|
return;
|
|
533
617
|
}
|
|
534
618
|
const styleOptions = [
|
|
@@ -540,7 +624,6 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
540
624
|
const activeStyle = ctx.kind === 'editable' ? ctx.blockStyle : undefined;
|
|
541
625
|
const selectedStyle = activeStyle ?? 'body';
|
|
542
626
|
const selectedLabel = styleOptions.find(([v]) => v === selectedStyle)?.[1] ?? '正文';
|
|
543
|
-
const styleAnchor = el('div', 'idocs-ws-popover-anchor');
|
|
544
627
|
const styleTrigger = el('button', 'idocs-ws-select-trigger');
|
|
545
628
|
styleTrigger.type = 'button';
|
|
546
629
|
styleTrigger.setAttribute('role', 'combobox');
|
|
@@ -596,11 +679,9 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
596
679
|
const toggleStylePopover = (forceOpen) => {
|
|
597
680
|
if (styleTrigger.disabled)
|
|
598
681
|
return;
|
|
599
|
-
const shouldOpen = forceOpen ?? stylePopover.hidden;
|
|
600
|
-
|
|
682
|
+
const shouldOpen = forceOpen ?? Boolean(stylePopover.hidden);
|
|
683
|
+
toggleToolbarMenu(styleTrigger, stylePopover, shouldOpen);
|
|
601
684
|
if (shouldOpen) {
|
|
602
|
-
stylePopover.hidden = false;
|
|
603
|
-
styleTrigger.setAttribute('aria-expanded', 'true');
|
|
604
685
|
syncStyleHighlight(Math.max(0, styleOptions.findIndex(([v]) => v === selectedStyle)));
|
|
605
686
|
}
|
|
606
687
|
};
|
|
@@ -637,8 +718,7 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
637
718
|
}
|
|
638
719
|
});
|
|
639
720
|
stylePopover.addEventListener('click', (event) => event.stopPropagation());
|
|
640
|
-
|
|
641
|
-
box.append(styleAnchor, el('span', 'idocs-ws-divider'));
|
|
721
|
+
box.append(styleTrigger, stylePopover, el('span', 'idocs-ws-divider'));
|
|
642
722
|
const collapsed = ctx.kind !== 'editable' || ctx.collapsed;
|
|
643
723
|
const marks = [
|
|
644
724
|
['bold', '粗体', ICONS.bold],
|
|
@@ -660,7 +740,6 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
660
740
|
box.append(btn);
|
|
661
741
|
}
|
|
662
742
|
box.append(el('span', 'idocs-ws-divider'));
|
|
663
|
-
const alignAnchor = el('div', 'idocs-ws-popover-anchor');
|
|
664
743
|
const alignBtn = el('button', 'idocs-ws-icon-btn');
|
|
665
744
|
alignBtn.type = 'button';
|
|
666
745
|
alignBtn.title = '对齐方式';
|
|
@@ -702,18 +781,14 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
702
781
|
}
|
|
703
782
|
alignBtn.addEventListener('click', (event) => {
|
|
704
783
|
event.stopPropagation();
|
|
705
|
-
|
|
706
|
-
closeAllPopovers();
|
|
707
|
-
alignPopover.hidden = nextHidden;
|
|
708
|
-
alignBtn.setAttribute('aria-expanded', String(!nextHidden));
|
|
784
|
+
toggleToolbarMenu(alignBtn, alignPopover, Boolean(alignPopover.hidden));
|
|
709
785
|
});
|
|
710
786
|
alignPopover.addEventListener('click', (event) => event.stopPropagation());
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
tbCenter.append(box);
|
|
787
|
+
box.append(alignBtn, alignPopover);
|
|
788
|
+
tbContent.append(box);
|
|
714
789
|
};
|
|
715
790
|
const paintThumbnails = () => {
|
|
716
|
-
if (!activeDoc || railTab !== 'pages')
|
|
791
|
+
if (!railOpen || !activeDoc || railTab !== 'pages')
|
|
717
792
|
return;
|
|
718
793
|
const canvases = railList.querySelectorAll('.idocs-ws-thumb-canvas');
|
|
719
794
|
const dpr = Math.max(1, Math.min(2, globalThis.devicePixelRatio || 1));
|
|
@@ -755,12 +830,37 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
755
830
|
});
|
|
756
831
|
};
|
|
757
832
|
const renderRail = () => {
|
|
833
|
+
if (!railOpen || (renderedRailDoc === activeDoc && renderedRailTab === railTab))
|
|
834
|
+
return;
|
|
835
|
+
renderedRailDoc = activeDoc;
|
|
836
|
+
renderedRailTab = railTab;
|
|
758
837
|
lastOutlinePath = undefined;
|
|
759
838
|
pagesTabBtn.setAttribute('aria-selected', String(railTab === 'pages'));
|
|
760
839
|
outlineTabBtn.setAttribute('aria-selected', String(railTab === 'outline'));
|
|
761
840
|
railList.classList.toggle('is-outline', railTab === 'outline');
|
|
762
841
|
railList.innerHTML = '';
|
|
763
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
|
+
}
|
|
764
864
|
const pages = activeDoc?.pages ?? [];
|
|
765
865
|
if (pages.length === 0) {
|
|
766
866
|
railList.append(el('div', 'idocs-ws-empty', '正在加载页面…'));
|
|
@@ -780,7 +880,7 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
780
880
|
sheet.append(canvas);
|
|
781
881
|
const label = el('span', 'idocs-ws-thumb-label', String(idx + 1));
|
|
782
882
|
label.setAttribute('aria-hidden', 'true');
|
|
783
|
-
card.append(
|
|
883
|
+
card.append(label, sheet);
|
|
784
884
|
card.addEventListener('click', () => view.goToPage(idx, { behavior: 'smooth' }));
|
|
785
885
|
railList.append(card);
|
|
786
886
|
});
|
|
@@ -813,25 +913,9 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
813
913
|
redoBtn.disabled = readOnly || !state.canRedo;
|
|
814
914
|
};
|
|
815
915
|
const syncDocument = (doc) => {
|
|
916
|
+
if (doc === activeDoc)
|
|
917
|
+
return;
|
|
816
918
|
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
919
|
renderRail();
|
|
836
920
|
renderToolbarCenter();
|
|
837
921
|
};
|
|
@@ -880,7 +964,10 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
880
964
|
const unsubscribe = session.subscribe(onSessionState);
|
|
881
965
|
onSessionState(session.state());
|
|
882
966
|
syncZoom();
|
|
883
|
-
const onDocClick = () =>
|
|
967
|
+
const onDocClick = (event) => {
|
|
968
|
+
if (!event.composedPath().includes(tableAnchor))
|
|
969
|
+
closeAllPopovers();
|
|
970
|
+
};
|
|
884
971
|
const onDocKeydown = (event) => {
|
|
885
972
|
if (event.key === 'Escape')
|
|
886
973
|
closeAllPopovers();
|
|
@@ -897,6 +984,13 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
897
984
|
railToggleBtn.title = label;
|
|
898
985
|
railToggleBtn.setAttribute('aria-label', label);
|
|
899
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
|
+
}
|
|
900
994
|
};
|
|
901
995
|
editorHost
|
|
902
996
|
.querySelector('.idocs-preview-scroller')
|
|
@@ -935,36 +1029,33 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
935
1029
|
findBtn.addEventListener('click', () => view.find.open());
|
|
936
1030
|
pillQuoteBtn?.addEventListener('click', () => {
|
|
937
1031
|
const sel = view.selection();
|
|
938
|
-
|
|
1032
|
+
const text = sel?.excerpt;
|
|
1033
|
+
if (!sel || !text?.trim())
|
|
939
1034
|
return;
|
|
940
|
-
options.onQuoteSelection?.({ text
|
|
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' });
|
|
1035
|
+
options.onQuoteSelection?.({ text, path: sel.path, page: sel.page });
|
|
950
1036
|
});
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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);
|
|
968
1059
|
tableGrid.addEventListener('mouseover', (event) => {
|
|
969
1060
|
const cell = event.target?.closest('.idocs-ws-table-cell');
|
|
970
1061
|
if (!cell)
|
|
@@ -1015,10 +1106,12 @@ export function mountEditorWorkspace(container, session, options = {}) {
|
|
|
1015
1106
|
if (destroyed)
|
|
1016
1107
|
return;
|
|
1017
1108
|
destroyed = true;
|
|
1109
|
+
toolbarScroller.destroy();
|
|
1018
1110
|
document.removeEventListener('click', onDocClick);
|
|
1019
1111
|
document.removeEventListener('keydown', onDocKeydown);
|
|
1020
1112
|
unsubscribe();
|
|
1021
1113
|
view.destroy();
|
|
1114
|
+
detachInsertButtons?.();
|
|
1022
1115
|
for (const img of thumbImages)
|
|
1023
1116
|
img?.close();
|
|
1024
1117
|
thumbImages = [];
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aseity/idocs-preview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Browser preview and editor for iDocs source documents, with DOCX preview and export, Web Worker layout, and Canvas rendering.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ty026/idocs.git",
|
|
9
|
+
"directory": "web"
|
|
10
|
+
},
|
|
6
11
|
"type": "module",
|
|
7
12
|
"exports": {
|
|
8
13
|
"./core": {
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
"predev": "node scripts/build-wasm.mjs",
|
|
37
42
|
"dev": "vite dev",
|
|
38
43
|
"changeset": "changeset",
|
|
39
|
-
"release:version": "changeset version && npm install --package-lock-only",
|
|
44
|
+
"release:version": "changeset version && npm install --package-lock-only --ignore-scripts --no-audit --no-fund",
|
|
40
45
|
"release:check": "node scripts/release-pack.mjs"
|
|
41
46
|
},
|
|
42
47
|
"engines": {
|
package/wasm/idocs_wasm_bg.wasm
CHANGED
|
Binary file
|