@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.
@@ -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="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"/>',
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
- const meta = readOnlyMeta(input.readOnlyTarget.reason);
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
- return `ed:${ctx.inTable}:${ctx.inTable && tableModePreferred}:${ctx.blockStyle ?? ''}:${ctx.align ?? ''}:${ctx.collapsed}`;
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
- toolbar.append(tbLeft, tbCenter, tbRight);
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 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;
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(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);
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
- 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
- });
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
- (sel.selection.range.start.flow === sel.selection.range.end.flow &&
381
- sel.selection.range.start.offset === sel.selection.range.end.offset) ||
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
- tbCenter.innerHTML = '';
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
- tbCenter.append(status);
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', ctx.badge));
428
- box.append(el('span', 'idocs-ws-note', ctx.message));
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 text = readOnlyQuoteText(activeDoc, ctx.target);
435
- options.onQuoteSelection?.({ text, path: ctx.target.path ?? '', page: ctx.target.page });
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
- tbCenter.append(box);
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
- tbCenter.append(box);
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', '删行', true],
520
- ['deleteColumn', '删列', true],
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
- tbCenter.append(box);
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
- closeAllPopovers();
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
- styleAnchor.append(styleTrigger, stylePopover);
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
- const nextHidden = !alignPopover.hidden;
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
- alignAnchor.append(alignBtn, alignPopover);
712
- box.append(alignAnchor);
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(sheet, label);
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 = () => closeAllPopovers();
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
- if (!sel || !sel.text)
1032
+ const text = sel?.excerpt;
1033
+ if (!sel || !text?.trim())
939
1034
  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' });
1035
+ options.onQuoteSelection?.({ text, path: sel.path, page: sel.page });
950
1036
  });
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());
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.4.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": {
Binary file