@a3s-lab/office 0.1.0 → 0.2.2

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.
Files changed (69) hide show
  1. package/README.md +101 -24
  2. package/dist/0~2582.js +485 -0
  3. package/dist/0~4595.js +14 -7
  4. package/dist/0~4750.js +939 -0
  5. package/dist/{0~work-document-paragraph-formatting.js → 0~5861.js} +174 -3
  6. package/dist/0~7048.js +8 -6
  7. package/dist/0~9512.js +7 -4
  8. package/dist/0~document-editor.js +3147 -531
  9. package/dist/0~markdown-editor.js +26 -1
  10. package/dist/0~presentation-editor.js +68 -12
  11. package/dist/0~spreadsheet-editor.js +37 -17
  12. package/dist/0~work-document-extensions.js +588 -13
  13. package/dist/0~work-document-model-codec.js +1 -1
  14. package/dist/0~work-docx-bibliography.js +1 -1
  15. package/dist/0~work-docx-export.js +116 -55
  16. package/dist/0~work-docx-import.js +766 -107
  17. package/dist/0~work-office-diagnostics.js +2 -2
  18. package/dist/{4704.js → 2881.js} +162 -71
  19. package/dist/4104.js +2 -62
  20. package/dist/internal/features/work/editors/document-comment-window.d.ts +37 -0
  21. package/dist/internal/features/work/editors/document-navigation-panel.d.ts +3 -1
  22. package/dist/internal/features/work/editors/document-navigation-window.d.ts +64 -0
  23. package/dist/internal/features/work/editors/document-outline-navigation.d.ts +11 -0
  24. package/dist/internal/features/work/editors/document-page-navigation.d.ts +11 -1
  25. package/dist/internal/features/work/editors/document-page-thumbnail.d.ts +31 -0
  26. package/dist/internal/features/work/editors/document-picture-properties-dialog-model.d.ts +28 -0
  27. package/dist/internal/features/work/editors/document-picture-properties-dialog.d.ts +4 -0
  28. package/dist/internal/features/work/editors/document-search-navigation.d.ts +12 -0
  29. package/dist/internal/features/work/editors/document-table-margins-popover.d.ts +4 -0
  30. package/dist/internal/features/work/editors/document-table-properties-dialog-model.d.ts +51 -0
  31. package/dist/internal/features/work/editors/document-table-properties-dialog-sections.d.ts +19 -0
  32. package/dist/internal/features/work/editors/document-table-properties-dialog.d.ts +8 -0
  33. package/dist/internal/features/work/editors/office-task-pane.d.ts +1 -1
  34. package/dist/internal/features/work/editors/presentation-chart-panel.d.ts +2 -1
  35. package/dist/internal/features/work/editors/presentation-comments-panel.d.ts +2 -1
  36. package/dist/internal/features/work/editors/spreadsheet-workbook-panel.d.ts +2 -1
  37. package/dist/internal/features/work/editors/use-document-pagination.d.ts +6 -0
  38. package/dist/internal/features/work/editors/use-presentation-review-commands.d.ts +1 -1
  39. package/dist/internal/features/work/work-document-image-layout.d.ts +11 -0
  40. package/dist/internal/features/work/work-document-page-capture.d.ts +15 -0
  41. package/dist/internal/features/work/work-document-paragraph-formatting.d.ts +4 -0
  42. package/dist/internal/features/work/work-document-section.d.ts +3 -1
  43. package/dist/internal/features/work/work-document-table-borders.d.ts +20 -0
  44. package/dist/internal/features/work/work-document-table-cell-formatting.d.ts +8 -5
  45. package/dist/internal/features/work/work-document-table-geometry.d.ts +41 -0
  46. package/dist/internal/features/work/work-document-table-property-changes.d.ts +25 -0
  47. package/dist/internal/features/work/work-document-table-sizing.d.ts +15 -2
  48. package/dist/internal/features/work/work-document-word-line-metrics.d.ts +11 -0
  49. package/dist/internal/features/work/work-docx-document-layout.d.ts +2 -0
  50. package/dist/internal/features/work/work-docx-paragraph-alignment-import.d.ts +2 -1
  51. package/dist/internal/features/work/work-docx-paragraph-direction-import.d.ts +2 -1
  52. package/dist/internal/features/work/work-docx-paragraph-indent-import.d.ts +2 -1
  53. package/dist/internal/features/work/work-docx-paragraph-pagination-import.d.ts +2 -1
  54. package/dist/internal/features/work/work-docx-paragraph-spacing-import.d.ts +3 -2
  55. package/dist/internal/features/work/work-docx-paragraph-styles.d.ts +2 -2
  56. package/dist/internal/features/work/work-docx-run-formatting-import.d.ts +8 -1
  57. package/dist/internal/features/work/work-docx-tab-stop-import.d.ts +2 -1
  58. package/dist/internal/features/work/work-docx-table-cell-export.d.ts +1 -1
  59. package/dist/internal/features/work/work-docx-table-cell-import.d.ts +14 -4
  60. package/dist/internal/features/work/work-docx-table-sizing-export.d.ts +1 -1
  61. package/dist/internal/features/work/work-docx-table-sizing-import.d.ts +4 -3
  62. package/dist/internal/features/work/work-docx-table-styles.d.ts +22 -0
  63. package/dist/internal/features/work/work-types.d.ts +6 -0
  64. package/dist/office-kernel.wasm +0 -0
  65. package/dist/styles.css +1288 -233
  66. package/docs/{browser-editor-architecture.md → latest/en/browser-editor-architecture.md} +144 -35
  67. package/package.json +9 -5
  68. package/dist/0~6552.js +0 -787
  69. package/dist/0~8155.js +0 -299
@@ -2,11 +2,11 @@ import { __webpack_require__ } from "./0~rslib-runtime.js";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import extension_placeholder from "@tiptap/extension-placeholder";
4
4
  import { EditorContent, useEditor } from "@tiptap/react";
5
- import { Fragment as external_react_Fragment, forwardRef, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import { Fragment as external_react_Fragment, forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Extension } from "@tiptap/core";
7
7
  import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
8
8
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
9
- import { AArrowDown, AArrowUp, AlignCenter, AlignJustify, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowUp, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, BookMarked, BookOpen, Check, CheckCheck, CheckCircle2, ChevronDown, ChevronRight, Cloud, Columns3, Eraser, FileDiff, FilePlus2, FileText, Globe2, Grid2X2, Hash, Highlighter, Image, IndentDecrease, IndentIncrease, Italic, Link2, List, ListChecks, ListOrdered, MessageSquarePlus, MessageSquareReply, MessagesSquare, MoveVertical, Palette, PanelBottom, PanelBottomOpen, PanelLeftOpen, PanelTop, PanelTopOpen, Pilcrow, PilcrowLeft, PilcrowRight, Plus, Quote, Redo2, RefreshCw, Replace, ReplaceAll, RotateCcw, Rows3, Ruler, ScanText, Search, Settings2, Strikethrough, Subscript, Superscript, Table2, TableCellsMerge, TableCellsSplit, TextWrap, Trash2, Underline, Undo2, Unlink, X, XCircle, ZoomIn, ZoomOut } from "lucide-react";
9
+ import { AArrowDown, AArrowUp, AlignCenter, AlignJustify, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowUp, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, BookMarked, BookOpen, Check, CheckCheck, CheckCircle2, ChevronDown, ChevronRight, Cloud, Columns3, Eraser, FileDiff, FilePlus2, FileText, Globe2, Grid2X2, Hash, Highlighter, Image, IndentDecrease, IndentIncrease, Italic, Link2, List, ListChecks, ListOrdered, MessageSquarePlus, MessageSquareReply, MessagesSquare, MoveVertical, Palette, PanelBottom, PanelBottomOpen, PanelLeftOpen, PanelTop, PanelTopOpen, Pilcrow, PilcrowLeft, PilcrowRight, Plus, Quote, Redo2, RefreshCw, Replace, ReplaceAll, RotateCcw, Rows3, Ruler, Search, Settings2, Strikethrough, Subscript, Superscript, Table2, TableCellsMerge, TableCellsSplit, TableProperties, TextWrap, Trash2, Underline, Undo2, Unlink, X, XCircle, ZoomIn, ZoomOut } from "lucide-react";
10
10
  import extension_color from "@tiptap/extension-color";
11
11
  import extension_image from "@tiptap/extension-image";
12
12
  import { TableKit } from "@tiptap/extension-table";
@@ -16,18 +16,18 @@ import extension_underline from "@tiptap/extension-underline";
16
16
  import starter_kit from "@tiptap/starter-kit";
17
17
  import { BubbleMenu } from "@tiptap/react/menus";
18
18
  import { TableMap, isInTable, selectedRect } from "@tiptap/pm/tables";
19
- import { nextDocumentTabAlignment, normalizedTabPosition, documentParagraphTabStops, documentImageLayoutOptions, documentImageAlternativeText, normalizeDocumentTabStops } from "./0~8155.js";
20
- import { documentTableSizing, documentTableRowRepeats, documentTableRowOptions, documentTableCellFormat, activeDocumentTableStyle, DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, documentTableRowCantSplit, documentTableHorizontalAlignment, DOCUMENT_TABLE_STYLE_OPTIONS, canSetDocumentTableRowRepeatHeader } from "./0~6552.js";
21
- import { normalizeDocumentHtml, documentInitialSectionLayout, isValidDocumentCitationTag, sanitizeDocumentPageChromeHtml, resolveWorkDocumentEditorInput, createDocumentBibliography, syncDocumentContentFromHtml, clampDocumentMargin, documentMargins, millimetersToPixels as work_document_layout_millimetersToPixels, documentCitationStyleDetails, updateDocumentPageChromeVariant, normalizeDocumentColumns, documentPageChromeLegacyFields, resolveDocumentPageChrome, setCustomDocumentColumns, documentCitationStyle, normalizeDocumentPageChrome, createWorkDocumentModel, documentSectionLayoutFromNodeAttributes, updateDocumentColumnWidth } from "./4704.js";
19
+ import { nextDocumentTabAlignment, normalizedTabPosition, documentParagraphTabStops, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS, documentImageLayoutOptions, documentImageProperties, normalizeDocumentTabStops } from "./0~4750.js";
20
+ import { documentTableRowRepeats, documentTableRowOptions, documentTableCellFormat, activeDocumentTableStyle, DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, documentTableRowCantSplit, documentTableHorizontalAlignment, DOCUMENT_TABLE_STYLE_OPTIONS, canSetDocumentTableRowRepeatHeader } from "./0~2582.js";
21
+ import { normalizeDocumentHtml, documentInitialSectionLayout, isValidDocumentCitationTag, sanitizeDocumentPageChromeHtml, mountWorkLiveDocumentCapture, resolveWorkDocumentEditorInput, createDocumentBibliography, syncDocumentContentFromHtml, clampDocumentMargin, documentMargins, millimetersToPixels as work_document_layout_millimetersToPixels, documentCitationStyleDetails, updateDocumentPageChromeVariant, normalizeDocumentColumns, documentPageChromeLegacyFields, resolveDocumentPageChrome, setCustomDocumentColumns, documentCitationStyle, normalizeDocumentPageChrome, createWorkDocumentModel, documentSectionLayoutFromNodeAttributes, updateDocumentColumnWidth } from "./2881.js";
22
22
  import { useOfficeTaskPaneEscape, normalizeOfficeFontFamily, OfficeCheckbox, useOfficeDialog, useOfficeTaskPaneModal, CommittedOfficeNumberField, officeFontFamilyLabel, officeFontFamilies, CollectionState, createOfficeKernelClient, OfficeNumberField } from "./0~7048.js";
23
23
  import { office_text_field_OfficeTextArea, Popover, officeOverlayPortalRoot, button_Button, icon_button_IconButton, office_text_field_OfficeTextField, useDialogFocusScope, useOfficeEditorWheelZoom, office_text_field_OfficeFileInput } from "./0~9512.js";
24
24
  import { WorkOfficePreviewBar, isWorkspaceContextMenuKeyboardEvent, Tabs, WorkOfficeZoomControls, WorkOfficeStatusBar, Dialog, WorkOfficeRibbon, workspaceContextMenuPosition, OfficeSelect, WorkspaceContextMenu, WorkOfficeRibbonGroup, WorkOfficeRibbonButton } from "./0~4595.js";
25
25
  import { createOfficeId as createWorkId } from "./1134.js";
26
- import { canInsertDocumentComment, activeDocumentSection, documentCommentViews, retainAnchoredDocumentComments, DocumentSubscript, documentCitationCount, documentBulletListStyle, editorDocumentCaptionTargets, collectDocumentChanges, collectDocumentCommentAnchors, documentTabLeaderLabel, documentOrderedListState, MAX_DOCUMENT_NUMBERING_START, documentCommentDraftRange, DocumentSuperscript, documentSectionById, createWorkDocumentExtensions } from "./0~work-document-extensions.js";
26
+ import { canInsertDocumentComment, documentTableSizing, activeDocumentSection, documentCommentViews, retainAnchoredDocumentComments, DocumentSubscript, documentCitationCount, documentBulletListStyle, editorDocumentCaptionTargets, collectDocumentChanges, collectDocumentCommentAnchors, documentTabLeaderLabel, documentOrderedListState, MAX_DOCUMENT_NUMBERING_START, documentCommentDraftRange, DocumentSuperscript, documentSectionById, createWorkDocumentExtensions } from "./0~work-document-extensions.js";
27
27
  import { normalizeDocumentHref, DOCUMENT_LINK_VALIDATION_MESSAGE } from "./0~work-document-links.js";
28
28
  import { OfficeColorPicker, isOfficeShortcutBlocked } from "./0~3635.js";
29
29
  import { OfficeTableInsertPopover, readOfficeFileAsDataUrl } from "./0~9445.js";
30
- import { documentParagraphSpacing, normalizeDocumentParagraphIndent, documentParagraphDirection, documentParagraphPagination, documentParagraphIndent, canChangeDocumentIndent } from "./0~work-document-paragraph-formatting.js";
30
+ import { documentParagraphSpacing, normalizeDocumentParagraphIndent, documentParagraphDirection, documentParagraphPagination, documentParagraphIndent, canChangeDocumentIndent } from "./0~5861.js";
31
31
  import { createWorkDocumentSelectionAction, documentPlainTextAsHtml, mergeOfficeTiptapExtensions, documentCurrentPage, documentAgentMenuItems, clampDocumentZoom, documentPageCount, documentWordCount, documentCustomSelectionMenuItems, createWorkDocumentSelectionSnapshot } from "./0~5050.js";
32
32
  import { OFFICE_DOCUMENT_LAYOUT_HEBREW_FONT_ID, WorkEditorLoadingState, OFFICE_DOCUMENT_LAYOUT_FONT_ID, OFFICE_DOCUMENT_LAYOUT_ARABIC_FONT_ID, documentLayoutFontKey, OFFICE_DOCUMENT_LAYOUT_LATIN_FONT_ID } from "./432.js";
33
33
  import { useOfficeDraft } from "./0~use-office-draft.js";
@@ -934,7 +934,7 @@ function createDocumentTableRowFragmentPlan(cells, rowHeight, maximumFragmentedR
934
934
  ] : [])
935
935
  }
936
936
  ];
937
- if (!cells.length || !Number.isFinite(maximumFragmentedRowHeight) || height > maximumFragmentedRowHeight) return atomic();
937
+ if (!cells.length || !Number.isFinite(maximumFragmentedRowHeight) || maximumFragmentedRowHeight <= 0) return atomic();
938
938
  const candidates = normalizedDocumentTableBreakCoordinates(cells.flatMap((cell)=>cell.boundaries.slice(1, -1).map((boundary)=>boundary.y).filter((y)=>y > 0.5 && y < height - 0.5))).slice(0, MAX_DOCUMENT_TABLE_ROW_FRAGMENTS - 1);
939
939
  if (!candidates.length) return atomic();
940
940
  const coordinates = [
@@ -1530,6 +1530,266 @@ function reusableDocumentListLayoutBlocks(previous, listId, nodeTo, dirtyFrom) {
1530
1530
  }
1531
1531
  }));
1532
1532
  }
1533
+ const DOCUMENT_NAVIGATION_COLLECTION_WINDOW_THRESHOLD = 48;
1534
+ const DOCUMENT_NAVIGATION_COLLECTION_WINDOW_LIMIT = 32;
1535
+ const DOCUMENT_NAVIGATION_COLLECTION_WINDOW_OVERSCAN = 4;
1536
+ const DOCUMENT_NAVIGATION_FALLBACK_VIEWPORT_HEIGHT = 720;
1537
+ function useDocumentNavigationWindow({ estimatedItemHeight, itemGap, keys, listPaddingTop, onRovingKeyChange, pinnedKeys = [], rovingKey }) {
1538
+ const viewportRef = useRef(null);
1539
+ const itemRefs = useRef(new Map());
1540
+ const keysRef = useRef(keys);
1541
+ const pendingFocusKeyRef = useRef(null);
1542
+ const rovingIndex = rovingKey ? keys.indexOf(rovingKey) : -1;
1543
+ const [anchorIndex, setAnchorIndex] = useState(Math.max(0, rovingIndex));
1544
+ const [viewportHeight, setViewportHeight] = useState(DOCUMENT_NAVIGATION_FALLBACK_VIEWPORT_HEIGHT);
1545
+ keysRef.current = keys;
1546
+ useEffect(()=>{
1547
+ if (rovingIndex < 0) return;
1548
+ setAnchorIndex(rovingIndex);
1549
+ }, [
1550
+ rovingIndex,
1551
+ rovingKey
1552
+ ]);
1553
+ useLayoutEffect(()=>{
1554
+ const viewport = viewportRef.current;
1555
+ if (!viewport) return;
1556
+ const update = ()=>{
1557
+ const nextHeight = viewport.clientHeight || DOCUMENT_NAVIGATION_FALLBACK_VIEWPORT_HEIGHT;
1558
+ setViewportHeight((current)=>current === nextHeight ? current : nextHeight);
1559
+ };
1560
+ update();
1561
+ if ("u" < typeof ResizeObserver) return;
1562
+ const observer = new ResizeObserver(update);
1563
+ observer.observe(viewport);
1564
+ return ()=>observer.disconnect();
1565
+ }, [
1566
+ keys.length
1567
+ ]);
1568
+ const itemHeights = useMemo(()=>Array.from({
1569
+ length: keys.length
1570
+ }, ()=>Math.max(1, estimatedItemHeight)), [
1571
+ estimatedItemHeight,
1572
+ keys.length
1573
+ ]);
1574
+ const itemStarts = useMemo(()=>documentNavigationItemStarts(itemHeights, itemGap), [
1575
+ itemGap,
1576
+ itemHeights
1577
+ ]);
1578
+ const averageItemHeight = itemHeights.length ? (itemStarts[itemStarts.length - 1] ?? 1) / itemHeights.length : 1;
1579
+ const range = useMemo(()=>calculateDocumentNavigationWindowRange({
1580
+ anchorIndex,
1581
+ averageItemHeight,
1582
+ itemCount: keys.length,
1583
+ limit: DOCUMENT_NAVIGATION_COLLECTION_WINDOW_LIMIT,
1584
+ overscan: DOCUMENT_NAVIGATION_COLLECTION_WINDOW_OVERSCAN,
1585
+ threshold: DOCUMENT_NAVIGATION_COLLECTION_WINDOW_THRESHOLD,
1586
+ viewportHeight
1587
+ }), [
1588
+ anchorIndex,
1589
+ averageItemHeight,
1590
+ keys.length,
1591
+ viewportHeight
1592
+ ]);
1593
+ const pinnedIndices = useMemo(()=>{
1594
+ const indices = new Set();
1595
+ if (rovingIndex >= 0) indices.add(rovingIndex);
1596
+ for (const key of pinnedKeys){
1597
+ if (!key) continue;
1598
+ const index = keys.indexOf(key);
1599
+ if (index >= 0) indices.add(index);
1600
+ }
1601
+ return [
1602
+ ...indices
1603
+ ];
1604
+ }, [
1605
+ keys,
1606
+ pinnedKeys,
1607
+ rovingIndex
1608
+ ]);
1609
+ const mountedIndices = useMemo(()=>documentNavigationWindowIndices({
1610
+ itemCount: keys.length,
1611
+ pinnedIndices,
1612
+ range
1613
+ }), [
1614
+ keys.length,
1615
+ pinnedIndices,
1616
+ range
1617
+ ]);
1618
+ const entries = useMemo(()=>documentNavigationWindowEntries(mountedIndices, itemStarts, keys.length, itemGap), [
1619
+ itemGap,
1620
+ itemStarts,
1621
+ keys.length,
1622
+ mountedIndices
1623
+ ]);
1624
+ const mountedIndicesKey = mountedIndices.join(',');
1625
+ const registerItem = useCallback((key, element)=>{
1626
+ if (element) itemRefs.current.set(key, element);
1627
+ else itemRefs.current.delete(key);
1628
+ }, []);
1629
+ const focusAt = useCallback((requestedIndex)=>{
1630
+ const currentKeys = keysRef.current;
1631
+ if (!currentKeys.length) return;
1632
+ const index = Math.min(currentKeys.length - 1, Math.max(0, Math.floor(requestedIndex)));
1633
+ const key = currentKeys[index];
1634
+ if (!key) return;
1635
+ setAnchorIndex(index);
1636
+ onRovingKeyChange(key);
1637
+ const mounted = itemRefs.current.get(key);
1638
+ if (mounted) {
1639
+ pendingFocusKeyRef.current = null;
1640
+ focusDocumentNavigationItem(mounted);
1641
+ return;
1642
+ }
1643
+ pendingFocusKeyRef.current = key;
1644
+ }, [
1645
+ onRovingKeyChange
1646
+ ]);
1647
+ const onItemFocus = useCallback((index)=>{
1648
+ const key = keysRef.current[index];
1649
+ if (!key) return;
1650
+ pendingFocusKeyRef.current = null;
1651
+ setAnchorIndex(index);
1652
+ onRovingKeyChange(key);
1653
+ }, [
1654
+ onRovingKeyChange
1655
+ ]);
1656
+ useLayoutEffect(()=>{
1657
+ const pendingKey = pendingFocusKeyRef.current;
1658
+ if (!pendingKey) return;
1659
+ if (!keys.includes(pendingKey)) {
1660
+ pendingFocusKeyRef.current = null;
1661
+ return;
1662
+ }
1663
+ const element = itemRefs.current.get(pendingKey);
1664
+ if (!element) return;
1665
+ pendingFocusKeyRef.current = null;
1666
+ focusDocumentNavigationItem(element);
1667
+ }, [
1668
+ keys,
1669
+ mountedIndicesKey,
1670
+ rovingKey
1671
+ ]);
1672
+ const onScroll = useCallback((event)=>{
1673
+ if (!range.windowed) return;
1674
+ const nextAnchor = documentNavigationIndexAtOffset(itemStarts, Math.max(0, event.currentTarget.scrollTop - listPaddingTop), keys.length);
1675
+ setAnchorIndex((current)=>current === nextAnchor ? current : nextAnchor);
1676
+ }, [
1677
+ itemStarts,
1678
+ keys.length,
1679
+ listPaddingTop,
1680
+ range.windowed
1681
+ ]);
1682
+ return {
1683
+ entries,
1684
+ focusAt,
1685
+ mountedCount: mountedIndices.length,
1686
+ onItemFocus,
1687
+ onScroll,
1688
+ range,
1689
+ registerItem,
1690
+ rovingIndex,
1691
+ viewportRef
1692
+ };
1693
+ }
1694
+ function DocumentNavigationWindowSpacer({ entry }) {
1695
+ return /*#__PURE__*/ jsx("li", {
1696
+ "aria-hidden": "true",
1697
+ className: "work-document-navigation-window-spacer",
1698
+ "data-document-navigation-spacer": entry.position,
1699
+ "data-document-navigation-spacer-end": entry.end,
1700
+ "data-document-navigation-spacer-start": entry.start + 1,
1701
+ role: "presentation",
1702
+ style: {
1703
+ height: `${entry.height}px`
1704
+ }
1705
+ });
1706
+ }
1707
+ function calculateDocumentNavigationWindowRange({ anchorIndex, averageItemHeight, itemCount, limit, overscan, threshold, viewportHeight }) {
1708
+ const count = Math.max(0, Math.floor(itemCount));
1709
+ if (count <= Math.max(0, Math.floor(threshold))) return {
1710
+ end: count,
1711
+ start: 0,
1712
+ windowed: false
1713
+ };
1714
+ const visibleCount = Math.max(1, Math.ceil(Math.max(1, viewportHeight) / Math.max(1, averageItemHeight)));
1715
+ const boundedOverscan = Math.max(0, Math.floor(overscan));
1716
+ const windowCount = Math.min(count, Math.max(1, Math.floor(limit)), visibleCount + 2 * boundedOverscan);
1717
+ const boundedAnchor = Math.min(count - 1, Math.max(0, Math.floor(anchorIndex)));
1718
+ const start = Math.min(count - windowCount, Math.max(0, boundedAnchor - boundedOverscan));
1719
+ return {
1720
+ end: start + windowCount,
1721
+ start,
1722
+ windowed: true
1723
+ };
1724
+ }
1725
+ function documentNavigationItemStarts(itemHeights, itemGap) {
1726
+ const starts = [
1727
+ 0
1728
+ ];
1729
+ const gap = Math.max(0, itemGap);
1730
+ for (const height of itemHeights)starts.push((starts[starts.length - 1] ?? 0) + Math.max(1, height) + gap);
1731
+ return starts;
1732
+ }
1733
+ function documentNavigationWindowIndices({ itemCount, pinnedIndices, range }) {
1734
+ const count = Math.max(0, Math.floor(itemCount));
1735
+ if (!range.windowed) return Array.from({
1736
+ length: count
1737
+ }, (_, index)=>index);
1738
+ const mounted = new Set();
1739
+ for(let index = range.start; index < range.end; index += 1)if (index >= 0 && index < count) mounted.add(index);
1740
+ for (const index of pinnedIndices)if (index >= 0 && index < count) mounted.add(index);
1741
+ return [
1742
+ ...mounted
1743
+ ].sort((left, right)=>left - right);
1744
+ }
1745
+ function documentNavigationWindowEntries(mountedIndices, itemStarts, itemCount, itemGap) {
1746
+ const entries = [];
1747
+ let cursor = 0;
1748
+ for (const index of mountedIndices){
1749
+ if (index > cursor) entries.push(documentNavigationWindowSpacerEntry(cursor, index, itemStarts, itemCount, itemGap));
1750
+ entries.push({
1751
+ index,
1752
+ kind: 'item'
1753
+ });
1754
+ cursor = index + 1;
1755
+ }
1756
+ if (cursor < itemCount) entries.push(documentNavigationWindowSpacerEntry(cursor, itemCount, itemStarts, itemCount, itemGap));
1757
+ return entries;
1758
+ }
1759
+ function documentNavigationIndexAtOffset(itemStarts, offset, itemCount) {
1760
+ if (itemCount <= 1) return 0;
1761
+ let low = 0;
1762
+ let high = itemCount - 1;
1763
+ while(low <= high){
1764
+ const middle = Math.floor((low + high) / 2);
1765
+ const start = itemStarts[middle] ?? 0;
1766
+ const next = itemStarts[middle + 1] ?? 1 / 0;
1767
+ if (offset < start) high = middle - 1;
1768
+ else {
1769
+ if (!(offset >= next)) return middle;
1770
+ low = middle + 1;
1771
+ }
1772
+ }
1773
+ return Math.min(itemCount - 1, Math.max(0, low));
1774
+ }
1775
+ function documentNavigationWindowSpacerEntry(start, end, itemStarts, itemCount, itemGap) {
1776
+ return {
1777
+ end,
1778
+ height: Math.max(0, (itemStarts[end] ?? 0) - (itemStarts[start] ?? 0) - Math.max(0, itemGap)),
1779
+ kind: 'spacer',
1780
+ position: 0 === start ? 'before' : end === itemCount ? 'after' : 'between',
1781
+ start
1782
+ };
1783
+ }
1784
+ function focusDocumentNavigationItem(element) {
1785
+ element.focus({
1786
+ preventScroll: true
1787
+ });
1788
+ element.scrollIntoView?.({
1789
+ block: 'nearest',
1790
+ inline: 'nearest'
1791
+ });
1792
+ }
1533
1793
  function DocumentTaskPane({ ariaLabel, title, description, closeLabel, className, children, onClose, onKeyDown }) {
1534
1794
  const titleId = useId();
1535
1795
  const descriptionId = useId();
@@ -1582,37 +1842,69 @@ function DocumentTaskPane({ ariaLabel, title, description, closeLabel, className
1582
1842
  ]
1583
1843
  });
1584
1844
  }
1845
+ const DOCUMENT_CHANGE_ITEM_HEIGHT = 84;
1846
+ const DOCUMENT_CHANGE_ITEM_GAP = 7;
1847
+ const DOCUMENT_CHANGE_LIST_PADDING_TOP = 10;
1585
1848
  function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesChange, onClose }) {
1586
1849
  const officeDialog = useOfficeDialog();
1587
1850
  const decisionButtonRefs = useRef(new Map());
1588
1851
  const pendingFocusRef = useRef(null);
1852
+ const changeKeys = useMemo(()=>changes.map(documentChangeWindowKey), [
1853
+ changes
1854
+ ]);
1855
+ const [rovingChangeKey, setRovingChangeKey] = useState(()=>changeKeys[0] ?? null);
1856
+ const effectiveRovingChangeKey = rovingChangeKey && changeKeys.includes(rovingChangeKey) ? rovingChangeKey : changeKeys[0] ?? null;
1857
+ const changeWindow = useDocumentNavigationWindow({
1858
+ estimatedItemHeight: DOCUMENT_CHANGE_ITEM_HEIGHT,
1859
+ itemGap: DOCUMENT_CHANGE_ITEM_GAP,
1860
+ keys: changeKeys,
1861
+ listPaddingTop: DOCUMENT_CHANGE_LIST_PADDING_TOP,
1862
+ onRovingKeyChange: setRovingChangeKey,
1863
+ rovingKey: effectiveRovingChangeKey
1864
+ });
1589
1865
  useLayoutEffect(()=>{
1590
1866
  const pending = pendingFocusRef.current;
1591
1867
  if (!pending) return;
1592
- pendingFocusRef.current = null;
1593
- const nextButton = pending.changeId ? decisionButtonRefs.current.get(documentChangeDecisionKey(pending.changeId, pending.decision)) : void 0;
1868
+ const nextButton = pending.changeKey ? decisionButtonRefs.current.get(documentChangeDecisionKey(pending.changeKey, pending.decision)) : void 0;
1594
1869
  if (nextButton) {
1870
+ pendingFocusRef.current = null;
1595
1871
  nextButton.focus({
1596
1872
  preventScroll: true
1597
1873
  });
1598
1874
  nextButton.scrollIntoView({
1599
1875
  block: 'nearest'
1600
1876
  });
1601
- } else if (!editor.isDestroyed) editor.view.dom.focus({
1602
- preventScroll: true
1603
- });
1877
+ } else if (!pending.changeKey && !editor.isDestroyed) {
1878
+ pendingFocusRef.current = null;
1879
+ editor.view.dom.focus({
1880
+ preventScroll: true
1881
+ });
1882
+ }
1604
1883
  }, [
1884
+ changeWindow.mountedCount,
1605
1885
  changes,
1606
1886
  editor
1607
1887
  ]);
1608
1888
  const decideChange = (change, index, decision)=>{
1609
1889
  const nextChange = changes[index + 1] ?? changes[index - 1] ?? null;
1890
+ const currentChangeKey = documentChangeWindowKey(change);
1891
+ const nextChangeKey = nextChange ? documentChangeWindowKey(nextChange) : null;
1610
1892
  pendingFocusRef.current = {
1611
- changeId: nextChange?.id ?? null,
1893
+ changeKey: nextChangeKey,
1612
1894
  decision
1613
1895
  };
1896
+ setRovingChangeKey(nextChangeKey);
1614
1897
  const handled = 'accept' === decision ? editor.commands.acceptDocumentChange(change.id) : editor.commands.rejectDocumentChange(change.id);
1615
- if (!handled) pendingFocusRef.current = null;
1898
+ if (!handled) {
1899
+ pendingFocusRef.current = null;
1900
+ setRovingChangeKey(currentChangeKey);
1901
+ }
1902
+ };
1903
+ const handleSummaryKeyDown = (event, index)=>{
1904
+ const nextIndex = documentChangeKeyboardDestination(event.key, index, changes.length);
1905
+ if (null === nextIndex) return;
1906
+ event.preventDefault();
1907
+ changeWindow.focusAt(nextIndex);
1616
1908
  };
1617
1909
  const acceptAll = async ()=>{
1618
1910
  const confirmed = await officeDialog.confirm({
@@ -1675,87 +1967,111 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
1675
1967
  })
1676
1968
  ]
1677
1969
  }),
1678
- /*#__PURE__*/ jsxs("div", {
1970
+ changes.length > 0 ? /*#__PURE__*/ jsx("ol", {
1971
+ ref: changeWindow.viewportRef,
1679
1972
  className: "work-document-change-list work-document-task-pane-body",
1680
- children: [
1681
- changes.map((change, index)=>/*#__PURE__*/ jsxs("article", {
1682
- className: change.kind,
1683
- children: [
1684
- /*#__PURE__*/ jsxs("button", {
1685
- type: "button",
1686
- className: "work-document-change-summary",
1687
- "aria-label": `定位修订 ${index + 1}`,
1688
- onClick: ()=>editor.chain().focus().setTextSelection({
1689
- from: Math.min(change.from, editor.state.doc.content.size),
1690
- to: Math.min(change.to, editor.state.doc.content.size)
1691
- }).run(),
1692
- children: [
1693
- /*#__PURE__*/ jsx("span", {
1694
- children: 'insertion' === change.kind ? '插入' : '删除'
1695
- }),
1696
- /*#__PURE__*/ jsx("strong", {
1697
- children: change.text.trim() || '(空白字符)'
1698
- }),
1699
- /*#__PURE__*/ jsxs("small", {
1700
- children: [
1701
- change.author,
1702
- change.date ? ` · ${formatChangeDate(change.date)}` : ''
1703
- ]
1704
- })
1705
- ]
1706
- }),
1707
- /*#__PURE__*/ jsxs("div", {
1708
- children: [
1709
- /*#__PURE__*/ jsxs(button_Button, {
1710
- ref: (element)=>{
1711
- const key = documentChangeDecisionKey(change.id, 'accept');
1712
- if (element) decisionButtonRefs.current.set(key, element);
1713
- else decisionButtonRefs.current.delete(key);
1714
- },
1715
- tone: "quiet",
1716
- "aria-label": `接受修订 ${index + 1}`,
1717
- onClick: ()=>decideChange(change, index, 'accept'),
1718
- children: [
1719
- /*#__PURE__*/ jsx(Check, {
1720
- size: 13
1721
- }),
1722
- "接受"
1723
- ]
1724
- }),
1725
- /*#__PURE__*/ jsxs(button_Button, {
1726
- ref: (element)=>{
1727
- const key = documentChangeDecisionKey(change.id, 'reject');
1728
- if (element) decisionButtonRefs.current.set(key, element);
1729
- else decisionButtonRefs.current.delete(key);
1730
- },
1731
- tone: "quiet",
1732
- "aria-label": `拒绝修订 ${index + 1}`,
1733
- onClick: ()=>decideChange(change, index, 'reject'),
1734
- children: [
1735
- /*#__PURE__*/ jsx(XCircle, {
1736
- size: 13
1737
- }),
1738
- "拒绝"
1739
- ]
1740
- })
1741
- ]
1742
- })
1743
- ]
1744
- }, `${change.kind}-${change.id}`)),
1745
- !changes.length && /*#__PURE__*/ jsx(CollectionState, {
1746
- className: "work-document-changes-empty",
1747
- icon: /*#__PURE__*/ jsx(FileDiff, {}),
1748
- actions: /*#__PURE__*/ jsx(button_Button, {
1749
- size: "compact",
1750
- tone: trackChanges ? 'quiet' : 'primary',
1751
- onClick: ()=>onTrackChangesChange(!trackChanges),
1752
- children: trackChanges ? '停止记录' : '开启修订'
1753
- }),
1754
- tone: trackChanges ? 'info' : 'neutral',
1755
- role: "status",
1756
- children: trackChanges ? '正在记录新的改动。' : '当前没有记录新的改动。'
1757
- })
1758
- ]
1973
+ "aria-label": "待处理修订",
1974
+ "data-document-change-count": changes.length,
1975
+ "data-document-change-mounted-count": changeWindow.mountedCount,
1976
+ "data-document-change-window-end": changeWindow.range.end,
1977
+ "data-document-change-window-limit": 32,
1978
+ "data-document-change-window-start": changeWindow.range.start,
1979
+ "data-document-change-windowed": changeWindow.range.windowed ? 'true' : 'false',
1980
+ onScroll: changeWindow.onScroll,
1981
+ children: changeWindow.entries.map((entry)=>{
1982
+ if ('spacer' === entry.kind) return /*#__PURE__*/ jsx(DocumentChangeWindowSpacer, {
1983
+ entry: entry
1984
+ }, `spacer-${entry.start}-${entry.end}`);
1985
+ const change = changes[entry.index];
1986
+ if (!change) return null;
1987
+ const changeKey = documentChangeWindowKey(change);
1988
+ return /*#__PURE__*/ jsxs("li", {
1989
+ "aria-posinset": entry.index + 1,
1990
+ "aria-setsize": changes.length,
1991
+ className: `work-document-change-item ${change.kind}`,
1992
+ "data-document-change-item": entry.index + 1,
1993
+ onFocusCapture: ()=>changeWindow.onItemFocus(entry.index),
1994
+ children: [
1995
+ /*#__PURE__*/ jsxs("button", {
1996
+ ref: (element)=>changeWindow.registerItem(changeKey, element),
1997
+ type: "button",
1998
+ className: "work-document-change-summary",
1999
+ tabIndex: changeWindow.rovingIndex === entry.index ? 0 : -1,
2000
+ "aria-label": `定位修订 ${entry.index + 1}`,
2001
+ onKeyDown: (event)=>handleSummaryKeyDown(event, entry.index),
2002
+ onClick: ()=>editor.chain().focus().setTextSelection({
2003
+ from: Math.min(change.from, editor.state.doc.content.size),
2004
+ to: Math.min(change.to, editor.state.doc.content.size)
2005
+ }).run(),
2006
+ children: [
2007
+ /*#__PURE__*/ jsx("span", {
2008
+ children: 'insertion' === change.kind ? '插入' : '删除'
2009
+ }),
2010
+ /*#__PURE__*/ jsx("strong", {
2011
+ children: change.text.trim() || '(空白字符)'
2012
+ }),
2013
+ /*#__PURE__*/ jsxs("small", {
2014
+ children: [
2015
+ change.author,
2016
+ change.date ? ` · ${formatChangeDate(change.date)}` : ''
2017
+ ]
2018
+ })
2019
+ ]
2020
+ }),
2021
+ /*#__PURE__*/ jsxs("div", {
2022
+ children: [
2023
+ /*#__PURE__*/ jsxs(button_Button, {
2024
+ ref: (element)=>{
2025
+ const key = documentChangeDecisionKey(changeKey, 'accept');
2026
+ if (element) decisionButtonRefs.current.set(key, element);
2027
+ else decisionButtonRefs.current.delete(key);
2028
+ },
2029
+ tone: "quiet",
2030
+ "aria-label": `接受修订 ${entry.index + 1}`,
2031
+ onClick: ()=>decideChange(change, entry.index, 'accept'),
2032
+ children: [
2033
+ /*#__PURE__*/ jsx(Check, {
2034
+ size: 13
2035
+ }),
2036
+ "接受"
2037
+ ]
2038
+ }),
2039
+ /*#__PURE__*/ jsxs(button_Button, {
2040
+ ref: (element)=>{
2041
+ const key = documentChangeDecisionKey(changeKey, 'reject');
2042
+ if (element) decisionButtonRefs.current.set(key, element);
2043
+ else decisionButtonRefs.current.delete(key);
2044
+ },
2045
+ tone: "quiet",
2046
+ "aria-label": `拒绝修订 ${entry.index + 1}`,
2047
+ onClick: ()=>decideChange(change, entry.index, 'reject'),
2048
+ children: [
2049
+ /*#__PURE__*/ jsx(XCircle, {
2050
+ size: 13
2051
+ }),
2052
+ "拒绝"
2053
+ ]
2054
+ })
2055
+ ]
2056
+ })
2057
+ ]
2058
+ }, changeKey);
2059
+ })
2060
+ }) : /*#__PURE__*/ jsx("div", {
2061
+ className: "work-document-change-list work-document-task-pane-body",
2062
+ children: /*#__PURE__*/ jsx(CollectionState, {
2063
+ className: "work-document-changes-empty",
2064
+ icon: /*#__PURE__*/ jsx(FileDiff, {}),
2065
+ actions: /*#__PURE__*/ jsx(button_Button, {
2066
+ size: "compact",
2067
+ tone: trackChanges ? 'quiet' : 'primary',
2068
+ onClick: ()=>onTrackChangesChange(!trackChanges),
2069
+ children: trackChanges ? '停止记录' : '开启修订'
2070
+ }),
2071
+ tone: trackChanges ? 'info' : 'neutral',
2072
+ role: "status",
2073
+ children: trackChanges ? '正在记录新的改动。' : '当前没有记录新的改动。'
2074
+ })
1759
2075
  })
1760
2076
  ]
1761
2077
  }),
@@ -1763,8 +2079,34 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
1763
2079
  ]
1764
2080
  });
1765
2081
  }
1766
- function documentChangeDecisionKey(changeId, decision) {
1767
- return `${changeId}:${decision}`;
2082
+ function documentChangeDecisionKey(changeKey, decision) {
2083
+ return `${changeKey}:${decision}`;
2084
+ }
2085
+ function documentChangeWindowKey(change) {
2086
+ return `${change.kind}:${change.id}`;
2087
+ }
2088
+ function documentChangeKeyboardDestination(key, index, itemCount) {
2089
+ if (!itemCount) return null;
2090
+ if ('ArrowDown' === key) return Math.min(itemCount - 1, index + 1);
2091
+ if ('ArrowUp' === key) return Math.max(0, index - 1);
2092
+ if ('PageDown' === key) return Math.min(itemCount - 1, index + 8);
2093
+ if ('PageUp' === key) return Math.max(0, index - 8);
2094
+ if ('Home' === key) return 0;
2095
+ if ('End' === key) return itemCount - 1;
2096
+ return null;
2097
+ }
2098
+ function DocumentChangeWindowSpacer({ entry }) {
2099
+ return /*#__PURE__*/ jsx("li", {
2100
+ "aria-hidden": "true",
2101
+ className: "work-document-change-window-spacer",
2102
+ "data-document-change-spacer": entry.position,
2103
+ "data-document-change-spacer-end": entry.end,
2104
+ "data-document-change-spacer-start": entry.start + 1,
2105
+ role: "presentation",
2106
+ style: {
2107
+ height: `${entry.height}px`
2108
+ }
2109
+ });
1768
2110
  }
1769
2111
  function formatChangeDate(value) {
1770
2112
  const time = Date.parse(value);
@@ -2614,6 +2956,191 @@ const document_comment_composer_DocumentCommentComposer = /*#__PURE__*/ forwardR
2614
2956
  ]
2615
2957
  });
2616
2958
  });
2959
+ const DOCUMENT_COMMENT_WINDOW_THRESHOLD = 48;
2960
+ const DOCUMENT_COMMENT_WINDOW_LIMIT = 32;
2961
+ const DOCUMENT_COMMENT_WINDOW_OVERSCAN = 4;
2962
+ function useDocumentCommentWindow({ keys, onRovingKeyChange, pinnedKeys = [], rovingKey }) {
2963
+ const itemRefs = useRef(new Map());
2964
+ const keysRef = useRef(keys);
2965
+ const pendingFocusKeyRef = useRef(null);
2966
+ const rovingIndex = rovingKey ? keys.indexOf(rovingKey) : -1;
2967
+ const [anchorIndex, setAnchorIndex] = useState(Math.max(0, rovingIndex));
2968
+ keysRef.current = keys;
2969
+ useEffect(()=>{
2970
+ if (rovingIndex < 0) return;
2971
+ setAnchorIndex(rovingIndex);
2972
+ }, [
2973
+ rovingIndex,
2974
+ rovingKey
2975
+ ]);
2976
+ const range = useMemo(()=>calculateDocumentCommentWindowRange({
2977
+ anchorIndex,
2978
+ itemCount: keys.length
2979
+ }), [
2980
+ anchorIndex,
2981
+ keys.length
2982
+ ]);
2983
+ const pinnedIndices = useMemo(()=>{
2984
+ const indices = new Set();
2985
+ if (rovingIndex >= 0) indices.add(rovingIndex);
2986
+ for (const key of pinnedKeys){
2987
+ if (!key) continue;
2988
+ const index = keys.indexOf(key);
2989
+ if (index >= 0) indices.add(index);
2990
+ }
2991
+ return [
2992
+ ...indices
2993
+ ];
2994
+ }, [
2995
+ keys,
2996
+ pinnedKeys,
2997
+ rovingIndex
2998
+ ]);
2999
+ const mountedIndices = useMemo(()=>documentCommentWindowIndices({
3000
+ itemCount: keys.length,
3001
+ pinnedIndices,
3002
+ range
3003
+ }), [
3004
+ keys.length,
3005
+ pinnedIndices,
3006
+ range
3007
+ ]);
3008
+ const mountedIndicesKey = mountedIndices.join(',');
3009
+ const registerItem = useCallback((key, element)=>{
3010
+ if (element) itemRefs.current.set(key, element);
3011
+ else itemRefs.current.delete(key);
3012
+ }, []);
3013
+ const focusAt = useCallback((requestedIndex)=>{
3014
+ const currentKeys = keysRef.current;
3015
+ if (!currentKeys.length) return;
3016
+ const index = Math.min(currentKeys.length - 1, Math.max(0, Math.floor(requestedIndex)));
3017
+ const key = currentKeys[index];
3018
+ if (!key) return;
3019
+ setAnchorIndex(index);
3020
+ onRovingKeyChange(key);
3021
+ const mounted = itemRefs.current.get(key);
3022
+ if (mounted) {
3023
+ pendingFocusKeyRef.current = null;
3024
+ focusDocumentCommentItem(mounted);
3025
+ return;
3026
+ }
3027
+ pendingFocusKeyRef.current = key;
3028
+ }, [
3029
+ onRovingKeyChange
3030
+ ]);
3031
+ const onItemFocus = useCallback((index)=>{
3032
+ const key = keysRef.current[index];
3033
+ if (!key) return;
3034
+ pendingFocusKeyRef.current = null;
3035
+ setAnchorIndex(index);
3036
+ onRovingKeyChange(key);
3037
+ }, [
3038
+ onRovingKeyChange
3039
+ ]);
3040
+ const onViewportAnchorChange = useCallback((index)=>{
3041
+ setAnchorIndex((current)=>current === index ? current : index);
3042
+ }, []);
3043
+ useLayoutEffect(()=>{
3044
+ const pendingKey = pendingFocusKeyRef.current;
3045
+ if (!pendingKey) return;
3046
+ if (!keys.includes(pendingKey)) {
3047
+ pendingFocusKeyRef.current = null;
3048
+ return;
3049
+ }
3050
+ const element = itemRefs.current.get(pendingKey);
3051
+ if (!element) return;
3052
+ pendingFocusKeyRef.current = null;
3053
+ focusDocumentCommentItem(element);
3054
+ }, [
3055
+ keys,
3056
+ mountedIndicesKey,
3057
+ rovingKey
3058
+ ]);
3059
+ return {
3060
+ focusAt,
3061
+ mountedIndices,
3062
+ onItemFocus,
3063
+ onViewportAnchorChange,
3064
+ range,
3065
+ registerItem,
3066
+ rovingIndex
3067
+ };
3068
+ }
3069
+ function calculateDocumentCommentWindowRange({ anchorIndex, itemCount, limit = DOCUMENT_COMMENT_WINDOW_LIMIT, overscan = DOCUMENT_COMMENT_WINDOW_OVERSCAN, threshold = DOCUMENT_COMMENT_WINDOW_THRESHOLD }) {
3070
+ const count = Math.max(0, Math.floor(itemCount));
3071
+ if (count <= Math.max(0, Math.floor(threshold))) return {
3072
+ end: count,
3073
+ start: 0,
3074
+ windowed: false
3075
+ };
3076
+ const windowCount = Math.min(count, Math.max(1, Math.floor(limit)));
3077
+ const boundedAnchor = Math.min(count - 1, Math.max(0, Math.floor(anchorIndex)));
3078
+ const start = Math.min(count - windowCount, Math.max(0, boundedAnchor - Math.max(0, Math.floor(overscan))));
3079
+ return {
3080
+ end: start + windowCount,
3081
+ start,
3082
+ windowed: true
3083
+ };
3084
+ }
3085
+ function documentCommentWindowIndices({ itemCount, pinnedIndices, range }) {
3086
+ const count = Math.max(0, Math.floor(itemCount));
3087
+ if (!range.windowed) return Array.from({
3088
+ length: count
3089
+ }, (_, index)=>index);
3090
+ const mounted = new Set();
3091
+ for(let index = range.start; index < range.end; index += 1)if (index >= 0 && index < count) mounted.add(index);
3092
+ for (const index of pinnedIndices)if (index >= 0 && index < count) mounted.add(index);
3093
+ return [
3094
+ ...mounted
3095
+ ].sort((left, right)=>left - right);
3096
+ }
3097
+ function documentCommentKeyboardDestination(key, index, itemCount) {
3098
+ if (!itemCount) return null;
3099
+ if ('ArrowDown' === key) return Math.min(itemCount - 1, index + 1);
3100
+ if ('ArrowUp' === key) return Math.max(0, index - 1);
3101
+ if ('PageDown' === key) return Math.min(itemCount - 1, index + 8);
3102
+ if ('PageUp' === key) return Math.max(0, index - 8);
3103
+ if ('Home' === key) return 0;
3104
+ if ('End' === key) return itemCount - 1;
3105
+ return null;
3106
+ }
3107
+ function focusDocumentCommentItem(element) {
3108
+ element.focus({
3109
+ preventScroll: true
3110
+ });
3111
+ scrollDocumentCommentItemIntoView(element);
3112
+ }
3113
+ function scrollDocumentCommentItemIntoView(element) {
3114
+ const panel = element.closest('.work-document-comments-panel');
3115
+ const view = element.ownerDocument.defaultView;
3116
+ const panelOverflow = panel ? view?.getComputedStyle(panel).overflowY : void 0;
3117
+ const panelOwnsScroll = Boolean(panel) && /^(auto|scroll)$/.test(panelOverflow ?? '') && (panel?.scrollHeight ?? 0) > (panel?.clientHeight ?? 0) + 1;
3118
+ const scrollContainer = panelOwnsScroll ? panel : panel?.closest('.work-document-scroll');
3119
+ if (!scrollContainer) return void element.scrollIntoView?.({
3120
+ block: 'nearest'
3121
+ });
3122
+ const scrollTarget = element.closest('.work-document-comment-card') ?? element;
3123
+ const elementRect = scrollTarget.getBoundingClientRect();
3124
+ const containerRect = scrollContainer.getBoundingClientRect();
3125
+ const header = panel?.querySelector(':scope > header');
3126
+ const headerHeight = header?.getBoundingClientRect().height ?? 0;
3127
+ const visibleTop = containerRect.top + headerHeight + 8;
3128
+ const visibleBottom = containerRect.bottom - 8;
3129
+ let nextScrollTop = null;
3130
+ if (elementRect.top < visibleTop) nextScrollTop = scrollContainer.scrollTop + elementRect.top - visibleTop;
3131
+ else if (elementRect.bottom > visibleBottom) nextScrollTop = scrollContainer.scrollTop + elementRect.bottom - visibleBottom;
3132
+ if (null === nextScrollTop) return;
3133
+ const top = Math.max(0, nextScrollTop);
3134
+ if ('function' == typeof scrollContainer.scrollTo) return void scrollContainer.scrollTo({
3135
+ behavior: 'instant',
3136
+ top
3137
+ });
3138
+ const previousScrollBehavior = scrollContainer.style.scrollBehavior;
3139
+ scrollContainer.style.scrollBehavior = 'auto';
3140
+ view?.getComputedStyle(scrollContainer).scrollBehavior;
3141
+ scrollContainer.scrollTop = top;
3142
+ scrollContainer.style.scrollBehavior = previousScrollBehavior;
3143
+ }
2617
3144
  const emptyLayout = {
2618
3145
  width: 1,
2619
3146
  height: 1,
@@ -2628,8 +3155,15 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2628
3155
  const panelRef = useRef(null);
2629
3156
  const trackRef = useRef(null);
2630
3157
  const cardRefs = useRef(new Map());
3158
+ const anchorRefs = useRef(new Map());
3159
+ const measuredCardHeightsRef = useRef(new Map());
3160
+ const layoutRef = useRef(emptyLayout);
2631
3161
  const completedDraftIdRef = useRef(null);
3162
+ const pendingFocusRef = useRef(null);
3163
+ const pendingRevealCommentIdRef = useRef(null);
2632
3164
  const frameRef = useRef(0);
3165
+ const pendingFocusFrameRef = useRef(0);
3166
+ const windowFrameRef = useRef(0);
2633
3167
  const officeDialog = useOfficeDialog();
2634
3168
  const modal = useOfficeTaskPaneModal();
2635
3169
  const modalAttributes = modal ? {
@@ -2639,6 +3173,51 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2639
3173
  const unresolved = comments.filter((comment)=>!comment.resolved).length;
2640
3174
  const repliesDirty = Object.values(drafts).some((value)=>Boolean(value.trim()));
2641
3175
  const commentsDirty = draftDirty || repliesDirty;
3176
+ const commentsById = useMemo(()=>new Map(comments.map((comment)=>[
3177
+ comment.id,
3178
+ comment
3179
+ ])), [
3180
+ comments
3181
+ ]);
3182
+ const commentKeys = useMemo(()=>comments.map((comment)=>comment.id), [
3183
+ comments
3184
+ ]);
3185
+ const commentIndexById = useMemo(()=>new Map(commentKeys.map((id, index)=>[
3186
+ id,
3187
+ index
3188
+ ])), [
3189
+ commentKeys
3190
+ ]);
3191
+ const effectiveActiveCommentId = activeCommentId && commentsById.has(activeCommentId) ? activeCommentId : comments.find((comment)=>!comment.resolved)?.id ?? comments[0]?.id ?? null;
3192
+ const dirtyReplyKeys = useMemo(()=>Object.entries(drafts).filter(([, value])=>Boolean(value.trim())).map(([id])=>id), [
3193
+ drafts
3194
+ ]);
3195
+ const commentWindow = useDocumentCommentWindow({
3196
+ keys: commentKeys,
3197
+ onRovingKeyChange: setActiveCommentId,
3198
+ pinnedKeys: dirtyReplyKeys,
3199
+ rovingKey: effectiveActiveCommentId
3200
+ });
3201
+ const mountedCommentIndicesKey = commentWindow.mountedIndices.join(',');
3202
+ const mountedCommentIds = useMemo(()=>new Set(commentWindow.mountedIndices.flatMap((index)=>{
3203
+ const id = commentKeys[index];
3204
+ return id ? [
3205
+ id
3206
+ ] : [];
3207
+ })), [
3208
+ commentKeys,
3209
+ commentWindow.mountedIndices
3210
+ ]);
3211
+ const layoutItemsById = useMemo(()=>new Map(layout.items.map((item)=>[
3212
+ item.id,
3213
+ item
3214
+ ])), [
3215
+ layout.items
3216
+ ]);
3217
+ const visibleConnectorItems = useMemo(()=>layout.items.filter((item)=>'draft' === item.kind || mountedCommentIds.has(item.id)), [
3218
+ layout.items,
3219
+ mountedCommentIds
3220
+ ]);
2642
3221
  useEffect(()=>{
2643
3222
  onDirtyChange?.(commentsDirty);
2644
3223
  }, [
@@ -2696,6 +3275,10 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2696
3275
  });
2697
3276
  if (!confirmed) return;
2698
3277
  const nextComment = comments[index + 1] ?? comments[index - 1];
3278
+ pendingFocusRef.current = {
3279
+ commentId: nextComment?.id ?? null
3280
+ };
3281
+ setActiveCommentId(nextComment?.id ?? null);
2699
3282
  setDrafts((current)=>{
2700
3283
  const next = {
2701
3284
  ...current
@@ -2704,15 +3287,15 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2704
3287
  return next;
2705
3288
  });
2706
3289
  onDelete(commentId);
2707
- requestAnimationFrame(()=>{
2708
- const nextTarget = nextComment ? cardRefs.current.get(nextComment.id)?.querySelector('.work-document-comment-anchor') : panelRef.current?.querySelector('.ds-icon-button.close');
2709
- if (nextTarget) nextTarget.focus({
2710
- preventScroll: true
2711
- });
2712
- else if (!editor.isDestroyed) editor.view.dom.focus({
2713
- preventScroll: true
2714
- });
2715
- });
3290
+ if (nextComment) selectDocumentComment(editor, nextComment);
3291
+ };
3292
+ const handleCommentAnchorKeyDown = (event, index)=>{
3293
+ const nextIndex = documentCommentKeyboardDestination(event.key, index, comments.length);
3294
+ if (null === nextIndex) return;
3295
+ event.preventDefault();
3296
+ const nextComment = comments[nextIndex];
3297
+ if (nextComment) selectDocumentComment(editor, nextComment);
3298
+ commentWindow.focusAt(nextIndex);
2716
3299
  };
2717
3300
  const measure = useCallback(()=>{
2718
3301
  if (editor.isDestroyed) return;
@@ -2730,34 +3313,30 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2730
3313
  const trackOffset = trackRect.top - surfaceRect.top;
2731
3314
  const visibleTrackTop = Math.max(8, Math.max(scrollRect?.top ?? trackRect.top, panelHeaderRect?.bottom ?? trackRect.top) - trackRect.top + 8);
2732
3315
  const visibleTrackBottom = Math.min(scrollRect?.bottom ?? trackRect.bottom, panelRect.bottom) - trackRect.top - 8;
2733
- const commentsById = new Map(comments.map((comment)=>[
2734
- comment.id,
2735
- comment
2736
- ]));
2737
3316
  const marks = [
2738
3317
  ...editor.view.dom.querySelectorAll('[data-document-comment][data-comment-id]')
2739
3318
  ];
3319
+ const anchorRectsById = new Map();
2740
3320
  for (const mark of marks){
2741
3321
  const id = mark.dataset.commentId ?? '';
2742
- mark.classList.toggle('is-active-comment', id === activeCommentId);
3322
+ mark.classList.toggle('is-active-comment', id === effectiveActiveCommentId);
2743
3323
  mark.classList.toggle('is-resolved-comment', Boolean(commentsById.get(id)?.resolved));
3324
+ const anchorRect = [
3325
+ ...mark.getClientRects()
3326
+ ].at(-1);
3327
+ if (id && anchorRect) anchorRectsById.set(id, {
3328
+ right: anchorRect.right,
3329
+ top: anchorRect.top,
3330
+ height: anchorRect.height
3331
+ });
2744
3332
  }
2745
- const entries = comments.map((comment)=>{
2746
- const anchorRect = marks.filter((mark)=>mark.dataset.commentId === comment.id).flatMap((mark)=>[
2747
- ...mark.getClientRects()
2748
- ]).at(-1);
2749
- return {
3333
+ const entries = comments.map((comment)=>({
2750
3334
  id: comment.id,
2751
3335
  kind: 'comment',
2752
3336
  commentId: comment.id,
2753
3337
  from: comment.from,
2754
- anchorRect: anchorRect ? {
2755
- right: anchorRect.right,
2756
- top: anchorRect.top,
2757
- height: anchorRect.height
2758
- } : void 0
2759
- };
2760
- });
3338
+ anchorRect: anchorRectsById.get(comment.id)
3339
+ }));
2761
3340
  if (draft) {
2762
3341
  const range = documentCommentDraftRange(editor) ?? draft;
2763
3342
  entries.push({
@@ -2771,7 +3350,15 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2771
3350
  let nextCardTop = 8;
2772
3351
  const items = entries.map((entry)=>{
2773
3352
  const { anchorRect } = entry;
2774
- const cardHeight = cardRefs.current.get(entry.id)?.getBoundingClientRect().height ?? ('draft' === entry.kind ? 164 : 112);
3353
+ const card = cardRefs.current.get(entry.id);
3354
+ const measuredCardHeight = card?.getBoundingClientRect().height ?? 0;
3355
+ let cardHeight = 164;
3356
+ if ('comment' === entry.kind) {
3357
+ const comment = commentsById.get(entry.id);
3358
+ const measurementKey = comment ? commentCardMeasurementKey(comment, comment.id === effectiveActiveCommentId) : entry.id;
3359
+ if (measuredCardHeight > 0) measuredCardHeightsRef.current.set(measurementKey, measuredCardHeight);
3360
+ cardHeight = measuredCardHeightsRef.current.get(measurementKey) ?? (comment ? estimateCommentCardHeight(comment, comment.id === effectiveActiveCommentId) : 112);
3361
+ } else if (measuredCardHeight > 0) cardHeight = measuredCardHeight;
2775
3362
  const preferredTop = anchorRect ? anchorRect.top - trackRect.top - 18 : nextCardTop;
2776
3363
  const minimumTop = Math.max(8, nextCardTop);
2777
3364
  let cardTop = Math.max(minimumTop, preferredTop);
@@ -2801,12 +3388,14 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2801
3388
  trackHeight: Math.max(1, nextCardTop),
2802
3389
  items
2803
3390
  };
3391
+ layoutRef.current = nextLayout;
2804
3392
  setLayout((current)=>sameCommentTrackLayout(current, nextLayout) ? current : nextLayout);
2805
3393
  }, [
2806
- activeCommentId,
2807
3394
  comments,
3395
+ commentsById,
2808
3396
  draft,
2809
3397
  editor,
3398
+ effectiveActiveCommentId,
2810
3399
  surfaceRef
2811
3400
  ]);
2812
3401
  const scheduleMeasure = useCallback(()=>{
@@ -2815,9 +3404,47 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2815
3404
  }, [
2816
3405
  measure
2817
3406
  ]);
3407
+ const updateWindowFromViewport = useCallback(()=>{
3408
+ const surface = surfaceRef.current;
3409
+ const panel = panelRef.current;
3410
+ const track = trackRef.current;
3411
+ if (!surface || !panel || !track) return;
3412
+ const focusedCard = panel.ownerDocument.activeElement?.closest('.work-document-comment-card[data-comment-id]');
3413
+ const focusedCommentId = focusedCard?.dataset.commentId;
3414
+ const focusedIndex = focusedCommentId ? commentIndexById.get(focusedCommentId) : void 0;
3415
+ if (void 0 !== focusedIndex) return void commentWindow.onViewportAnchorChange(focusedIndex);
3416
+ const scroll = surface.closest('.work-document-scroll');
3417
+ const scrollRect = scroll?.getBoundingClientRect();
3418
+ const panelRect = panel.getBoundingClientRect();
3419
+ const trackRect = track.getBoundingClientRect();
3420
+ const panelHeader = panel.firstElementChild;
3421
+ const panelHeaderRect = panelHeader instanceof HTMLElement ? panelHeader.getBoundingClientRect() : void 0;
3422
+ const visibleTop = Math.max(scrollRect?.top ?? trackRect.top, panelRect.top, panelHeaderRect?.bottom ?? panelRect.top) - trackRect.top;
3423
+ const visibleComments = layoutRef.current.items.filter((item)=>'comment' === item.kind);
3424
+ let anchor = visibleComments[0];
3425
+ for (const item of visibleComments){
3426
+ if (item.cardTop > visibleTop + 8) break;
3427
+ anchor = item;
3428
+ }
3429
+ const index = anchor ? commentIndexById.get(anchor.id) : void 0;
3430
+ if (void 0 !== index) commentWindow.onViewportAnchorChange(index);
3431
+ }, [
3432
+ commentIndexById,
3433
+ commentWindow.onViewportAnchorChange,
3434
+ surfaceRef
3435
+ ]);
3436
+ const scheduleWindowUpdate = useCallback(()=>{
3437
+ cancelAnimationFrame(windowFrameRef.current);
3438
+ windowFrameRef.current = requestAnimationFrame(updateWindowFromViewport);
3439
+ }, [
3440
+ updateWindowFromViewport
3441
+ ]);
2818
3442
  useLayoutEffect(()=>{
2819
3443
  measure();
2820
- return ()=>cancelAnimationFrame(frameRef.current);
3444
+ return ()=>{
3445
+ cancelAnimationFrame(frameRef.current);
3446
+ cancelAnimationFrame(windowFrameRef.current);
3447
+ };
2821
3448
  }, [
2822
3449
  measure
2823
3450
  ]);
@@ -2840,6 +3467,35 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2840
3467
  comments,
2841
3468
  draft
2842
3469
  ]);
3470
+ useLayoutEffect(()=>{
3471
+ const pending = pendingFocusRef.current;
3472
+ if (!pending || pendingFocusFrameRef.current) return;
3473
+ pendingFocusFrameRef.current = requestAnimationFrame(()=>{
3474
+ pendingFocusFrameRef.current = 0;
3475
+ const current = pendingFocusRef.current;
3476
+ if (!current) return;
3477
+ const target = current.commentId ? anchorRefs.current.get(current.commentId) : panelRef.current?.querySelector('.ds-icon-button.close');
3478
+ if (!target) return;
3479
+ pendingFocusRef.current = null;
3480
+ focusDocumentCommentItem(target);
3481
+ });
3482
+ }, [
3483
+ comments,
3484
+ effectiveActiveCommentId,
3485
+ mountedCommentIndicesKey
3486
+ ]);
3487
+ useEffect(()=>()=>cancelAnimationFrame(pendingFocusFrameRef.current), []);
3488
+ useLayoutEffect(()=>{
3489
+ const commentId = pendingRevealCommentIdRef.current;
3490
+ if (!commentId) return;
3491
+ const card = cardRefs.current.get(commentId);
3492
+ if (!card) return;
3493
+ pendingRevealCommentIdRef.current = null;
3494
+ scrollDocumentCommentItemIntoView(card);
3495
+ }, [
3496
+ effectiveActiveCommentId,
3497
+ mountedCommentIndicesKey
3498
+ ]);
2843
3499
  useEffect(()=>{
2844
3500
  const surface = surfaceRef.current;
2845
3501
  const panel = panelRef.current;
@@ -2857,19 +3513,27 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2857
3513
  subtree: true
2858
3514
  });
2859
3515
  const scroll = surface.closest('.work-document-scroll');
2860
- scroll?.addEventListener('scroll', scheduleMeasure, {
3516
+ const handleScroll = draft ? scheduleMeasure : scheduleWindowUpdate;
3517
+ scroll?.addEventListener('scroll', handleScroll, {
3518
+ passive: true
3519
+ });
3520
+ panel.addEventListener('scroll', handleScroll, {
2861
3521
  passive: true
2862
3522
  });
2863
3523
  window.addEventListener('resize', scheduleMeasure);
2864
3524
  return ()=>{
2865
3525
  observer?.disconnect();
2866
3526
  mutationObserver?.disconnect();
2867
- scroll?.removeEventListener('scroll', scheduleMeasure);
3527
+ scroll?.removeEventListener('scroll', handleScroll);
3528
+ panel.removeEventListener('scroll', handleScroll);
2868
3529
  window.removeEventListener('resize', scheduleMeasure);
2869
3530
  };
2870
3531
  }, [
3532
+ draft,
2871
3533
  editor,
3534
+ mountedCommentIndicesKey,
2872
3535
  scheduleMeasure,
3536
+ scheduleWindowUpdate,
2873
3537
  surfaceRef
2874
3538
  ]);
2875
3539
  useEffect(()=>{
@@ -2880,7 +3544,10 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2880
3544
  if (!(target instanceof Element)) return;
2881
3545
  const anchor = target.closest('[data-document-comment][data-comment-id]');
2882
3546
  const id = anchor?.dataset.commentId;
2883
- if (id && comments.some((comment)=>comment.id === id)) setActiveCommentId(id);
3547
+ if (id && comments.some((comment)=>comment.id === id)) {
3548
+ pendingRevealCommentIdRef.current = id;
3549
+ setActiveCommentId(id);
3550
+ }
2884
3551
  };
2885
3552
  editorDom.addEventListener('click', openAnchoredComment);
2886
3553
  return ()=>editorDom.removeEventListener('click', openAnchoredComment);
@@ -2895,12 +3562,12 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2895
3562
  viewBox: `0 0 ${layout.width} ${layout.height}`,
2896
3563
  preserveAspectRatio: "none",
2897
3564
  "aria-hidden": "true",
2898
- children: layout.items.map((item)=>{
2899
- const comment = comments.find((candidate)=>candidate.id === item.commentId);
3565
+ children: visibleConnectorItems.map((item)=>{
3566
+ const comment = item.commentId ? commentsById.get(item.commentId) : void 0;
2900
3567
  const bendX = Math.max(item.startX + 16, item.endX - 28);
2901
3568
  const className = [
2902
3569
  comment?.resolved ? 'resolved' : '',
2903
- activeCommentId === item.commentId ? 'active' : '',
3570
+ effectiveActiveCommentId === item.commentId ? 'active' : '',
2904
3571
  'draft' === item.kind ? 'draft active' : ''
2905
3572
  ].filter(Boolean).join(' ');
2906
3573
  return /*#__PURE__*/ jsxs("g", {
@@ -2947,50 +3614,72 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
2947
3614
  })
2948
3615
  ]
2949
3616
  }),
2950
- /*#__PURE__*/ jsxs("div", {
3617
+ /*#__PURE__*/ jsxs("ol", {
2951
3618
  ref: trackRef,
2952
3619
  className: "work-document-comment-track",
3620
+ "aria-label": "文档批注",
3621
+ "data-document-comment-count": comments.length,
3622
+ "data-document-comment-mounted-count": commentWindow.mountedIndices.length,
3623
+ "data-document-comment-track-height": Math.round(layout.trackHeight),
3624
+ "data-document-comment-window-end": commentWindow.range.end,
3625
+ "data-document-comment-window-limit": 32,
3626
+ "data-document-comment-window-start": commentWindow.range.start,
3627
+ "data-document-comment-windowed": commentWindow.range.windowed ? 'true' : 'false',
2953
3628
  style: {
2954
3629
  minHeight: `${layout.trackHeight}px`
2955
3630
  },
2956
3631
  children: [
2957
- draft && /*#__PURE__*/ jsx(document_comment_composer_DocumentCommentComposer, {
2958
- ref: (element)=>{
2959
- const id = `draft:${draft.id}`;
2960
- if (element) cardRefs.current.set(id, element);
2961
- else cardRefs.current.delete(id);
2962
- },
2963
- draft: draft,
2964
- top: layout.items.find((item)=>'draft' === item.kind)?.cardTop ?? 8,
2965
- onCancel: ()=>void cancelDraft(),
2966
- onDirtyChange: setDraftDirty,
2967
- onSubmit: onSubmitDraft
3632
+ draft && /*#__PURE__*/ jsx("li", {
3633
+ className: "work-document-comment-draft-item",
3634
+ children: /*#__PURE__*/ jsx(document_comment_composer_DocumentCommentComposer, {
3635
+ ref: (element)=>{
3636
+ const id = `draft:${draft.id}`;
3637
+ if (element) cardRefs.current.set(id, element);
3638
+ else cardRefs.current.delete(id);
3639
+ },
3640
+ draft: draft,
3641
+ top: layoutItemsById.get(`draft:${draft.id}`)?.cardTop ?? 8,
3642
+ onCancel: ()=>void cancelDraft(),
3643
+ onDirtyChange: setDraftDirty,
3644
+ onSubmit: onSubmitDraft
3645
+ })
2968
3646
  }, draft.id),
2969
- comments.map((comment, index)=>{
2970
- const item = layout.items.find((candidate)=>candidate.id === comment.id);
2971
- const active = activeCommentId === comment.id;
2972
- return /*#__PURE__*/ jsxs("article", {
3647
+ commentWindow.mountedIndices.map((index)=>{
3648
+ const comment = comments[index];
3649
+ if (!comment) return null;
3650
+ const item = layoutItemsById.get(comment.id);
3651
+ const active = effectiveActiveCommentId === comment.id;
3652
+ return /*#__PURE__*/ jsxs("li", {
3653
+ "aria-posinset": index + 1,
3654
+ "aria-setsize": comments.length,
2973
3655
  ref: (element)=>{
2974
3656
  if (element) cardRefs.current.set(comment.id, element);
2975
3657
  else cardRefs.current.delete(comment.id);
2976
3658
  },
2977
- className: `${comment.resolved ? 'resolved' : ''}${active ? ' active' : ''}`,
3659
+ className: `work-document-comment-card${comment.resolved ? ' resolved' : ''}${active ? ' active' : ''}`,
2978
3660
  "data-comment-id": comment.id,
3661
+ "data-document-comment-card-top": Math.round(item?.cardTop ?? 8),
3662
+ "data-document-comment-item": index + 1,
2979
3663
  style: {
2980
3664
  top: `${item?.cardTop ?? 8}px`
2981
3665
  },
2982
- onFocusCapture: ()=>setActiveCommentId(comment.id),
3666
+ onFocusCapture: ()=>commentWindow.onItemFocus(index),
2983
3667
  children: [
2984
3668
  /*#__PURE__*/ jsxs("button", {
3669
+ ref: (element)=>{
3670
+ commentWindow.registerItem(comment.id, element);
3671
+ if (element) anchorRefs.current.set(comment.id, element);
3672
+ else anchorRefs.current.delete(comment.id);
3673
+ },
2985
3674
  type: "button",
2986
3675
  className: "work-document-comment-anchor",
2987
3676
  "aria-label": `定位批注 ${index + 1}`,
3677
+ "aria-current": active ? 'location' : void 0,
3678
+ tabIndex: commentWindow.rovingIndex === index ? 0 : -1,
3679
+ onKeyDown: (event)=>handleCommentAnchorKeyDown(event, index),
2988
3680
  onClick: ()=>{
2989
3681
  setActiveCommentId(comment.id);
2990
- editor.chain().focus().setTextSelection({
2991
- from: Math.min(comment.from, editor.state.doc.content.size),
2992
- to: Math.min(comment.to, editor.state.doc.content.size)
2993
- }).run();
3682
+ editor.chain().focus().setTextSelection(documentCommentSelection(editor, comment)).run();
2994
3683
  },
2995
3684
  children: [
2996
3685
  /*#__PURE__*/ jsx("span", {
@@ -3107,10 +3796,13 @@ function DocumentCommentsPanel({ editor, comments, draft, surfaceRef, onReply, o
3107
3796
  ]
3108
3797
  }, comment.id);
3109
3798
  }),
3110
- !comments.length && !draft && /*#__PURE__*/ jsx(CollectionState, {
3111
- className: "work-document-comments-empty",
3112
- role: "status",
3113
- children: "选择文字并添加批注。"
3799
+ !comments.length && !draft && /*#__PURE__*/ jsx("li", {
3800
+ className: "work-document-comment-empty-item",
3801
+ children: /*#__PURE__*/ jsx(CollectionState, {
3802
+ className: "work-document-comments-empty",
3803
+ role: "status",
3804
+ children: "选择文字并添加批注。"
3805
+ })
3114
3806
  })
3115
3807
  ]
3116
3808
  })
@@ -3140,6 +3832,29 @@ function sameCommentTrackLayout(current, next) {
3140
3832
  return candidate?.id === item.id && candidate.kind === item.kind && candidate.commentId === item.commentId && Math.abs(candidate.cardTop - item.cardTop) <= 0.5 && Math.abs(candidate.startX - item.startX) <= 0.5 && Math.abs(candidate.startY - item.startY) <= 0.5 && Math.abs(candidate.endX - item.endX) <= 0.5 && Math.abs(candidate.endY - item.endY) <= 0.5;
3141
3833
  });
3142
3834
  }
3835
+ function commentCardMeasurementKey(comment, active) {
3836
+ const replies = (comment.replies ?? []).map((reply)=>`${reply.id}:${reply.text.length}`).join('|');
3837
+ return `${comment.id}:${active ? 'active' : 'idle'}:${comment.text.length}:${replies}`;
3838
+ }
3839
+ function estimateCommentCardHeight(comment, active) {
3840
+ const bodyLines = estimatedWrappedLines(comment.text, 34);
3841
+ const replyHeight = (comment.replies ?? []).reduce((height, reply)=>height + 32 + 14 * estimatedWrappedLines(reply.text, 31), 0);
3842
+ return 78 + 15 * bodyLines + replyHeight + (active ? 92 : 0);
3843
+ }
3844
+ function selectDocumentComment(editor, comment) {
3845
+ if (editor.isDestroyed) return;
3846
+ editor.commands.setTextSelection(documentCommentSelection(editor, comment));
3847
+ }
3848
+ function documentCommentSelection(editor, comment) {
3849
+ const maximum = Math.max(1, editor.state.doc.content.size - 1);
3850
+ return {
3851
+ from: Math.min(comment.from, maximum),
3852
+ to: Math.min(comment.to, maximum)
3853
+ };
3854
+ }
3855
+ function estimatedWrappedLines(value, charactersPerLine) {
3856
+ return Math.max(1, value.split('\n').reduce((lines, line)=>lines + Math.max(1, Math.ceil(line.length / charactersPerLine)), 0));
3857
+ }
3143
3858
  function commentAuthorInitials(author) {
3144
3859
  const words = author.trim().split(/\s+/).filter(Boolean);
3145
3860
  if (!words.length) return '审';
@@ -4543,43 +5258,401 @@ function normalizedDocumentHeadingText(value) {
4543
5258
  function normalizedDocumentOutlineQuery(value) {
4544
5259
  return value.normalize('NFKC').trim().toLocaleLowerCase();
4545
5260
  }
4546
- function DocumentPageNavigation({ currentPage, pages, onSelectPage }) {
5261
+ const DOCUMENT_OUTLINE_ITEM_HEIGHT = 31;
5262
+ const DOCUMENT_OUTLINE_LIST_PADDING_TOP = 6;
5263
+ const document_outline_navigation_DocumentOutlineNavigation = /*#__PURE__*/ forwardRef(function({ activeItemId, collapsedIds, items, rovingId, onNavigate, onRovingIdChange, onToggleCollapsed }, ref) {
5264
+ const keys = useMemo(()=>items.map((item)=>item.id), [
5265
+ items
5266
+ ]);
5267
+ const pinnedKeys = useMemo(()=>[
5268
+ activeItemId
5269
+ ], [
5270
+ activeItemId
5271
+ ]);
5272
+ const navigation = useDocumentNavigationWindow({
5273
+ estimatedItemHeight: DOCUMENT_OUTLINE_ITEM_HEIGHT,
5274
+ itemGap: 0,
5275
+ keys,
5276
+ listPaddingTop: DOCUMENT_OUTLINE_LIST_PADDING_TOP,
5277
+ onRovingKeyChange: onRovingIdChange,
5278
+ pinnedKeys,
5279
+ rovingKey: rovingId
5280
+ });
5281
+ useImperativeHandle(ref, ()=>({
5282
+ focusFirst: ()=>navigation.focusAt(0),
5283
+ focusLast: ()=>navigation.focusAt(items.length - 1)
5284
+ }), [
5285
+ items.length,
5286
+ navigation.focusAt
5287
+ ]);
5288
+ const handleItemKeyDown = (event, item, index)=>{
5289
+ if ('ArrowDown' === event.key) {
5290
+ event.preventDefault();
5291
+ navigation.focusAt(Math.min(items.length - 1, index + 1));
5292
+ } else if ('ArrowUp' === event.key) {
5293
+ event.preventDefault();
5294
+ navigation.focusAt(Math.max(0, index - 1));
5295
+ } else if ('Home' === event.key) {
5296
+ event.preventDefault();
5297
+ navigation.focusAt(0);
5298
+ } else if ('End' === event.key) {
5299
+ event.preventDefault();
5300
+ navigation.focusAt(items.length - 1);
5301
+ } else if ('ArrowRight' === event.key && item.hasChildren && collapsedIds.has(item.id)) {
5302
+ event.preventDefault();
5303
+ onToggleCollapsed(item.id);
5304
+ } else if ('ArrowRight' === event.key && item.hasChildren) {
5305
+ event.preventDefault();
5306
+ const child = items[index + 1];
5307
+ if (child?.depth > item.depth) navigation.focusAt(index + 1);
5308
+ } else if ('ArrowLeft' === event.key) {
5309
+ event.preventDefault();
5310
+ if (item.hasChildren && !collapsedIds.has(item.id)) onToggleCollapsed(item.id);
5311
+ else if (item.parentId) navigation.focusAt(items.findIndex((candidate)=>candidate.id === item.parentId));
5312
+ }
5313
+ };
5314
+ return /*#__PURE__*/ jsx("nav", {
5315
+ ref: navigation.viewportRef,
5316
+ className: "work-document-task-pane-body work-document-outline",
5317
+ "aria-label": "文档标题",
5318
+ "data-document-navigation-collection": "outline",
5319
+ "data-document-navigation-item-count": items.length,
5320
+ "data-document-navigation-mounted-count": navigation.mountedCount,
5321
+ "data-document-navigation-window-end": navigation.range.end,
5322
+ "data-document-navigation-window-limit": 32,
5323
+ "data-document-navigation-window-start": navigation.range.start,
5324
+ "data-document-navigation-windowed": navigation.range.windowed ? 'true' : 'false',
5325
+ onScroll: navigation.onScroll,
5326
+ children: items.length ? /*#__PURE__*/ jsx("ol", {
5327
+ children: navigation.entries.map((entry)=>{
5328
+ if ('spacer' === entry.kind) return /*#__PURE__*/ jsx(DocumentNavigationWindowSpacer, {
5329
+ entry: entry
5330
+ }, `spacer-${entry.start}-${entry.end}`);
5331
+ const item = items[entry.index];
5332
+ if (!item) return null;
5333
+ const collapsed = collapsedIds.has(item.id);
5334
+ return /*#__PURE__*/ jsxs("li", {
5335
+ "aria-posinset": entry.index + 1,
5336
+ "aria-setsize": items.length,
5337
+ className: activeItemId === item.id ? 'active' : void 0,
5338
+ "data-document-navigation-item": entry.index + 1,
5339
+ style: {
5340
+ '--work-document-outline-indent': `${14 * item.depth}px`
5341
+ },
5342
+ children: [
5343
+ item.hasChildren ? /*#__PURE__*/ jsx("button", {
5344
+ type: "button",
5345
+ tabIndex: -1,
5346
+ className: "work-document-outline-toggle",
5347
+ "aria-label": `${collapsed ? '展开' : '折叠'} ${item.text}`,
5348
+ title: collapsed ? '展开下级标题' : '折叠下级标题',
5349
+ onClick: ()=>onToggleCollapsed(item.id),
5350
+ children: collapsed ? /*#__PURE__*/ jsx(ChevronRight, {
5351
+ size: 13
5352
+ }) : /*#__PURE__*/ jsx(ChevronDown, {
5353
+ size: 13
5354
+ })
5355
+ }) : /*#__PURE__*/ jsx("span", {
5356
+ className: "work-document-outline-toggle-spacer"
5357
+ }),
5358
+ /*#__PURE__*/ jsx("button", {
5359
+ ref: (element)=>navigation.registerItem(item.id, element),
5360
+ type: "button",
5361
+ className: "work-document-outline-item",
5362
+ tabIndex: navigation.rovingIndex === entry.index ? 0 : -1,
5363
+ "aria-current": activeItemId === item.id ? 'location' : void 0,
5364
+ "aria-expanded": item.hasChildren ? !collapsed : void 0,
5365
+ title: item.text,
5366
+ onFocus: ()=>navigation.onItemFocus(entry.index),
5367
+ onKeyDown: (event)=>handleItemKeyDown(event, item, entry.index),
5368
+ onClick: ()=>onNavigate(item.from),
5369
+ children: item.text
5370
+ })
5371
+ ]
5372
+ }, item.id);
5373
+ })
5374
+ }) : /*#__PURE__*/ jsx("div", {
5375
+ className: "work-document-outline-empty",
5376
+ children: "应用标题样式后,可在这里快速跳转"
5377
+ })
5378
+ });
5379
+ });
5380
+ let thumbnailCaptureQueue = Promise.resolve();
5381
+ function DocumentPageThumbnail({ backgroundColor, fallbackText, orientation, pageIndex, priority = false, source, renderThumbnail = renderDocumentPageThumbnail }) {
5382
+ const rootRef = useRef(null);
5383
+ const nearViewport = useNearThumbnailViewport(rootRef);
5384
+ const captureActive = priority || nearViewport;
5385
+ const sourceMutation = useThumbnailSourceMutation(source, captureActive);
5386
+ const [image, setImage] = useState(null);
5387
+ const [state, setState] = useState(source ? 'loading' : 'idle');
5388
+ useEffect(()=>{
5389
+ if (!source || !captureActive) {
5390
+ setImage(null);
5391
+ setState('idle');
5392
+ return;
5393
+ }
5394
+ if (pageIndex < 0 || pageIndex >= source.pageCount) {
5395
+ setImage(null);
5396
+ setState('failed');
5397
+ return;
5398
+ }
5399
+ let cancelled = false;
5400
+ setState((current)=>'ready' === current ? 'refreshing' : 'loading');
5401
+ const targetPixelWidth = 'landscape' === orientation ? 340 : 276;
5402
+ const task = async ()=>{
5403
+ if (cancelled) return;
5404
+ const nextImage = await renderThumbnail(source, pageIndex, targetPixelWidth);
5405
+ if (cancelled) return;
5406
+ setImage(nextImage);
5407
+ setState('ready');
5408
+ };
5409
+ const queued = thumbnailCaptureQueue.then(task, task);
5410
+ thumbnailCaptureQueue = queued.catch(()=>void 0);
5411
+ queued.catch(()=>{
5412
+ if (cancelled) return;
5413
+ setImage(null);
5414
+ setState('failed');
5415
+ });
5416
+ return ()=>{
5417
+ cancelled = true;
5418
+ };
5419
+ }, [
5420
+ captureActive,
5421
+ orientation,
5422
+ pageIndex,
5423
+ renderThumbnail,
5424
+ source?.element,
5425
+ source?.pageCount,
5426
+ source?.pageGap,
5427
+ source?.pageHeight,
5428
+ source?.pageWidth,
5429
+ source?.revision,
5430
+ sourceMutation
5431
+ ]);
5432
+ return /*#__PURE__*/ jsx("span", {
5433
+ ref: rootRef,
5434
+ className: `work-document-page-thumbnail ${orientation}`,
5435
+ "data-testid": "document-page-thumbnail",
5436
+ "data-thumbnail-source": source ? 'live-page' : 'text-fallback',
5437
+ "data-thumbnail-state": state,
5438
+ "aria-hidden": "true",
5439
+ style: {
5440
+ aspectRatio: source ? `${source.pageWidth} / ${source.pageHeight}` : void 0,
5441
+ backgroundColor
5442
+ },
5443
+ children: image ? /*#__PURE__*/ jsx("img", {
5444
+ alt: "",
5445
+ "data-document-page-raster": pageIndex + 1,
5446
+ draggable: false,
5447
+ src: image
5448
+ }) : /*#__PURE__*/ jsx("span", {
5449
+ className: "work-document-page-thumbnail-fallback",
5450
+ children: fallbackText || '空白页'
5451
+ })
5452
+ });
5453
+ }
5454
+ async function renderDocumentPageThumbnail(source, pageIndex, targetPixelWidth, renderCanvas = renderDocumentPageCanvas) {
5455
+ if (!Number.isSafeInteger(pageIndex) || pageIndex < 0 || pageIndex >= source.pageCount) throw new Error('The requested document thumbnail page is out of range.');
5456
+ const capture = mountWorkLiveDocumentCapture(source, pageIndex);
5457
+ try {
5458
+ const scale = Math.max(0.1, Math.min(1, targetPixelWidth / source.pageWidth));
5459
+ const canvas = await renderCanvas(capture.viewport, {
5460
+ backgroundColor: capture.backgroundColor,
5461
+ fontReadyTimeout: 0,
5462
+ height: source.pageHeight,
5463
+ logging: false,
5464
+ scale,
5465
+ useCORS: true,
5466
+ width: source.pageWidth,
5467
+ windowHeight: Math.ceil(source.pageHeight),
5468
+ windowWidth: Math.ceil(source.pageWidth)
5469
+ });
5470
+ return canvas.toDataURL('image/png');
5471
+ } finally{
5472
+ capture.host.remove();
5473
+ }
5474
+ }
5475
+ async function renderDocumentPageCanvas(element, options) {
5476
+ const { default: html2canvas } = await import("html2canvas");
5477
+ return html2canvas(element, options);
5478
+ }
5479
+ function useNearThumbnailViewport(rootRef) {
5480
+ const [nearViewport, setNearViewport] = useState(()=>"u" < typeof IntersectionObserver);
5481
+ useEffect(()=>{
5482
+ const element = rootRef.current;
5483
+ if (!element) return;
5484
+ if ("u" < typeof IntersectionObserver) return void setNearViewport(true);
5485
+ const root = element.closest('.work-document-page-navigation');
5486
+ const observer = new IntersectionObserver((entries)=>{
5487
+ const entry = entries[0];
5488
+ if (entry) setNearViewport(entry.isIntersecting);
5489
+ }, {
5490
+ root,
5491
+ rootMargin: '280px 0px'
5492
+ });
5493
+ observer.observe(element);
5494
+ return ()=>observer.disconnect();
5495
+ }, [
5496
+ rootRef
5497
+ ]);
5498
+ return nearViewport;
5499
+ }
5500
+ function useThumbnailSourceMutation(source, active) {
5501
+ const [revision, setRevision] = useState(0);
5502
+ useEffect(()=>{
5503
+ if (!source || !active || "u" < typeof MutationObserver) return;
5504
+ let timer = 0;
5505
+ const observer = new MutationObserver(()=>{
5506
+ window.clearTimeout(timer);
5507
+ timer = window.setTimeout(()=>setRevision((current)=>current + 1), 160);
5508
+ });
5509
+ observer.observe(source.element, {
5510
+ attributes: true,
5511
+ characterData: true,
5512
+ childList: true,
5513
+ subtree: true
5514
+ });
5515
+ return ()=>{
5516
+ observer.disconnect();
5517
+ window.clearTimeout(timer);
5518
+ };
5519
+ }, [
5520
+ active,
5521
+ source?.element,
5522
+ source?.revision
5523
+ ]);
5524
+ return revision;
5525
+ }
5526
+ const DOCUMENT_PAGE_WINDOW_THRESHOLD = 48;
5527
+ const DOCUMENT_PAGE_WINDOW_OVERSCAN = 4;
5528
+ const DOCUMENT_PAGE_FALLBACK_VIEWPORT_HEIGHT = 720;
5529
+ const DOCUMENT_PAGE_LIST_PADDING_TOP = 12;
5530
+ const DOCUMENT_PAGE_ITEM_GAP = 12;
5531
+ const DOCUMENT_PAGE_PORTRAIT_WIDTH = 138;
5532
+ const DOCUMENT_PAGE_LANDSCAPE_WIDTH = 170;
5533
+ const DOCUMENT_PAGE_BUTTON_CHROME_HEIGHT = 36;
5534
+ const DOCUMENT_PAGE_WINDOW_LIMIT = 24;
5535
+ function DocumentPageNavigation({ currentPage, pages, thumbnailSource, onSelectPage }) {
5536
+ const viewportRef = useRef(null);
4547
5537
  const pageRefs = useRef(new Map());
4548
- const [selectedPage, setSelectedPage] = useState(currentPage);
4549
- const [rovingPage, setRovingPage] = useState(currentPage);
5538
+ const pagesRef = useRef(pages);
5539
+ const pendingFocusPageRef = useRef(null);
5540
+ const initialIndex = documentPageIndex(pages, currentPage);
5541
+ const initialPhysicalPage = pages[initialIndex]?.physicalPage ?? currentPage;
5542
+ const [selectedPage, setSelectedPage] = useState(initialPhysicalPage);
5543
+ const [rovingPage, setRovingPage] = useState(initialPhysicalPage);
5544
+ const [anchorIndex, setAnchorIndex] = useState(Math.max(0, initialIndex));
5545
+ const [viewportHeight, setViewportHeight] = useState(DOCUMENT_PAGE_FALLBACK_VIEWPORT_HEIGHT);
5546
+ pagesRef.current = pages;
5547
+ const currentPageIndex = documentPageIndex(pages, currentPage);
5548
+ const resolvedCurrentPage = pages[currentPageIndex]?.physicalPage ?? null;
4550
5549
  useEffect(()=>{
4551
- setSelectedPage(currentPage);
4552
- setRovingPage(currentPage);
5550
+ if (null === resolvedCurrentPage) return;
5551
+ setSelectedPage(resolvedCurrentPage);
5552
+ setRovingPage(resolvedCurrentPage);
5553
+ setAnchorIndex(currentPageIndex);
4553
5554
  }, [
4554
- currentPage
5555
+ currentPageIndex,
5556
+ resolvedCurrentPage
4555
5557
  ]);
4556
5558
  useEffect(()=>{
4557
- if (pages.some((page)=>page.physicalPage === rovingPage)) return;
4558
- const fallback = pages.find((page)=>page.physicalPage === currentPage) ?? pages[0];
4559
- if (fallback) setRovingPage(fallback.physicalPage);
5559
+ if (!pages.length) return;
5560
+ const fallback = pages[Math.max(0, currentPageIndex)] ?? pages[0];
5561
+ if (!fallback) return;
5562
+ if (!pages.some((page)=>page.physicalPage === selectedPage)) setSelectedPage(fallback.physicalPage);
5563
+ if (!pages.some((page)=>page.physicalPage === rovingPage)) setRovingPage(fallback.physicalPage);
4560
5564
  }, [
4561
- currentPage,
5565
+ currentPageIndex,
4562
5566
  pages,
4563
- rovingPage
5567
+ rovingPage,
5568
+ selectedPage
4564
5569
  ]);
4565
- useEffect(()=>{
4566
- pageRefs.current.get(selectedPage)?.scrollIntoView({
5570
+ useLayoutEffect(()=>{
5571
+ const viewport = viewportRef.current;
5572
+ if (!viewport) return;
5573
+ const update = ()=>{
5574
+ const nextHeight = viewport.clientHeight || DOCUMENT_PAGE_FALLBACK_VIEWPORT_HEIGHT;
5575
+ setViewportHeight((current)=>current === nextHeight ? current : nextHeight);
5576
+ };
5577
+ update();
5578
+ if ("u" < typeof ResizeObserver) return;
5579
+ const observer = new ResizeObserver(update);
5580
+ observer.observe(viewport);
5581
+ return ()=>observer.disconnect();
5582
+ }, []);
5583
+ const itemHeights = useMemo(()=>pages.map((page)=>estimatedDocumentPageItemHeight(page, thumbnailSource)), [
5584
+ pages,
5585
+ thumbnailSource?.pageHeight,
5586
+ thumbnailSource?.pageWidth
5587
+ ]);
5588
+ const pageStarts = useMemo(()=>documentNavigationItemStarts(itemHeights, DOCUMENT_PAGE_ITEM_GAP), [
5589
+ itemHeights
5590
+ ]);
5591
+ const averageItemHeight = itemHeights.length ? (pageStarts[pageStarts.length - 1] ?? 1) / itemHeights.length : 1;
5592
+ const range = useMemo(()=>calculateDocumentPageRange({
5593
+ anchorIndex,
5594
+ averageItemHeight,
5595
+ pageCount: pages.length,
5596
+ viewportHeight
5597
+ }), [
5598
+ anchorIndex,
5599
+ averageItemHeight,
5600
+ pages.length,
5601
+ viewportHeight
5602
+ ]);
5603
+ const mountedIndices = useMemo(()=>documentNavigationWindowIndices({
5604
+ itemCount: pages.length,
5605
+ pinnedIndices: [
5606
+ documentPageIndex(pages, selectedPage),
5607
+ documentPageIndex(pages, rovingPage)
5608
+ ],
5609
+ range
5610
+ }), [
5611
+ pages,
5612
+ range,
5613
+ rovingPage,
5614
+ selectedPage
5615
+ ]);
5616
+ const listEntries = useMemo(()=>documentNavigationWindowEntries(mountedIndices, pageStarts, pages.length, DOCUMENT_PAGE_ITEM_GAP), [
5617
+ mountedIndices,
5618
+ pageStarts,
5619
+ pages.length
5620
+ ]);
5621
+ const mountedPageKey = mountedIndices.join(',');
5622
+ useLayoutEffect(()=>{
5623
+ pageRefs.current.get(selectedPage)?.scrollIntoView?.({
4567
5624
  block: 'nearest',
4568
5625
  inline: 'nearest'
4569
5626
  });
4570
5627
  }, [
4571
5628
  selectedPage
4572
5629
  ]);
5630
+ useLayoutEffect(()=>{
5631
+ const pendingPage = pendingFocusPageRef.current;
5632
+ if (null === pendingPage) return;
5633
+ const element = pageRefs.current.get(pendingPage);
5634
+ if (!element) return;
5635
+ pendingFocusPageRef.current = null;
5636
+ focusDocumentPageButton(element);
5637
+ }, [
5638
+ mountedPageKey,
5639
+ rovingPage
5640
+ ]);
4573
5641
  const focusPage = useCallback((pageNumber)=>{
5642
+ const pageIndex = documentPageIndex(pagesRef.current, pageNumber);
5643
+ if (pageIndex < 0) return;
4574
5644
  setRovingPage(pageNumber);
4575
- requestAnimationFrame(()=>{
4576
- pageRefs.current.get(pageNumber)?.focus({
4577
- preventScroll: true
4578
- });
4579
- });
5645
+ setAnchorIndex(pageIndex);
5646
+ const mounted = pageRefs.current.get(pageNumber);
5647
+ if (mounted) {
5648
+ pendingFocusPageRef.current = null;
5649
+ focusDocumentPageButton(mounted);
5650
+ return;
5651
+ }
5652
+ pendingFocusPageRef.current = pageNumber;
4580
5653
  }, []);
4581
5654
  const handleKeyDown = (event, page)=>{
4582
- const index = pages.findIndex((candidate)=>candidate.physicalPage === page.physicalPage);
5655
+ const index = documentPageIndex(pages, page.physicalPage);
4583
5656
  const focusAt = (requestedIndex)=>{
4584
5657
  const next = pages[requestedIndex];
4585
5658
  if (next) focusPage(next.physicalPage);
@@ -4599,12 +5672,41 @@ function DocumentPageNavigation({ currentPage, pages, onSelectPage }) {
4599
5672
  }
4600
5673
  };
4601
5674
  return /*#__PURE__*/ jsx("nav", {
5675
+ ref: viewportRef,
4602
5676
  className: "work-document-task-pane-body work-document-page-navigation",
4603
5677
  "aria-label": "文档页面",
5678
+ "data-document-page-count": pages.length,
5679
+ "data-document-page-mounted-count": mountedIndices.length,
5680
+ "data-document-page-window-end": range.end,
5681
+ "data-document-page-window-limit": DOCUMENT_PAGE_WINDOW_LIMIT,
5682
+ "data-document-page-window-start": range.start,
5683
+ "data-document-page-windowed": range.windowed ? 'true' : 'false',
5684
+ onScroll: (event)=>{
5685
+ if (!range.windowed) return;
5686
+ const nextAnchor = documentNavigationIndexAtOffset(pageStarts, Math.max(0, event.currentTarget.scrollTop - DOCUMENT_PAGE_LIST_PADDING_TOP), pages.length);
5687
+ setAnchorIndex((current)=>current === nextAnchor ? current : nextAnchor);
5688
+ },
4604
5689
  children: pages.length ? /*#__PURE__*/ jsx("ol", {
4605
- children: pages.map((page)=>{
5690
+ children: listEntries.map((entry)=>{
5691
+ if ('spacer' === entry.kind) return /*#__PURE__*/ jsx("li", {
5692
+ "aria-hidden": "true",
5693
+ className: "work-document-page-navigation-spacer",
5694
+ "data-document-page-spacer": entry.position,
5695
+ "data-document-page-spacer-end": entry.end,
5696
+ "data-document-page-spacer-start": entry.start + 1,
5697
+ role: "presentation",
5698
+ style: {
5699
+ height: `${entry.height}px`
5700
+ }
5701
+ }, `spacer-${entry.start}-${entry.end}`);
5702
+ const page = pages[entry.index];
5703
+ if (!page) return null;
4606
5704
  const current = page.physicalPage === selectedPage;
4607
5705
  return /*#__PURE__*/ jsx("li", {
5706
+ "aria-posinset": entry.index + 1,
5707
+ "aria-setsize": pages.length,
5708
+ "data-document-page-item": page.physicalPage,
5709
+ "data-document-page-orientation": page.orientation,
4608
5710
  children: /*#__PURE__*/ jsxs("button", {
4609
5711
  ref: (element)=>{
4610
5712
  if (element) pageRefs.current.set(page.physicalPage, element);
@@ -4620,18 +5722,17 @@ function DocumentPageNavigation({ currentPage, pages, onSelectPage }) {
4620
5722
  onKeyDown: (event)=>handleKeyDown(event, page),
4621
5723
  onClick: ()=>{
4622
5724
  setSelectedPage(page.physicalPage);
5725
+ setAnchorIndex(entry.index);
4623
5726
  onSelectPage(page);
4624
5727
  },
4625
5728
  children: [
4626
- /*#__PURE__*/ jsx("span", {
4627
- className: `work-document-page-thumbnail ${page.orientation}`,
4628
- "aria-hidden": "true",
4629
- style: {
4630
- backgroundColor: page.backgroundColor
4631
- },
4632
- children: /*#__PURE__*/ jsx("span", {
4633
- children: page.previewText || '空白页'
4634
- })
5729
+ /*#__PURE__*/ jsx(DocumentPageThumbnail, {
5730
+ backgroundColor: page.backgroundColor,
5731
+ fallbackText: page.previewText,
5732
+ orientation: page.orientation,
5733
+ pageIndex: page.physicalPage - 1,
5734
+ priority: Math.abs(page.physicalPage - selectedPage) <= 1,
5735
+ source: thumbnailSource
4635
5736
  }),
4636
5737
  /*#__PURE__*/ jsxs("span", {
4637
5738
  className: "work-document-page-thumbnail-label",
@@ -4657,11 +5758,151 @@ function DocumentPageNavigation({ currentPage, pages, onSelectPage }) {
4657
5758
  })
4658
5759
  });
4659
5760
  }
4660
- function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages = [], onClose }) {
5761
+ function calculateDocumentPageRange({ anchorIndex, averageItemHeight, pageCount, viewportHeight }) {
5762
+ return calculateDocumentNavigationWindowRange({
5763
+ anchorIndex,
5764
+ averageItemHeight,
5765
+ itemCount: pageCount,
5766
+ limit: DOCUMENT_PAGE_WINDOW_LIMIT,
5767
+ overscan: DOCUMENT_PAGE_WINDOW_OVERSCAN,
5768
+ threshold: DOCUMENT_PAGE_WINDOW_THRESHOLD,
5769
+ viewportHeight
5770
+ });
5771
+ }
5772
+ function documentPageIndex(pages, physicalPage) {
5773
+ return pages.findIndex((page)=>page.physicalPage === physicalPage);
5774
+ }
5775
+ function estimatedDocumentPageItemHeight(page, source) {
5776
+ const width = 'landscape' === page.orientation ? DOCUMENT_PAGE_LANDSCAPE_WIDTH : DOCUMENT_PAGE_PORTRAIT_WIDTH;
5777
+ const sourceRatio = source && source.pageWidth > 0 && source.pageHeight > 0 ? source.pageHeight / source.pageWidth : null;
5778
+ const fallbackRatio = 'landscape' === page.orientation ? 210 / 297 : 297 / 210;
5779
+ return Math.ceil(width * (sourceRatio ?? fallbackRatio) + DOCUMENT_PAGE_BUTTON_CHROME_HEIGHT);
5780
+ }
5781
+ function focusDocumentPageButton(element) {
5782
+ element.focus({
5783
+ preventScroll: true
5784
+ });
5785
+ element.scrollIntoView?.({
5786
+ block: 'nearest',
5787
+ inline: 'nearest'
5788
+ });
5789
+ }
5790
+ const DOCUMENT_SEARCH_RESULT_HEIGHT = 56;
5791
+ const DOCUMENT_SEARCH_RESULT_GAP = 5;
5792
+ const DOCUMENT_SEARCH_LIST_PADDING_TOP = 7;
5793
+ const document_search_navigation_DocumentSearchNavigation = /*#__PURE__*/ forwardRef(function({ matches, outline, rovingId, selectedMatchIndex, onRovingIdChange, onSelect }, ref) {
5794
+ const keys = useMemo(()=>matches.map(documentNavigationMatchId), [
5795
+ matches
5796
+ ]);
5797
+ const selectedMatch = matches[selectedMatchIndex];
5798
+ const selectedKey = selectedMatch ? documentNavigationMatchId(selectedMatch) : null;
5799
+ const pinnedKeys = useMemo(()=>[
5800
+ selectedKey
5801
+ ], [
5802
+ selectedKey
5803
+ ]);
5804
+ const navigation = useDocumentNavigationWindow({
5805
+ estimatedItemHeight: DOCUMENT_SEARCH_RESULT_HEIGHT,
5806
+ itemGap: DOCUMENT_SEARCH_RESULT_GAP,
5807
+ keys,
5808
+ listPaddingTop: DOCUMENT_SEARCH_LIST_PADDING_TOP,
5809
+ onRovingKeyChange: onRovingIdChange,
5810
+ pinnedKeys,
5811
+ rovingKey: rovingId
5812
+ });
5813
+ useImperativeHandle(ref, ()=>({
5814
+ focusFirst: ()=>navigation.focusAt(0),
5815
+ focusLast: ()=>navigation.focusAt(matches.length - 1)
5816
+ }), [
5817
+ matches.length,
5818
+ navigation.focusAt
5819
+ ]);
5820
+ const handleResultKeyDown = (event, index)=>{
5821
+ if ('ArrowDown' === event.key) {
5822
+ event.preventDefault();
5823
+ navigation.focusAt(Math.min(matches.length - 1, index + 1));
5824
+ } else if ('ArrowUp' === event.key) {
5825
+ event.preventDefault();
5826
+ navigation.focusAt(Math.max(0, index - 1));
5827
+ } else if ('Home' === event.key) {
5828
+ event.preventDefault();
5829
+ navigation.focusAt(0);
5830
+ } else if ('End' === event.key) {
5831
+ event.preventDefault();
5832
+ navigation.focusAt(matches.length - 1);
5833
+ }
5834
+ };
5835
+ return /*#__PURE__*/ jsx("nav", {
5836
+ ref: navigation.viewportRef,
5837
+ className: "work-document-task-pane-body work-document-search-results",
5838
+ "aria-label": "文档搜索结果",
5839
+ "data-document-navigation-collection": "search",
5840
+ "data-document-navigation-item-count": matches.length,
5841
+ "data-document-navigation-mounted-count": navigation.mountedCount,
5842
+ "data-document-navigation-window-end": navigation.range.end,
5843
+ "data-document-navigation-window-limit": 32,
5844
+ "data-document-navigation-window-start": navigation.range.start,
5845
+ "data-document-navigation-windowed": navigation.range.windowed ? 'true' : 'false',
5846
+ onScroll: navigation.onScroll,
5847
+ children: matches.length ? /*#__PURE__*/ jsx("ol", {
5848
+ children: navigation.entries.map((entry)=>{
5849
+ if ('spacer' === entry.kind) return /*#__PURE__*/ jsx(DocumentNavigationWindowSpacer, {
5850
+ entry: entry
5851
+ }, `spacer-${entry.start}-${entry.end}`);
5852
+ const match = matches[entry.index];
5853
+ if (!match) return null;
5854
+ const resultId = documentNavigationMatchId(match);
5855
+ const section = currentWorkDocumentOutlineItem(outline, match.from)?.text ?? '文档开头';
5856
+ return /*#__PURE__*/ jsx("li", {
5857
+ "aria-posinset": entry.index + 1,
5858
+ "aria-setsize": matches.length,
5859
+ "data-document-navigation-item": entry.index + 1,
5860
+ children: /*#__PURE__*/ jsxs("button", {
5861
+ ref: (element)=>navigation.registerItem(resultId, element),
5862
+ type: "button",
5863
+ className: "work-document-search-result",
5864
+ tabIndex: navigation.rovingIndex === entry.index ? 0 : -1,
5865
+ "aria-label": `第 ${entry.index + 1} 个匹配:${match.matchedText}`,
5866
+ "aria-current": selectedMatchIndex === entry.index ? 'location' : void 0,
5867
+ onFocus: ()=>navigation.onItemFocus(entry.index),
5868
+ onKeyDown: (event)=>handleResultKeyDown(event, entry.index),
5869
+ onClick: ()=>onSelect(match),
5870
+ children: [
5871
+ /*#__PURE__*/ jsx("span", {
5872
+ className: "work-document-search-section",
5873
+ children: section
5874
+ }),
5875
+ /*#__PURE__*/ jsxs("span", {
5876
+ className: "work-document-search-excerpt",
5877
+ children: [
5878
+ match.truncatedBefore && '…',
5879
+ match.before,
5880
+ /*#__PURE__*/ jsx("mark", {
5881
+ children: match.matchedText
5882
+ }),
5883
+ match.after,
5884
+ match.truncatedAfter && '…'
5885
+ ]
5886
+ })
5887
+ ]
5888
+ })
5889
+ }, resultId);
5890
+ })
5891
+ }) : /*#__PURE__*/ jsx("div", {
5892
+ className: "work-document-outline-empty",
5893
+ children: "尝试更短或不同的文字"
5894
+ })
5895
+ });
5896
+ });
5897
+ function documentNavigationMatchId(match) {
5898
+ return `match-${match.from}-${match.to}`;
5899
+ }
5900
+ const instantDocumentScrollStates = new WeakMap();
5901
+ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages = [], pageThumbnailSource, onClose }) {
4661
5902
  const tabsId = useId();
4662
5903
  const searchRef = useRef(null);
4663
- const itemRefs = useRef(new Map());
4664
- const resultRefs = useRef(new Map());
5904
+ const outlineNavigationRef = useRef(null);
5905
+ const searchNavigationRef = useRef(null);
4665
5906
  const [, renderEditorState] = useState(0);
4666
5907
  const [navigationView, setNavigationView] = useState('headings');
4667
5908
  const [query, setQuery] = useState('');
@@ -4732,28 +5973,12 @@ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages
4732
5973
  ]);
4733
5974
  useEffect(()=>{
4734
5975
  if (!normalizedQuery) return;
4735
- const validResultIds = new Set(matches.map(documentMatchId));
4736
- setRovingResultId((current)=>current && validResultIds.has(current) ? current : matches[0] ? documentMatchId(matches[0]) : null);
5976
+ const validResultIds = new Set(matches.map(documentNavigationMatchId));
5977
+ setRovingResultId((current)=>current && validResultIds.has(current) ? current : matches[0] ? documentNavigationMatchId(matches[0]) : null);
4737
5978
  }, [
4738
5979
  matches,
4739
5980
  normalizedQuery
4740
5981
  ]);
4741
- const focusItem = useCallback((itemId)=>{
4742
- setRovingId(itemId);
4743
- requestAnimationFrame(()=>{
4744
- itemRefs.current.get(itemId)?.focus({
4745
- preventScroll: true
4746
- });
4747
- });
4748
- }, []);
4749
- const focusResult = useCallback((resultId)=>{
4750
- setRovingResultId(resultId);
4751
- requestAnimationFrame(()=>{
4752
- resultRefs.current.get(resultId)?.focus({
4753
- preventScroll: true
4754
- });
4755
- });
4756
- }, []);
4757
5982
  const toggleCollapsed = useCallback((itemId)=>{
4758
5983
  setCollapsedIds((current)=>{
4759
5984
  const next = new Set(current);
@@ -4762,45 +5987,15 @@ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages
4762
5987
  return next;
4763
5988
  });
4764
5989
  }, []);
4765
- const handleItemKeyDown = (event, item)=>{
4766
- const index = visibleItems.findIndex((candidate)=>candidate.id === item.id);
4767
- const focusAt = (requestedIndex)=>{
4768
- const next = visibleItems[requestedIndex];
4769
- if (next) focusItem(next.id);
4770
- };
4771
- if ('ArrowDown' === event.key) {
4772
- event.preventDefault();
4773
- focusAt(Math.min(visibleItems.length - 1, index + 1));
4774
- } else if ('ArrowUp' === event.key) {
4775
- event.preventDefault();
4776
- focusAt(Math.max(0, index - 1));
4777
- } else if ('Home' === event.key) {
4778
- event.preventDefault();
4779
- focusAt(0);
4780
- } else if ('End' === event.key) {
4781
- event.preventDefault();
4782
- focusAt(visibleItems.length - 1);
4783
- } else if ('ArrowRight' === event.key && !normalizedQuery && item.hasChildren && collapsedIds.has(item.id)) {
4784
- event.preventDefault();
4785
- toggleCollapsed(item.id);
4786
- } else if ('ArrowRight' === event.key && !normalizedQuery && item.hasChildren) {
4787
- event.preventDefault();
4788
- const child = visibleItems[index + 1];
4789
- if (child?.depth > item.depth) focusItem(child.id);
4790
- } else if ('ArrowLeft' === event.key && !normalizedQuery) {
4791
- event.preventDefault();
4792
- if (item.hasChildren && !collapsedIds.has(item.id)) toggleCollapsed(item.id);
4793
- else if (item.parentId) focusItem(item.parentId);
4794
- }
4795
- };
4796
- const navigateTo = async (selection)=>{
5990
+ const navigateTo = async (selection, focusEditor = true)=>{
5991
+ const resolvedSelection = 'number' == typeof selection ? TextSelection.near(editor.state.doc.resolve(Math.min(editor.state.doc.content.size, Math.max(0, selection))), 1).from : selection;
4797
5992
  const chain = editor.chain();
4798
- if (!modal) chain.focus();
4799
- chain.setTextSelection(selection).scrollIntoView().run();
5993
+ if (!modal && focusEditor) chain.focus();
5994
+ runWithInstantDocumentScroll(editor, ()=>chain.setTextSelection(resolvedSelection).scrollIntoView().run());
4800
5995
  if (!modal) return;
4801
5996
  await onClose();
4802
5997
  requestAnimationFrame(()=>{
4803
- if (!editor.isDestroyed) editor.chain().focus().scrollIntoView().run();
5998
+ if (!editor.isDestroyed) runWithInstantDocumentScroll(editor, ()=>editor.chain().focus().scrollIntoView().run());
4804
5999
  });
4805
6000
  };
4806
6001
  const selectMatch = (match)=>{
@@ -4809,26 +6004,6 @@ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages
4809
6004
  to: match.to
4810
6005
  });
4811
6006
  };
4812
- const handleResultKeyDown = (event, match)=>{
4813
- const index = matches.findIndex((candidate)=>documentMatchId(candidate) === documentMatchId(match));
4814
- const focusAt = (requestedIndex)=>{
4815
- const next = matches[requestedIndex];
4816
- if (next) focusResult(documentMatchId(next));
4817
- };
4818
- if ('ArrowDown' === event.key) {
4819
- event.preventDefault();
4820
- focusAt(Math.min(matches.length - 1, index + 1));
4821
- } else if ('ArrowUp' === event.key) {
4822
- event.preventDefault();
4823
- focusAt(Math.max(0, index - 1));
4824
- } else if ('Home' === event.key) {
4825
- event.preventDefault();
4826
- focusAt(0);
4827
- } else if ('End' === event.key) {
4828
- event.preventDefault();
4829
- focusAt(matches.length - 1);
4830
- }
4831
- };
4832
6007
  return /*#__PURE__*/ jsxs(DocumentTaskPane, {
4833
6008
  ariaLabel: "文档导航",
4834
6009
  className: "work-document-navigation-panel",
@@ -4873,7 +6048,8 @@ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages
4873
6048
  /*#__PURE__*/ jsx(DocumentPageNavigation, {
4874
6049
  currentPage: currentPage,
4875
6050
  pages: pages,
4876
- onSelectPage: (page)=>navigateTo(page.selectionPosition)
6051
+ thumbnailSource: pageThumbnailSource,
6052
+ onSelectPage: (page)=>navigateTo(page.selectionPosition, false)
4877
6053
  })
4878
6054
  ]
4879
6055
  }) : /*#__PURE__*/ jsxs("div", {
@@ -4901,11 +6077,10 @@ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages
4901
6077
  event.preventDefault();
4902
6078
  selectMatch(matches[0]);
4903
6079
  } else if ('ArrowDown' === event.key) {
4904
- const firstTarget = normalizedQuery ? matches[0] ? documentMatchId(matches[0]) : null : visibleItems[0]?.id;
4905
- if (!firstTarget) return;
6080
+ const target = normalizedQuery ? searchNavigationRef.current : outlineNavigationRef.current;
6081
+ if (!target) return;
4906
6082
  event.preventDefault();
4907
- if (normalizedQuery) focusResult(firstTarget);
4908
- else focusItem(firstTarget);
6083
+ target.focusFirst();
4909
6084
  }
4910
6085
  }
4911
6086
  })
@@ -4916,112 +6091,58 @@ function DocumentNavigationPanel({ currentPage = 1, editor, modal = false, pages
4916
6091
  "aria-live": "polite",
4917
6092
  children: normalizedQuery ? matches.length ? `${matches.length} 个匹配` : '没有匹配内容' : `${outline.length} 个标题`
4918
6093
  }),
4919
- normalizedQuery ? /*#__PURE__*/ jsx("nav", {
4920
- className: "work-document-task-pane-body work-document-search-results",
4921
- "aria-label": "文档搜索结果",
4922
- children: matches.length ? /*#__PURE__*/ jsx("ol", {
4923
- children: matches.map((match, index)=>{
4924
- const resultId = documentMatchId(match);
4925
- const section = currentWorkDocumentOutlineItem(outline, match.from)?.text ?? '文档开头';
4926
- return /*#__PURE__*/ jsx("li", {
4927
- children: /*#__PURE__*/ jsxs("button", {
4928
- ref: (element)=>{
4929
- if (element) resultRefs.current.set(resultId, element);
4930
- else resultRefs.current.delete(resultId);
4931
- },
4932
- type: "button",
4933
- className: "work-document-search-result",
4934
- tabIndex: resultId === rovingResultId ? 0 : -1,
4935
- "aria-label": `第 ${index + 1} 个匹配:${match.matchedText}`,
4936
- "aria-current": selectedMatchIndex === index ? 'location' : void 0,
4937
- onFocus: ()=>setRovingResultId(resultId),
4938
- onKeyDown: (event)=>handleResultKeyDown(event, match),
4939
- onClick: ()=>selectMatch(match),
4940
- children: [
4941
- /*#__PURE__*/ jsx("span", {
4942
- className: "work-document-search-section",
4943
- children: section
4944
- }),
4945
- /*#__PURE__*/ jsxs("span", {
4946
- className: "work-document-search-excerpt",
4947
- children: [
4948
- match.truncatedBefore && '…',
4949
- match.before,
4950
- /*#__PURE__*/ jsx("mark", {
4951
- children: match.matchedText
4952
- }),
4953
- match.after,
4954
- match.truncatedAfter && '…'
4955
- ]
4956
- })
4957
- ]
4958
- })
4959
- }, resultId);
4960
- })
4961
- }) : /*#__PURE__*/ jsx("div", {
4962
- className: "work-document-outline-empty",
4963
- children: "尝试更短或不同的文字"
4964
- })
4965
- }) : /*#__PURE__*/ jsx("nav", {
4966
- className: "work-document-task-pane-body work-document-outline",
4967
- "aria-label": "文档标题",
4968
- children: visibleItems.length ? /*#__PURE__*/ jsx("ol", {
4969
- children: visibleItems.map((item)=>{
4970
- const collapsed = collapsedIds.has(item.id);
4971
- return /*#__PURE__*/ jsxs("li", {
4972
- className: activeItem?.id === item.id ? 'active' : void 0,
4973
- style: {
4974
- '--work-document-outline-indent': `${14 * item.depth}px`
4975
- },
4976
- children: [
4977
- item.hasChildren ? /*#__PURE__*/ jsx("button", {
4978
- type: "button",
4979
- tabIndex: -1,
4980
- className: "work-document-outline-toggle",
4981
- "aria-label": `${collapsed ? '展开' : '折叠'} ${item.text}`,
4982
- title: collapsed ? '展开下级标题' : '折叠下级标题',
4983
- onClick: ()=>toggleCollapsed(item.id),
4984
- children: collapsed ? /*#__PURE__*/ jsx(ChevronRight, {
4985
- size: 13
4986
- }) : /*#__PURE__*/ jsx(ChevronDown, {
4987
- size: 13
4988
- })
4989
- }) : /*#__PURE__*/ jsx("span", {
4990
- className: "work-document-outline-toggle-spacer"
4991
- }),
4992
- /*#__PURE__*/ jsx("button", {
4993
- ref: (element)=>{
4994
- if (element) itemRefs.current.set(item.id, element);
4995
- else itemRefs.current.delete(item.id);
4996
- },
4997
- type: "button",
4998
- className: "work-document-outline-item",
4999
- tabIndex: item.id === rovingId ? 0 : -1,
5000
- "aria-current": activeItem?.id === item.id ? 'location' : void 0,
5001
- "aria-expanded": item.hasChildren ? !collapsed : void 0,
5002
- title: item.text,
5003
- onFocus: ()=>setRovingId(item.id),
5004
- onKeyDown: (event)=>handleItemKeyDown(event, item),
5005
- onClick: ()=>{
5006
- navigateTo(item.from);
5007
- },
5008
- children: item.text
5009
- })
5010
- ]
5011
- }, item.id);
5012
- })
5013
- }) : /*#__PURE__*/ jsx("div", {
5014
- className: "work-document-outline-empty",
5015
- children: "应用标题样式后,可在这里快速跳转"
5016
- })
6094
+ normalizedQuery ? /*#__PURE__*/ jsx(document_search_navigation_DocumentSearchNavigation, {
6095
+ ref: searchNavigationRef,
6096
+ matches: matches,
6097
+ outline: outline,
6098
+ rovingId: rovingResultId,
6099
+ selectedMatchIndex: selectedMatchIndex,
6100
+ onRovingIdChange: setRovingResultId,
6101
+ onSelect: selectMatch
6102
+ }) : /*#__PURE__*/ jsx(document_outline_navigation_DocumentOutlineNavigation, {
6103
+ ref: outlineNavigationRef,
6104
+ activeItemId: activeItem?.id ?? null,
6105
+ collapsedIds: collapsedIds,
6106
+ items: visibleItems,
6107
+ rovingId: rovingId,
6108
+ onNavigate: (position)=>void navigateTo(position),
6109
+ onRovingIdChange: setRovingId,
6110
+ onToggleCollapsed: toggleCollapsed
5017
6111
  })
5018
6112
  ]
5019
6113
  })
5020
6114
  ]
5021
6115
  });
5022
6116
  }
5023
- function documentMatchId(match) {
5024
- return `match-${match.from}-${match.to}`;
6117
+ function runWithInstantDocumentScroll(editor, run) {
6118
+ const scrollSurface = editor.view.dom.closest('.work-document-scroll');
6119
+ if (!scrollSurface) return run();
6120
+ let state = instantDocumentScrollStates.get(scrollSurface);
6121
+ if (state) {
6122
+ if (null !== state.restoreFrame && 'function' == typeof cancelAnimationFrame) {
6123
+ cancelAnimationFrame(state.restoreFrame);
6124
+ state.restoreFrame = null;
6125
+ }
6126
+ } else {
6127
+ state = {
6128
+ previousBehavior: scrollSurface.style.scrollBehavior,
6129
+ restoreFrame: null
6130
+ };
6131
+ instantDocumentScrollStates.set(scrollSurface, state);
6132
+ }
6133
+ scrollSurface.style.scrollBehavior = 'auto';
6134
+ scrollSurface.getBoundingClientRect();
6135
+ try {
6136
+ return run();
6137
+ } finally{
6138
+ const restore = ()=>{
6139
+ if (instantDocumentScrollStates.get(scrollSurface) !== state) return;
6140
+ scrollSurface.style.scrollBehavior = state.previousBehavior;
6141
+ instantDocumentScrollStates.delete(scrollSurface);
6142
+ };
6143
+ if ('function' == typeof requestAnimationFrame) state.restoreFrame = requestAnimationFrame(restore);
6144
+ else restore();
6145
+ }
5025
6146
  }
5026
6147
  function DocumentPageStack({ pageColor, pageCount, pageGap, pageHeight }) {
5027
6148
  const count = Math.max(1, Math.trunc(pageCount));
@@ -6421,7 +7542,11 @@ function runDocumentListMenuCommand(editor, close, command) {
6421
7542
  const handled = command();
6422
7543
  if (!handled) return false;
6423
7544
  close();
6424
- editor.commands.focus();
7545
+ queueMicrotask(()=>{
7546
+ if (!editor.isDestroyed) editor.commands.focus(void 0, {
7547
+ scrollIntoView: false
7548
+ });
7549
+ });
6425
7550
  return true;
6426
7551
  }
6427
7552
  function handleGalleryKeyDown(event, index, count, columns, refs, setFocusIndex, select) {
@@ -7439,11 +8564,457 @@ function documentPaginationSnapshot(editor) {
7439
8564
  }
7440
8565
  });
7441
8566
  }
7442
- const imageWrapDistanceOptions = [
7443
- {
7444
- value: '0',
7445
- label: '无间距'
7446
- },
8567
+ const IMAGE_PIXELS_PER_CENTIMETER = 96 / 2.54;
8568
+ const FALLBACK_IMAGE_WIDTH_PIXELS = 320;
8569
+ const FALLBACK_IMAGE_HEIGHT_PIXELS = 180;
8570
+ const MINIMUM_IMAGE_SIZE_CENTIMETERS = 0.01;
8571
+ const MAXIMUM_IMAGE_SIZE_CENTIMETERS = 55.87;
8572
+ const MAXIMUM_WRAP_DISTANCE_MILLIMETERS = 25;
8573
+ function createDocumentPicturePropertiesDraft(source) {
8574
+ const width = effectiveDimension(source.properties.width, source.renderedWidth, FALLBACK_IMAGE_WIDTH_PIXELS);
8575
+ const height = effectiveDimension(source.properties.height, source.renderedHeight, FALLBACK_IMAGE_HEIGHT_PIXELS);
8576
+ return {
8577
+ width: document_picture_properties_dialog_model_centimeters(width),
8578
+ height: document_picture_properties_dialog_model_centimeters(height),
8579
+ lockAspectRatio: source.properties.lockAspectRatio,
8580
+ aspectRatio: validAspectRatio(width, height),
8581
+ layout: source.properties.layout,
8582
+ alignment: source.properties.alignment,
8583
+ wrapDistance: formatNumber(source.properties.wrapDistance),
8584
+ alternativeText: source.properties.alternativeText
8585
+ };
8586
+ }
8587
+ function withDocumentPictureDimension(draft, dimension, value) {
8588
+ const next = {
8589
+ ...draft,
8590
+ [dimension]: value
8591
+ };
8592
+ const numeric = numericDraft(value);
8593
+ if (!draft.lockAspectRatio || null === numeric) return next;
8594
+ if ('width' === dimension) return {
8595
+ ...next,
8596
+ height: formatNumber(numeric / draft.aspectRatio)
8597
+ };
8598
+ return {
8599
+ ...next,
8600
+ width: formatNumber(numeric * draft.aspectRatio)
8601
+ };
8602
+ }
8603
+ function withDocumentPictureAspectRatioLock(draft, lockAspectRatio) {
8604
+ if (!lockAspectRatio) return {
8605
+ ...draft,
8606
+ lockAspectRatio: false
8607
+ };
8608
+ const width = numericDraft(draft.width);
8609
+ const height = numericDraft(draft.height);
8610
+ return {
8611
+ ...draft,
8612
+ lockAspectRatio: true,
8613
+ aspectRatio: null !== width && null !== height ? validAspectRatio(width, height) : draft.aspectRatio
8614
+ };
8615
+ }
8616
+ function documentPicturePropertiesErrors(draft) {
8617
+ return {
8618
+ width: imageDimensionError(draft.width),
8619
+ height: imageDimensionError(draft.height),
8620
+ wrapDistance: 'inline' === draft.layout || validNumber(draft.wrapDistance, 0, MAXIMUM_WRAP_DISTANCE_MILLIMETERS) ? null : '请输入 0 到 25 之间的毫米数。'
8621
+ };
8622
+ }
8623
+ function hasDocumentPicturePropertiesErrors(errors) {
8624
+ return Boolean(errors.width || errors.height || errors.wrapDistance);
8625
+ }
8626
+ function documentPicturePropertyChanges(initial, current) {
8627
+ const errors = documentPicturePropertiesErrors(current);
8628
+ if (hasDocumentPicturePropertiesErrors(errors)) return null;
8629
+ const changes = {};
8630
+ if (!sameDraftNumber(initial.width, current.width)) changes.width = rounded(Number(current.width) * IMAGE_PIXELS_PER_CENTIMETER);
8631
+ if (!sameDraftNumber(initial.height, current.height)) changes.height = rounded(Number(current.height) * IMAGE_PIXELS_PER_CENTIMETER);
8632
+ if (initial.lockAspectRatio !== current.lockAspectRatio) changes.lockAspectRatio = current.lockAspectRatio;
8633
+ if (initial.layout !== current.layout) changes.layout = current.layout;
8634
+ if (initial.alignment !== current.alignment) changes.alignment = current.alignment;
8635
+ if (!sameDraftNumber(initial.wrapDistance, current.wrapDistance)) changes.wrapDistance = Number(current.wrapDistance);
8636
+ const alternativeText = current.alternativeText.trim();
8637
+ if (initial.alternativeText.trim() !== alternativeText) changes.alternativeText = alternativeText;
8638
+ return Object.keys(changes).length ? changes : null;
8639
+ }
8640
+ function effectiveDimension(explicit, rendered, fallback) {
8641
+ if (null !== explicit && Number.isFinite(explicit) && explicit > 0) return explicit;
8642
+ if (void 0 !== rendered && Number.isFinite(rendered) && rendered > 0) return rendered;
8643
+ return fallback;
8644
+ }
8645
+ function imageDimensionError(value) {
8646
+ return validNumber(value, MINIMUM_IMAGE_SIZE_CENTIMETERS, MAXIMUM_IMAGE_SIZE_CENTIMETERS) ? null : '请输入 0.01 到 55.87 之间的厘米数。';
8647
+ }
8648
+ function validNumber(value, minimum, maximum) {
8649
+ const number = numericDraft(value);
8650
+ return null !== number && number >= minimum && number <= maximum;
8651
+ }
8652
+ function numericDraft(value) {
8653
+ if (!value.trim()) return null;
8654
+ const number = Number(value);
8655
+ return Number.isFinite(number) && number >= 0 ? number : null;
8656
+ }
8657
+ function sameDraftNumber(left, right) {
8658
+ if (!left.trim() || !right.trim()) return left === right;
8659
+ return Number(left) === Number(right);
8660
+ }
8661
+ function validAspectRatio(width, height) {
8662
+ const ratio = width / height;
8663
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
8664
+ }
8665
+ function document_picture_properties_dialog_model_centimeters(pixels) {
8666
+ return formatNumber(pixels / IMAGE_PIXELS_PER_CENTIMETER);
8667
+ }
8668
+ function formatNumber(value) {
8669
+ return Number.isFinite(value) ? value.toFixed(2).replace(/\.?0+$/, '') : '';
8670
+ }
8671
+ function rounded(value) {
8672
+ return Math.round(100 * value) / 100;
8673
+ }
8674
+ const document_picture_properties_dialog_layoutOptions = [
8675
+ {
8676
+ value: 'inline',
8677
+ label: '嵌入文字',
8678
+ icon: Rows3
8679
+ },
8680
+ {
8681
+ value: 'square',
8682
+ label: '四周环绕',
8683
+ icon: TextWrap
8684
+ },
8685
+ {
8686
+ value: 'topBottom',
8687
+ label: '上下环绕',
8688
+ icon: Rows3
8689
+ }
8690
+ ];
8691
+ const alignmentOptions = [
8692
+ {
8693
+ value: 'left',
8694
+ label: '左对齐',
8695
+ icon: AlignLeft
8696
+ },
8697
+ {
8698
+ value: 'center',
8699
+ label: '居中',
8700
+ icon: AlignCenter
8701
+ },
8702
+ {
8703
+ value: 'right',
8704
+ label: '右对齐',
8705
+ icon: AlignRight
8706
+ }
8707
+ ];
8708
+ function DocumentPicturePropertiesControl({ editor }) {
8709
+ const [source, setSource] = useState(null);
8710
+ const triggerRef = useRef(null);
8711
+ const openDialog = (event)=>{
8712
+ const position = selectedDocumentImagePosition(editor);
8713
+ if (null === position) return;
8714
+ const rendered = selectedDocumentImageDimensions(editor, position);
8715
+ triggerRef.current = event.currentTarget;
8716
+ setSource({
8717
+ position,
8718
+ properties: documentImageProperties(editor),
8719
+ renderedWidth: rendered?.width,
8720
+ renderedHeight: rendered?.height
8721
+ });
8722
+ };
8723
+ return /*#__PURE__*/ jsxs(Fragment, {
8724
+ children: [
8725
+ /*#__PURE__*/ jsx(WorkOfficeRibbonButton, {
8726
+ label: "图片属性",
8727
+ visibleLabel: "图片属性",
8728
+ disabled: !editor.isActive('image'),
8729
+ onClick: openDialog,
8730
+ children: /*#__PURE__*/ jsx(Settings2, {
8731
+ size: 18
8732
+ })
8733
+ }),
8734
+ source && /*#__PURE__*/ jsx(DocumentPicturePropertiesDialog, {
8735
+ editor: editor,
8736
+ source: source,
8737
+ restoreFocusTarget: ()=>triggerRef.current,
8738
+ onClose: ()=>setSource(null)
8739
+ })
8740
+ ]
8741
+ });
8742
+ }
8743
+ function DocumentPicturePropertiesDialog({ editor, source, restoreFocusTarget, onClose }) {
8744
+ const [initial] = useState(()=>createDocumentPicturePropertiesDraft(source));
8745
+ const [draft, setDraft] = useState(initial);
8746
+ const formId = useId();
8747
+ const alternativeTextId = useId();
8748
+ const errors = documentPicturePropertiesErrors(draft);
8749
+ const invalid = hasDocumentPicturePropertiesErrors(errors);
8750
+ const submit = (event)=>{
8751
+ event?.preventDefault();
8752
+ if (invalid) return;
8753
+ const changes = documentPicturePropertyChanges(initial, draft);
8754
+ if (!changes) return void onClose();
8755
+ if (editor.state.doc.nodeAt(source.position)?.type.name !== 'image') return void onClose();
8756
+ const applied = editor.chain().setNodeSelection(source.position).setDocumentImageProperties(changes, {
8757
+ restoreFocus: false
8758
+ }).run();
8759
+ if (applied) onClose();
8760
+ };
8761
+ return /*#__PURE__*/ jsx(Dialog, {
8762
+ title: "图片属性",
8763
+ description: "调整当前图片的大小、排列方式和辅助说明。",
8764
+ className: "work-document-picture-properties-dialog",
8765
+ restoreFocusTarget: restoreFocusTarget,
8766
+ onClose: onClose,
8767
+ footer: /*#__PURE__*/ jsxs(Fragment, {
8768
+ children: [
8769
+ /*#__PURE__*/ jsx(button_Button, {
8770
+ tone: "quiet",
8771
+ onClick: onClose,
8772
+ children: "取消"
8773
+ }),
8774
+ /*#__PURE__*/ jsx(button_Button, {
8775
+ tone: "primary",
8776
+ type: "submit",
8777
+ form: formId,
8778
+ disabled: invalid,
8779
+ children: "确定"
8780
+ })
8781
+ ]
8782
+ }),
8783
+ children: /*#__PURE__*/ jsxs("form", {
8784
+ id: formId,
8785
+ onSubmit: submit,
8786
+ children: [
8787
+ /*#__PURE__*/ jsx(PictureSizeSection, {
8788
+ draft: draft,
8789
+ errors: errors,
8790
+ onDraftChange: setDraft
8791
+ }),
8792
+ /*#__PURE__*/ jsx(PictureLayoutSection, {
8793
+ draft: draft,
8794
+ onDraftChange: setDraft
8795
+ }),
8796
+ /*#__PURE__*/ jsx(PictureAlignmentSection, {
8797
+ draft: draft,
8798
+ onDraftChange: setDraft
8799
+ }),
8800
+ /*#__PURE__*/ jsxs("label", {
8801
+ className: "work-document-picture-properties-alt-text",
8802
+ htmlFor: alternativeTextId,
8803
+ children: [
8804
+ /*#__PURE__*/ jsx("span", {
8805
+ children: "替代文字"
8806
+ }),
8807
+ /*#__PURE__*/ jsx(office_text_field_OfficeTextArea, {
8808
+ id: alternativeTextId,
8809
+ "aria-label": "图片替代文字",
8810
+ value: draft.alternativeText,
8811
+ maxLength: 512,
8812
+ placeholder: "描述图片中的关键信息;装饰性图片可留空",
8813
+ onChange: (event)=>{
8814
+ const alternativeText = event.currentTarget.value;
8815
+ setDraft((current)=>({
8816
+ ...current,
8817
+ alternativeText
8818
+ }));
8819
+ }
8820
+ }),
8821
+ /*#__PURE__*/ jsx("small", {
8822
+ children: "供屏幕阅读器使用,不会显示在正文中。"
8823
+ })
8824
+ ]
8825
+ })
8826
+ ]
8827
+ })
8828
+ });
8829
+ }
8830
+ function PictureSizeSection({ draft, errors, onDraftChange }) {
8831
+ return /*#__PURE__*/ jsxs("fieldset", {
8832
+ className: "work-document-picture-properties-section size",
8833
+ children: [
8834
+ /*#__PURE__*/ jsx("legend", {
8835
+ children: "大小"
8836
+ }),
8837
+ /*#__PURE__*/ jsx(PictureNumberRow, {
8838
+ label: "宽度",
8839
+ ariaLabel: "图片宽度(厘米)",
8840
+ value: draft.width,
8841
+ unit: "厘米",
8842
+ min: 0.01,
8843
+ max: 55.87,
8844
+ step: 0.1,
8845
+ invalid: Boolean(errors.width),
8846
+ onValueChange: (width)=>onDraftChange((current)=>withDocumentPictureDimension(current, 'width', width))
8847
+ }),
8848
+ errors.width && /*#__PURE__*/ jsx("p", {
8849
+ role: "alert",
8850
+ children: errors.width
8851
+ }),
8852
+ /*#__PURE__*/ jsx(PictureNumberRow, {
8853
+ label: "高度",
8854
+ ariaLabel: "图片高度(厘米)",
8855
+ value: draft.height,
8856
+ unit: "厘米",
8857
+ min: 0.01,
8858
+ max: 55.87,
8859
+ step: 0.1,
8860
+ invalid: Boolean(errors.height),
8861
+ onValueChange: (height)=>onDraftChange((current)=>withDocumentPictureDimension(current, 'height', height))
8862
+ }),
8863
+ errors.height && /*#__PURE__*/ jsx("p", {
8864
+ role: "alert",
8865
+ children: errors.height
8866
+ }),
8867
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
8868
+ ariaLabel: "锁定纵横比",
8869
+ checked: draft.lockAspectRatio,
8870
+ onCheckedChange: (locked)=>onDraftChange((current)=>withDocumentPictureAspectRatioLock(current, locked)),
8871
+ children: "锁定纵横比"
8872
+ })
8873
+ ]
8874
+ });
8875
+ }
8876
+ function PictureLayoutSection({ draft, onDraftChange }) {
8877
+ const errors = documentPicturePropertiesErrors(draft);
8878
+ return /*#__PURE__*/ jsxs("fieldset", {
8879
+ className: "work-document-picture-properties-section layout",
8880
+ children: [
8881
+ /*#__PURE__*/ jsx("legend", {
8882
+ children: "文字环绕"
8883
+ }),
8884
+ /*#__PURE__*/ jsx("div", {
8885
+ className: "work-document-picture-properties-choice-grid",
8886
+ children: document_picture_properties_dialog_layoutOptions.map((option)=>{
8887
+ const Icon = option.icon;
8888
+ return /*#__PURE__*/ jsxs("label", {
8889
+ children: [
8890
+ /*#__PURE__*/ jsx("input", {
8891
+ type: "radio",
8892
+ name: "picture-properties-layout",
8893
+ value: option.value,
8894
+ checked: draft.layout === option.value,
8895
+ onChange: ()=>onDraftChange((current)=>({
8896
+ ...current,
8897
+ layout: option.value
8898
+ }))
8899
+ }),
8900
+ /*#__PURE__*/ jsxs("span", {
8901
+ children: [
8902
+ /*#__PURE__*/ jsx(Icon, {
8903
+ size: 16,
8904
+ "aria-hidden": "true"
8905
+ }),
8906
+ option.label
8907
+ ]
8908
+ })
8909
+ ]
8910
+ }, option.value);
8911
+ })
8912
+ }),
8913
+ /*#__PURE__*/ jsx(PictureNumberRow, {
8914
+ label: "文字距离",
8915
+ ariaLabel: "图片与文字距离(毫米)",
8916
+ value: draft.wrapDistance,
8917
+ unit: "毫米",
8918
+ min: 0,
8919
+ max: 25,
8920
+ step: 0.5,
8921
+ disabled: 'inline' === draft.layout,
8922
+ invalid: Boolean(errors.wrapDistance),
8923
+ onValueChange: (wrapDistance)=>onDraftChange((current)=>({
8924
+ ...current,
8925
+ wrapDistance
8926
+ }))
8927
+ }),
8928
+ errors.wrapDistance && /*#__PURE__*/ jsx("p", {
8929
+ role: "alert",
8930
+ children: errors.wrapDistance
8931
+ })
8932
+ ]
8933
+ });
8934
+ }
8935
+ function PictureAlignmentSection({ draft, onDraftChange }) {
8936
+ return /*#__PURE__*/ jsxs("fieldset", {
8937
+ className: "work-document-picture-properties-section position",
8938
+ children: [
8939
+ /*#__PURE__*/ jsx("legend", {
8940
+ children: "位置"
8941
+ }),
8942
+ /*#__PURE__*/ jsx("div", {
8943
+ className: "work-document-picture-properties-choice-grid",
8944
+ children: alignmentOptions.map((option)=>{
8945
+ const Icon = option.icon;
8946
+ return /*#__PURE__*/ jsxs("label", {
8947
+ children: [
8948
+ /*#__PURE__*/ jsx("input", {
8949
+ type: "radio",
8950
+ name: "picture-properties-alignment",
8951
+ value: option.value,
8952
+ checked: draft.alignment === option.value,
8953
+ onChange: ()=>onDraftChange((current)=>({
8954
+ ...current,
8955
+ alignment: option.value
8956
+ }))
8957
+ }),
8958
+ /*#__PURE__*/ jsxs("span", {
8959
+ children: [
8960
+ /*#__PURE__*/ jsx(Icon, {
8961
+ size: 16,
8962
+ "aria-hidden": "true"
8963
+ }),
8964
+ option.label
8965
+ ]
8966
+ })
8967
+ ]
8968
+ }, option.value);
8969
+ })
8970
+ })
8971
+ ]
8972
+ });
8973
+ }
8974
+ function PictureNumberRow({ label, ariaLabel, value, unit, min, max, step, disabled = false, invalid, onValueChange }) {
8975
+ return /*#__PURE__*/ jsxs("div", {
8976
+ className: "work-document-picture-properties-number-row",
8977
+ children: [
8978
+ /*#__PURE__*/ jsx("span", {
8979
+ children: label
8980
+ }),
8981
+ /*#__PURE__*/ jsx(OfficeNumberField, {
8982
+ ariaLabel: ariaLabel,
8983
+ value: value,
8984
+ min: min,
8985
+ max: max,
8986
+ step: step,
8987
+ disabled: disabled,
8988
+ validationInvalid: invalid,
8989
+ onValueChange: onValueChange
8990
+ }),
8991
+ /*#__PURE__*/ jsx("small", {
8992
+ children: unit
8993
+ })
8994
+ ]
8995
+ });
8996
+ }
8997
+ function selectedDocumentImagePosition(editor) {
8998
+ const selection = editor.state.selection;
8999
+ return selection instanceof NodeSelection && 'image' === selection.node.type.name ? selection.from : null;
9000
+ }
9001
+ function selectedDocumentImageDimensions(editor, position) {
9002
+ const node = editor.view.nodeDOM(position);
9003
+ const image = node instanceof HTMLImageElement ? node : node instanceof HTMLElement ? node.querySelector('img') : null;
9004
+ if (!(image instanceof HTMLImageElement)) return null;
9005
+ const bounds = image.getBoundingClientRect();
9006
+ const width = bounds.width || image.width || image.naturalWidth;
9007
+ const height = bounds.height || image.height || image.naturalHeight;
9008
+ return width > 0 && height > 0 ? {
9009
+ width,
9010
+ height
9011
+ } : null;
9012
+ }
9013
+ const imageWrapDistanceOptions = [
9014
+ {
9015
+ value: '0',
9016
+ label: '无间距'
9017
+ },
7447
9018
  {
7448
9019
  value: '2',
7449
9020
  label: '2 毫米'
@@ -7465,7 +9036,6 @@ function DocumentPictureRibbon({ editor }) {
7465
9036
  const image = documentImageLayoutOptions(editor);
7466
9037
  const imageSelected = editor.isActive('image');
7467
9038
  const wrapDistanceValue = String(image.wrapDistance);
7468
- const officeDialog = useOfficeDialog();
7469
9039
  const updateLayout = (layout)=>editor.commands.setDocumentImageLayoutOptions({
7470
9040
  layout
7471
9041
  });
@@ -7486,143 +9056,1034 @@ function DocumentPictureRibbon({ editor }) {
7486
9056
  size: 18
7487
9057
  })
7488
9058
  }),
7489
- /*#__PURE__*/ jsx(PictureButton, {
7490
- label: "四周环绕",
7491
- active: 'square' === image.layout,
7492
- disabled: !imageSelected,
7493
- onClick: ()=>updateLayout('square'),
7494
- children: /*#__PURE__*/ jsx(TextWrap, {
7495
- size: 18
7496
- })
9059
+ /*#__PURE__*/ jsx(PictureButton, {
9060
+ label: "四周环绕",
9061
+ active: 'square' === image.layout,
9062
+ disabled: !imageSelected,
9063
+ onClick: ()=>updateLayout('square'),
9064
+ children: /*#__PURE__*/ jsx(TextWrap, {
9065
+ size: 18
9066
+ })
9067
+ }),
9068
+ /*#__PURE__*/ jsx(PictureButton, {
9069
+ label: "上下环绕",
9070
+ active: 'topBottom' === image.layout,
9071
+ disabled: !imageSelected,
9072
+ onClick: ()=>updateLayout('topBottom'),
9073
+ children: /*#__PURE__*/ jsx(Rows3, {
9074
+ size: 18
9075
+ })
9076
+ })
9077
+ ]
9078
+ }),
9079
+ /*#__PURE__*/ jsxs(WorkOfficeRibbonGroup, {
9080
+ label: "位置",
9081
+ children: [
9082
+ /*#__PURE__*/ jsx(PictureButton, {
9083
+ label: "左对齐",
9084
+ active: 'left' === image.alignment,
9085
+ disabled: !imageSelected,
9086
+ onClick: ()=>updateAlignment('left'),
9087
+ children: /*#__PURE__*/ jsx(AlignLeft, {
9088
+ size: 18
9089
+ })
9090
+ }),
9091
+ /*#__PURE__*/ jsx(PictureButton, {
9092
+ label: "居中",
9093
+ active: 'center' === image.alignment,
9094
+ disabled: !imageSelected,
9095
+ onClick: ()=>updateAlignment('center'),
9096
+ children: /*#__PURE__*/ jsx(AlignCenter, {
9097
+ size: 18
9098
+ })
9099
+ }),
9100
+ /*#__PURE__*/ jsx(PictureButton, {
9101
+ label: "右对齐",
9102
+ active: 'right' === image.alignment,
9103
+ disabled: !imageSelected,
9104
+ onClick: ()=>updateAlignment('right'),
9105
+ children: /*#__PURE__*/ jsx(AlignRight, {
9106
+ size: 18
9107
+ })
9108
+ })
9109
+ ]
9110
+ }),
9111
+ /*#__PURE__*/ jsx(WorkOfficeRibbonGroup, {
9112
+ label: "与文字距离",
9113
+ children: /*#__PURE__*/ jsx(OfficeSelect, {
9114
+ ariaLabel: "图片与文字距离",
9115
+ value: wrapDistanceValue,
9116
+ options: imageWrapDistanceOptionsForValue(wrapDistanceValue),
9117
+ disabled: !imageSelected || 'inline' === image.layout,
9118
+ onValueChange: (value)=>editor.commands.setDocumentImageLayoutOptions({
9119
+ wrapDistance: Number(value)
9120
+ })
9121
+ })
9122
+ }),
9123
+ /*#__PURE__*/ jsxs(WorkOfficeRibbonGroup, {
9124
+ label: "图片",
9125
+ children: [
9126
+ /*#__PURE__*/ jsx(DocumentPicturePropertiesControl, {
9127
+ editor: editor
9128
+ }),
9129
+ /*#__PURE__*/ jsx(PictureButton, {
9130
+ label: "删除图片",
9131
+ disabled: !imageSelected,
9132
+ onClick: ()=>editor.chain().focus().deleteSelection().run(),
9133
+ children: /*#__PURE__*/ jsx(Trash2, {
9134
+ size: 18
9135
+ })
9136
+ })
9137
+ ]
9138
+ })
9139
+ ]
9140
+ });
9141
+ }
9142
+ function imageWrapDistanceOptionsForValue(value) {
9143
+ if (imageWrapDistanceOptions.some((option)=>option.value === value)) return imageWrapDistanceOptions;
9144
+ return [
9145
+ ...imageWrapDistanceOptions,
9146
+ {
9147
+ value,
9148
+ label: `${value} 毫米`
9149
+ }
9150
+ ];
9151
+ }
9152
+ function PictureButton({ label, active = false, disabled = false, onClick, children }) {
9153
+ return /*#__PURE__*/ jsx(WorkOfficeRibbonButton, {
9154
+ label: label,
9155
+ displayLabel: true,
9156
+ active: active,
9157
+ disabled: disabled,
9158
+ onClick: onClick,
9159
+ children: children
9160
+ });
9161
+ }
9162
+ function DocumentTableInsertPopover({ editor }) {
9163
+ return /*#__PURE__*/ jsx(OfficeTableInsertPopover, {
9164
+ className: "work-document-table-insert-popover",
9165
+ label: "插入表格",
9166
+ onInsert: (dimensions)=>editor.commands.insertDocumentTable(dimensions)
9167
+ });
9168
+ }
9169
+ const PIXELS_PER_CENTIMETER = 96 / 2.54;
9170
+ const document_table_margins_popover_marginFields = [
9171
+ {
9172
+ side: 'top',
9173
+ label: '上',
9174
+ ariaLabel: '单元格上边距(厘米)'
9175
+ },
9176
+ {
9177
+ side: 'bottom',
9178
+ label: '下',
9179
+ ariaLabel: '单元格下边距(厘米)'
9180
+ },
9181
+ {
9182
+ side: 'left',
9183
+ label: '左',
9184
+ ariaLabel: '单元格左边距(厘米)'
9185
+ },
9186
+ {
9187
+ side: 'right',
9188
+ label: '右',
9189
+ ariaLabel: '单元格右边距(厘米)'
9190
+ }
9191
+ ];
9192
+ function DocumentTableMarginsPopover({ editor }) {
9193
+ const margins = currentMargins(editor);
9194
+ const [open, setOpen] = useState(false);
9195
+ const [drafts, setDrafts] = useState(()=>marginDrafts(margins));
9196
+ const customized = !sameMargins(margins, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS);
9197
+ const commit = (side, rawValue)=>{
9198
+ const centimeters = Number(rawValue);
9199
+ if (!Number.isFinite(centimeters) || centimeters < 0 || centimeters > 5) return void setDrafts(marginDrafts(currentMargins(editor)));
9200
+ const value = Math.round(centimeters * PIXELS_PER_CENTIMETER * 100) / 100;
9201
+ const next = {
9202
+ ...currentMargins(editor),
9203
+ [side]: value
9204
+ };
9205
+ editor.commands.setDocumentTableCellMargins(next) ? setDrafts(marginDrafts(next)) : setDrafts(marginDrafts(currentMargins(editor)));
9206
+ };
9207
+ const restoreDefault = ()=>{
9208
+ const next = {
9209
+ ...DEFAULT_DOCUMENT_TABLE_CELL_MARGINS
9210
+ };
9211
+ if (editor.commands.setDocumentTableCellMargins(next)) setDrafts(marginDrafts(next));
9212
+ };
9213
+ return /*#__PURE__*/ jsx(Popover, {
9214
+ label: "单元格边距",
9215
+ panelLabel: "单元格边距设置",
9216
+ panelRole: "dialog",
9217
+ portal: true,
9218
+ open: open,
9219
+ onOpenChange: (nextOpen)=>{
9220
+ setOpen(nextOpen);
9221
+ if (nextOpen) setDrafts(marginDrafts(currentMargins(editor)));
9222
+ },
9223
+ focusFirstOnOpen: true,
9224
+ className: "work-document-table-margins-popover",
9225
+ panelClassName: "work-document-table-margins-panel",
9226
+ trigger: (triggerProps, { open: popoverOpen })=>/*#__PURE__*/ jsxs("button", {
9227
+ ...triggerProps,
9228
+ className: `with-label${customized || popoverOpen ? ' active' : ''}`,
9229
+ "aria-pressed": customized,
9230
+ children: [
9231
+ /*#__PURE__*/ jsx(Grid2X2, {
9232
+ size: 18
9233
+ }),
9234
+ /*#__PURE__*/ jsx("span", {
9235
+ children: "单元格边距"
9236
+ })
9237
+ ]
9238
+ }),
9239
+ children: /*#__PURE__*/ jsxs("fieldset", {
9240
+ children: [
9241
+ /*#__PURE__*/ jsx("legend", {
9242
+ children: "单元格边距"
9243
+ }),
9244
+ /*#__PURE__*/ jsx("p", {
9245
+ children: "设置文字到单元格边框的距离。"
9246
+ }),
9247
+ /*#__PURE__*/ jsx("div", {
9248
+ className: "work-document-table-margins-grid",
9249
+ children: document_table_margins_popover_marginFields.map(({ side, label, ariaLabel })=>/*#__PURE__*/ jsxs("fieldset", {
9250
+ className: "work-document-table-margin-field",
9251
+ children: [
9252
+ /*#__PURE__*/ jsxs("legend", {
9253
+ className: "sr-only",
9254
+ children: [
9255
+ label,
9256
+ "边距"
9257
+ ]
9258
+ }),
9259
+ /*#__PURE__*/ jsx("span", {
9260
+ "aria-hidden": "true",
9261
+ children: label
9262
+ }),
9263
+ /*#__PURE__*/ jsx(OfficeNumberField, {
9264
+ ariaLabel: ariaLabel,
9265
+ value: drafts[side],
9266
+ min: 0,
9267
+ max: 5,
9268
+ step: 0.05,
9269
+ onValueChange: (value)=>setDrafts((current)=>({
9270
+ ...current,
9271
+ [side]: value
9272
+ })),
9273
+ onCommit: (value)=>commit(side, value),
9274
+ onCancel: ()=>setDrafts(marginDrafts(currentMargins(editor)))
9275
+ }),
9276
+ /*#__PURE__*/ jsx("small", {
9277
+ children: "厘米"
9278
+ })
9279
+ ]
9280
+ }, side))
9281
+ }),
9282
+ /*#__PURE__*/ jsx("button", {
9283
+ type: "button",
9284
+ className: "work-document-table-margins-reset",
9285
+ disabled: !customized,
9286
+ onClick: restoreDefault,
9287
+ children: "恢复标准边距"
9288
+ })
9289
+ ]
9290
+ })
9291
+ });
9292
+ }
9293
+ function currentMargins(editor) {
9294
+ return documentTableSizing(editor.state)?.cellMargins ?? {
9295
+ ...DEFAULT_DOCUMENT_TABLE_CELL_MARGINS
9296
+ };
9297
+ }
9298
+ function marginDrafts(margins) {
9299
+ return {
9300
+ top: document_table_margins_popover_centimeters(margins.top),
9301
+ right: document_table_margins_popover_centimeters(margins.right),
9302
+ bottom: document_table_margins_popover_centimeters(margins.bottom),
9303
+ left: document_table_margins_popover_centimeters(margins.left)
9304
+ };
9305
+ }
9306
+ function document_table_margins_popover_centimeters(pixels) {
9307
+ return (pixels / PIXELS_PER_CENTIMETER).toFixed(2).replace(/\.?0+$/, '');
9308
+ }
9309
+ function sameMargins(left, right) {
9310
+ return document_table_margins_popover_marginFields.every(({ side })=>Math.abs(left[side] - right[side]) < 0.01);
9311
+ }
9312
+ const document_table_properties_dialog_model_PIXELS_PER_CENTIMETER = 96 / 2.54;
9313
+ const FALLBACK_TABLE_WIDTH_CENTIMETERS = 15;
9314
+ const FALLBACK_ROW_HEIGHT_CENTIMETERS = 0.95;
9315
+ const FALLBACK_COLUMN_WIDTH_CENTIMETERS = 3.18;
9316
+ const marginSides = [
9317
+ 'top',
9318
+ 'right',
9319
+ 'bottom',
9320
+ 'left'
9321
+ ];
9322
+ function createDocumentTablePropertiesDraft(source) {
9323
+ const widthType = source.sizing.preferredWidthType;
9324
+ const effectiveMargins = effectiveCellMargins(source.cellFormat, source.sizing.cellMargins);
9325
+ return {
9326
+ table: {
9327
+ widthType,
9328
+ width: 'percent' === widthType ? document_table_properties_dialog_model_formatNumber(source.sizing.preferredWidth ?? 100) : 'pixels' === widthType ? document_table_properties_dialog_model_centimeters(source.sizing.preferredWidth ?? source.renderedTableWidth ?? FALLBACK_TABLE_WIDTH_CENTIMETERS * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER) : '',
9329
+ alignment: source.sizing.alignment,
9330
+ indent: document_table_properties_dialog_model_centimeters(source.sizing.indent)
9331
+ },
9332
+ row: {
9333
+ heightEnabled: null !== source.sizing.rowHeight,
9334
+ height: document_table_properties_dialog_model_centimeters(source.sizing.rowHeight ?? source.renderedRowHeight ?? FALLBACK_ROW_HEIGHT_CENTIMETERS * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER),
9335
+ heightRule: source.sizing.rowHeightRule ?? 'atLeast',
9336
+ cantSplit: source.rowOptions.cantSplit,
9337
+ repeatHeader: source.rowOptions.repeatHeader
9338
+ },
9339
+ column: {
9340
+ width: document_table_properties_dialog_model_centimeters(source.renderedColumnWidth ?? source.sizing.columnWidth ?? FALLBACK_COLUMN_WIDTH_CENTIMETERS * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER)
9341
+ },
9342
+ cell: {
9343
+ verticalAlign: source.cellFormat.verticalAlign,
9344
+ useTableMargins: null === source.cellFormat.margins,
9345
+ margins: document_table_properties_dialog_model_marginDrafts(effectiveMargins)
9346
+ }
9347
+ };
9348
+ }
9349
+ function documentTablePropertiesErrors(draft) {
9350
+ const cellMargins = {};
9351
+ if (!draft.cell.useTableMargins) {
9352
+ for (const side of marginSides)if (!document_table_properties_dialog_model_validNumber(draft.cell.margins[side], 0, 5)) cellMargins[side] = '请输入 0 到 5 之间的厘米数。';
9353
+ }
9354
+ return {
9355
+ tableWidth: tableWidthError(draft),
9356
+ tableIndent: tableIndentError(draft),
9357
+ rowHeight: draft.row.heightEnabled ? dimensionError(draft.row.height) : null,
9358
+ columnWidth: dimensionError(draft.column.width),
9359
+ cellMargins
9360
+ };
9361
+ }
9362
+ function hasDocumentTablePropertiesErrors(errors) {
9363
+ return Boolean(errors.tableWidth || errors.tableIndent || errors.rowHeight || errors.columnWidth || Object.keys(errors.cellMargins).length);
9364
+ }
9365
+ function draftForTableWidthType(current, widthType, renderedTableWidth) {
9366
+ if (widthType === current.table.widthType) return current;
9367
+ const width = 'auto' === widthType ? '' : 'percent' === widthType ? '100' : document_table_properties_dialog_model_centimeters(renderedTableWidth ?? FALLBACK_TABLE_WIDTH_CENTIMETERS * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER);
9368
+ return {
9369
+ ...current,
9370
+ table: {
9371
+ ...current.table,
9372
+ widthType,
9373
+ width
9374
+ }
9375
+ };
9376
+ }
9377
+ function documentTablePropertyChanges(initial, current, source) {
9378
+ const errors = documentTablePropertiesErrors(current);
9379
+ if (hasDocumentTablePropertiesErrors(errors)) return null;
9380
+ const changes = {};
9381
+ const tableChanged = initial.table.widthType !== current.table.widthType || !document_table_properties_dialog_model_sameDraftNumber(initial.table.width, current.table.width) || initial.table.alignment !== current.table.alignment || !document_table_properties_dialog_model_sameDraftNumber(initial.table.indent, current.table.indent);
9382
+ if (tableChanged) changes.table = tablePropertiesForChanges(initial, current, source.sizing);
9383
+ const rowChanged = initial.row.heightEnabled !== current.row.heightEnabled || current.row.heightEnabled && !document_table_properties_dialog_model_sameDraftNumber(initial.row.height, current.row.height) || initial.row.heightRule !== current.row.heightRule || initial.row.cantSplit !== current.row.cantSplit || source.canRepeatHeader && initial.row.repeatHeader !== current.row.repeatHeader;
9384
+ if (rowChanged) changes.row = {
9385
+ height: rowHeightForChanges(initial, current, source.sizing),
9386
+ heightRule: current.row.heightRule,
9387
+ cantSplit: current.row.cantSplit,
9388
+ ...source.canRepeatHeader ? {
9389
+ repeatHeader: current.row.repeatHeader
9390
+ } : {}
9391
+ };
9392
+ if (!document_table_properties_dialog_model_sameDraftNumber(initial.column.width, current.column.width)) changes.column = {
9393
+ width: document_table_properties_dialog_model_rounded(Number(current.column.width) * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER),
9394
+ renderedColumnWidths: source.renderedColumnWidths
9395
+ };
9396
+ const marginsChanged = initial.cell.useTableMargins !== current.cell.useTableMargins || !current.cell.useTableMargins && marginSides.some((side)=>!document_table_properties_dialog_model_sameDraftNumber(initial.cell.margins[side], current.cell.margins[side]));
9397
+ if (initial.cell.verticalAlign !== current.cell.verticalAlign || marginsChanged) changes.cell = {
9398
+ verticalAlign: current.cell.verticalAlign,
9399
+ margins: cellMarginsForChanges(initial, current, source)
9400
+ };
9401
+ return Object.keys(changes).length ? changes : null;
9402
+ }
9403
+ function tablePropertiesForChanges(initial, current, sizing) {
9404
+ const widthType = current.table.widthType;
9405
+ const preserveWidth = initial.table.widthType === widthType && document_table_properties_dialog_model_sameDraftNumber(initial.table.width, current.table.width) && sizing.preferredWidthType === widthType;
9406
+ const widthValue = 'auto' === widthType ? null : preserveWidth && null !== sizing.preferredWidth ? sizing.preferredWidth : 'percent' === widthType ? Number(current.table.width) : Number(current.table.width) * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER;
9407
+ return {
9408
+ width: {
9409
+ type: widthType,
9410
+ value: null === widthValue ? null : document_table_properties_dialog_model_rounded(widthValue)
9411
+ },
9412
+ alignment: current.table.alignment,
9413
+ indent: document_table_properties_dialog_model_sameDraftNumber(initial.table.indent, current.table.indent) ? sizing.indent : document_table_properties_dialog_model_rounded(Number(current.table.indent) * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER)
9414
+ };
9415
+ }
9416
+ function rowHeightForChanges(initial, current, sizing) {
9417
+ if (!current.row.heightEnabled) return null;
9418
+ if (initial.row.heightEnabled && document_table_properties_dialog_model_sameDraftNumber(initial.row.height, current.row.height) && null !== sizing.rowHeight) return sizing.rowHeight;
9419
+ return document_table_properties_dialog_model_rounded(Number(current.row.height) * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER);
9420
+ }
9421
+ function cellMarginsForChanges(initial, current, source) {
9422
+ if (current.cell.useTableMargins) return null;
9423
+ const margins = initial.cell.useTableMargins ? {
9424
+ ...source.sizing.cellMargins
9425
+ } : {
9426
+ ...source.cellFormat.margins ?? {}
9427
+ };
9428
+ for (const side of marginSides)if (!document_table_properties_dialog_model_sameDraftNumber(initial.cell.margins[side], current.cell.margins[side])) margins[side] = document_table_properties_dialog_model_rounded(Number(current.cell.margins[side]) * document_table_properties_dialog_model_PIXELS_PER_CENTIMETER);
9429
+ return margins;
9430
+ }
9431
+ function effectiveCellMargins(format, tableMargins) {
9432
+ return {
9433
+ top: format.margins?.top ?? tableMargins.top,
9434
+ right: format.margins?.right ?? tableMargins.right,
9435
+ bottom: format.margins?.bottom ?? tableMargins.bottom,
9436
+ left: format.margins?.left ?? tableMargins.left
9437
+ };
9438
+ }
9439
+ function document_table_properties_dialog_model_marginDrafts(margins) {
9440
+ return {
9441
+ top: document_table_properties_dialog_model_centimeters(margins.top),
9442
+ right: document_table_properties_dialog_model_centimeters(margins.right),
9443
+ bottom: document_table_properties_dialog_model_centimeters(margins.bottom),
9444
+ left: document_table_properties_dialog_model_centimeters(margins.left)
9445
+ };
9446
+ }
9447
+ function tableWidthError(draft) {
9448
+ if ('auto' === draft.table.widthType) return null;
9449
+ if ('percent' === draft.table.widthType) return document_table_properties_dialog_model_validNumber(draft.table.width, 1, 100) ? null : '请输入 1 到 100 之间的百分比。';
9450
+ return document_table_properties_dialog_model_validNumber(draft.table.width, 0.5, 30) ? null : '请输入 0.5 到 30 之间的厘米数。';
9451
+ }
9452
+ function tableIndentError(draft) {
9453
+ if ('left' !== draft.table.alignment) return null;
9454
+ return document_table_properties_dialog_model_validNumber(draft.table.indent, 0, 30) ? null : '请输入 0 到 30 之间的厘米数。';
9455
+ }
9456
+ function dimensionError(value) {
9457
+ return document_table_properties_dialog_model_validNumber(value, 0.5, 30) ? null : '请输入 0.5 到 30 之间的厘米数。';
9458
+ }
9459
+ function document_table_properties_dialog_model_validNumber(value, minimum, maximum) {
9460
+ if (!value.trim()) return false;
9461
+ const numeric = Number(value);
9462
+ return Number.isFinite(numeric) && numeric >= minimum && numeric <= maximum;
9463
+ }
9464
+ function document_table_properties_dialog_model_sameDraftNumber(left, right) {
9465
+ if (!left.trim() || !right.trim()) return left === right;
9466
+ return Number(left) === Number(right);
9467
+ }
9468
+ function document_table_properties_dialog_model_centimeters(pixels) {
9469
+ return document_table_properties_dialog_model_formatNumber(pixels / document_table_properties_dialog_model_PIXELS_PER_CENTIMETER);
9470
+ }
9471
+ function document_table_properties_dialog_model_formatNumber(value) {
9472
+ return Number.isFinite(value) ? value.toFixed(2).replace(/\.?0+$/, '') : '';
9473
+ }
9474
+ function document_table_properties_dialog_model_rounded(value) {
9475
+ return Math.round(100 * value) / 100;
9476
+ }
9477
+ const tabs = [
9478
+ {
9479
+ value: 'table',
9480
+ label: '表格'
9481
+ },
9482
+ {
9483
+ value: 'row',
9484
+ label: '行'
9485
+ },
9486
+ {
9487
+ value: 'column',
9488
+ label: '列'
9489
+ },
9490
+ {
9491
+ value: 'cell',
9492
+ label: '单元格'
9493
+ }
9494
+ ];
9495
+ const widthOptions = [
9496
+ {
9497
+ value: 'auto',
9498
+ label: '自动'
9499
+ },
9500
+ {
9501
+ value: 'percent',
9502
+ label: '百分比'
9503
+ },
9504
+ {
9505
+ value: 'pixels',
9506
+ label: '厘米'
9507
+ }
9508
+ ];
9509
+ const document_table_properties_dialog_sections_alignmentOptions = [
9510
+ {
9511
+ value: 'left',
9512
+ label: '左对齐',
9513
+ icon: AlignLeft
9514
+ },
9515
+ {
9516
+ value: 'center',
9517
+ label: '居中',
9518
+ icon: AlignCenter
9519
+ },
9520
+ {
9521
+ value: 'right',
9522
+ label: '右对齐',
9523
+ icon: AlignRight
9524
+ }
9525
+ ];
9526
+ const rowHeightRuleOptions = [
9527
+ {
9528
+ value: 'atLeast',
9529
+ label: '最小值'
9530
+ },
9531
+ {
9532
+ value: 'exact',
9533
+ label: '固定值'
9534
+ }
9535
+ ];
9536
+ const verticalAlignmentOptions = [
9537
+ {
9538
+ value: 'top',
9539
+ label: '顶端',
9540
+ icon: AlignVerticalJustifyStart
9541
+ },
9542
+ {
9543
+ value: 'middle',
9544
+ label: '居中',
9545
+ icon: AlignVerticalJustifyCenter
9546
+ },
9547
+ {
9548
+ value: 'bottom',
9549
+ label: '底端',
9550
+ icon: AlignVerticalJustifyEnd
9551
+ }
9552
+ ];
9553
+ const document_table_properties_dialog_sections_marginFields = [
9554
+ {
9555
+ side: 'top',
9556
+ label: '上',
9557
+ ariaLabel: '当前单元格上边距(厘米)'
9558
+ },
9559
+ {
9560
+ side: 'bottom',
9561
+ label: '下',
9562
+ ariaLabel: '当前单元格下边距(厘米)'
9563
+ },
9564
+ {
9565
+ side: 'left',
9566
+ label: '左',
9567
+ ariaLabel: '当前单元格左边距(厘米)'
9568
+ },
9569
+ {
9570
+ side: 'right',
9571
+ label: '右',
9572
+ ariaLabel: '当前单元格右边距(厘米)'
9573
+ }
9574
+ ];
9575
+ function DocumentTablePropertiesTabs({ activeTab, idBase, onTabChange }) {
9576
+ return /*#__PURE__*/ jsx("div", {
9577
+ className: "work-document-table-properties-tabs",
9578
+ role: "tablist",
9579
+ "aria-label": "表格属性分类",
9580
+ children: tabs.map((tab)=>/*#__PURE__*/ jsx("button", {
9581
+ type: "button",
9582
+ id: `${idBase}-${tab.value}-tab`,
9583
+ role: "tab",
9584
+ "aria-controls": `${idBase}-${tab.value}-panel`,
9585
+ "aria-selected": activeTab === tab.value,
9586
+ tabIndex: activeTab === tab.value ? 0 : -1,
9587
+ onClick: ()=>onTabChange(tab.value),
9588
+ children: tab.label
9589
+ }, tab.value))
9590
+ });
9591
+ }
9592
+ function DocumentTablePropertiesPanel({ activeTab, idBase, ...props }) {
9593
+ return /*#__PURE__*/ jsxs("section", {
9594
+ id: `${idBase}-${activeTab}-panel`,
9595
+ className: "work-document-table-properties-panel",
9596
+ role: "tabpanel",
9597
+ "aria-labelledby": `${idBase}-${activeTab}-tab`,
9598
+ tabIndex: -1,
9599
+ children: [
9600
+ 'table' === activeTab && /*#__PURE__*/ jsx(TableSection, {
9601
+ ...props
9602
+ }),
9603
+ 'row' === activeTab && /*#__PURE__*/ jsx(RowSection, {
9604
+ ...props
9605
+ }),
9606
+ 'column' === activeTab && /*#__PURE__*/ jsx(ColumnSection, {
9607
+ ...props
9608
+ }),
9609
+ 'cell' === activeTab && /*#__PURE__*/ jsx(CellSection, {
9610
+ ...props
9611
+ })
9612
+ ]
9613
+ });
9614
+ }
9615
+ function TableSection({ draft, setDraft, errors, source }) {
9616
+ return /*#__PURE__*/ jsxs(Fragment, {
9617
+ children: [
9618
+ /*#__PURE__*/ jsxs("fieldset", {
9619
+ className: "work-document-table-properties-section",
9620
+ children: [
9621
+ /*#__PURE__*/ jsx("legend", {
9622
+ children: "首选宽度"
9623
+ }),
9624
+ /*#__PURE__*/ jsx("div", {
9625
+ className: "work-document-table-properties-choice-grid width",
9626
+ children: widthOptions.map((option)=>/*#__PURE__*/ jsxs("label", {
9627
+ children: [
9628
+ /*#__PURE__*/ jsx("input", {
9629
+ type: "radio",
9630
+ name: "table-properties-width",
9631
+ value: option.value,
9632
+ checked: draft.table.widthType === option.value,
9633
+ "data-autofocus": draft.table.widthType === option.value || void 0,
9634
+ onChange: ()=>setDraft((current)=>draftForTableWidthType(current, option.value, source.renderedTableWidth))
9635
+ }),
9636
+ /*#__PURE__*/ jsx("span", {
9637
+ children: option.label
9638
+ })
9639
+ ]
9640
+ }, option.value))
9641
+ }),
9642
+ 'auto' !== draft.table.widthType && /*#__PURE__*/ jsx(NumberRow, {
9643
+ label: "宽度",
9644
+ ariaLabel: 'percent' === draft.table.widthType ? '表格宽度(百分比)' : '表格宽度(厘米)',
9645
+ value: draft.table.width,
9646
+ unit: 'percent' === draft.table.widthType ? '%' : '厘米',
9647
+ min: 'percent' === draft.table.widthType ? 1 : 0.5,
9648
+ max: 'percent' === draft.table.widthType ? 100 : 30,
9649
+ step: 'percent' === draft.table.widthType ? 1 : 0.1,
9650
+ invalid: Boolean(errors.tableWidth),
9651
+ onValueChange: (width)=>setDraft((current)=>({
9652
+ ...current,
9653
+ table: {
9654
+ ...current.table,
9655
+ width
9656
+ }
9657
+ }))
9658
+ }),
9659
+ errors.tableWidth && /*#__PURE__*/ jsx("p", {
9660
+ role: "alert",
9661
+ children: errors.tableWidth
9662
+ })
9663
+ ]
9664
+ }),
9665
+ /*#__PURE__*/ jsxs("fieldset", {
9666
+ className: "work-document-table-properties-section",
9667
+ children: [
9668
+ /*#__PURE__*/ jsx("legend", {
9669
+ children: "位置"
9670
+ }),
9671
+ /*#__PURE__*/ jsx("div", {
9672
+ className: "work-document-table-properties-choice-grid alignment",
9673
+ children: document_table_properties_dialog_sections_alignmentOptions.map((option)=>{
9674
+ const Icon = option.icon;
9675
+ return /*#__PURE__*/ jsxs("label", {
9676
+ children: [
9677
+ /*#__PURE__*/ jsx("input", {
9678
+ type: "radio",
9679
+ name: "table-properties-alignment",
9680
+ value: option.value,
9681
+ checked: draft.table.alignment === option.value,
9682
+ onChange: ()=>setDraft((current)=>({
9683
+ ...current,
9684
+ table: {
9685
+ ...current.table,
9686
+ alignment: option.value
9687
+ }
9688
+ }))
9689
+ }),
9690
+ /*#__PURE__*/ jsxs("span", {
9691
+ children: [
9692
+ /*#__PURE__*/ jsx(Icon, {
9693
+ size: 16,
9694
+ "aria-hidden": "true"
9695
+ }),
9696
+ option.label
9697
+ ]
9698
+ })
9699
+ ]
9700
+ }, option.value);
9701
+ })
9702
+ }),
9703
+ /*#__PURE__*/ jsx(NumberRow, {
9704
+ label: "左缩进",
9705
+ ariaLabel: "表格左缩进(厘米)",
9706
+ value: draft.table.indent,
9707
+ unit: "厘米",
9708
+ min: 0,
9709
+ max: 30,
9710
+ step: 0.1,
9711
+ disabled: 'left' !== draft.table.alignment,
9712
+ invalid: Boolean(errors.tableIndent),
9713
+ onValueChange: (indent)=>setDraft((current)=>({
9714
+ ...current,
9715
+ table: {
9716
+ ...current.table,
9717
+ indent
9718
+ }
9719
+ }))
9720
+ }),
9721
+ errors.tableIndent && /*#__PURE__*/ jsx("p", {
9722
+ role: "alert",
9723
+ children: errors.tableIndent
9724
+ })
9725
+ ]
9726
+ })
9727
+ ]
9728
+ });
9729
+ }
9730
+ function RowSection({ draft, setDraft, errors, source }) {
9731
+ return /*#__PURE__*/ jsxs(Fragment, {
9732
+ children: [
9733
+ /*#__PURE__*/ jsxs("fieldset", {
9734
+ className: "work-document-table-properties-section",
9735
+ children: [
9736
+ /*#__PURE__*/ jsx("legend", {
9737
+ children: "当前行尺寸"
9738
+ }),
9739
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
9740
+ ariaLabel: "指定行高",
9741
+ checked: draft.row.heightEnabled,
9742
+ onCheckedChange: (heightEnabled)=>setDraft((current)=>({
9743
+ ...current,
9744
+ row: {
9745
+ ...current.row,
9746
+ heightEnabled
9747
+ }
9748
+ })),
9749
+ children: "指定行高"
9750
+ }),
9751
+ /*#__PURE__*/ jsx(NumberRow, {
9752
+ label: "行高",
9753
+ ariaLabel: "当前行高(厘米)",
9754
+ value: draft.row.height,
9755
+ unit: "厘米",
9756
+ min: 0.5,
9757
+ max: 30,
9758
+ step: 0.1,
9759
+ disabled: !draft.row.heightEnabled,
9760
+ invalid: Boolean(errors.rowHeight),
9761
+ onValueChange: (height)=>setDraft((current)=>({
9762
+ ...current,
9763
+ row: {
9764
+ ...current.row,
9765
+ height
9766
+ }
9767
+ }))
9768
+ }),
9769
+ /*#__PURE__*/ jsxs("div", {
9770
+ className: "work-document-table-properties-select-row",
9771
+ children: [
9772
+ /*#__PURE__*/ jsx("span", {
9773
+ children: "行高规则"
9774
+ }),
9775
+ /*#__PURE__*/ jsx(OfficeSelect, {
9776
+ ariaLabel: "行高规则",
9777
+ value: draft.row.heightRule,
9778
+ options: rowHeightRuleOptions,
9779
+ disabled: !draft.row.heightEnabled,
9780
+ onValueChange: (heightRule)=>setDraft((current)=>({
9781
+ ...current,
9782
+ row: {
9783
+ ...current.row,
9784
+ heightRule
9785
+ }
9786
+ }))
9787
+ })
9788
+ ]
7497
9789
  }),
7498
- /*#__PURE__*/ jsx(PictureButton, {
7499
- label: "上下环绕",
7500
- active: 'topBottom' === image.layout,
7501
- disabled: !imageSelected,
7502
- onClick: ()=>updateLayout('topBottom'),
7503
- children: /*#__PURE__*/ jsx(Rows3, {
7504
- size: 18
7505
- })
9790
+ errors.rowHeight && /*#__PURE__*/ jsx("p", {
9791
+ role: "alert",
9792
+ children: errors.rowHeight
7506
9793
  })
7507
9794
  ]
7508
9795
  }),
7509
- /*#__PURE__*/ jsxs(WorkOfficeRibbonGroup, {
7510
- label: "位置",
9796
+ /*#__PURE__*/ jsxs("fieldset", {
9797
+ className: "work-document-table-properties-section",
7511
9798
  children: [
7512
- /*#__PURE__*/ jsx(PictureButton, {
7513
- label: "左对齐",
7514
- active: 'left' === image.alignment,
7515
- disabled: !imageSelected,
7516
- onClick: ()=>updateAlignment('left'),
7517
- children: /*#__PURE__*/ jsx(AlignLeft, {
7518
- size: 18
7519
- })
7520
- }),
7521
- /*#__PURE__*/ jsx(PictureButton, {
7522
- label: "居中",
7523
- active: 'center' === image.alignment,
7524
- disabled: !imageSelected,
7525
- onClick: ()=>updateAlignment('center'),
7526
- children: /*#__PURE__*/ jsx(AlignCenter, {
7527
- size: 18
7528
- })
9799
+ /*#__PURE__*/ jsx("legend", {
9800
+ children: "分页"
7529
9801
  }),
7530
- /*#__PURE__*/ jsx(PictureButton, {
7531
- label: "右对齐",
7532
- active: 'right' === image.alignment,
7533
- disabled: !imageSelected,
7534
- onClick: ()=>updateAlignment('right'),
7535
- children: /*#__PURE__*/ jsx(AlignRight, {
7536
- size: 18
7537
- })
9802
+ /*#__PURE__*/ jsxs("div", {
9803
+ className: "work-document-table-properties-checkboxes",
9804
+ children: [
9805
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
9806
+ ariaLabel: "允许跨页断行",
9807
+ checked: !draft.row.cantSplit,
9808
+ onCheckedChange: (allowSplit)=>setDraft((current)=>({
9809
+ ...current,
9810
+ row: {
9811
+ ...current.row,
9812
+ cantSplit: !allowSplit
9813
+ }
9814
+ })),
9815
+ children: "允许跨页断行"
9816
+ }),
9817
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
9818
+ ariaLabel: "在各页顶端重复标题行",
9819
+ checked: draft.row.repeatHeader,
9820
+ disabled: !source.canRepeatHeader,
9821
+ onCheckedChange: (repeatHeader)=>setDraft((current)=>({
9822
+ ...current,
9823
+ row: {
9824
+ ...current.row,
9825
+ repeatHeader
9826
+ }
9827
+ })),
9828
+ children: "在各页顶端重复标题行"
9829
+ })
9830
+ ]
7538
9831
  })
7539
9832
  ]
9833
+ })
9834
+ ]
9835
+ });
9836
+ }
9837
+ function ColumnSection({ draft, setDraft, errors }) {
9838
+ return /*#__PURE__*/ jsxs("fieldset", {
9839
+ className: "work-document-table-properties-section",
9840
+ children: [
9841
+ /*#__PURE__*/ jsx("legend", {
9842
+ children: "当前列尺寸"
7540
9843
  }),
7541
- /*#__PURE__*/ jsx(WorkOfficeRibbonGroup, {
7542
- label: "与文字距离",
7543
- children: /*#__PURE__*/ jsx(OfficeSelect, {
7544
- ariaLabel: "图片与文字距离",
7545
- value: wrapDistanceValue,
7546
- options: imageWrapDistanceOptionsForValue(wrapDistanceValue),
7547
- disabled: !imageSelected || 'inline' === image.layout,
7548
- onValueChange: (value)=>editor.commands.setDocumentImageLayoutOptions({
7549
- wrapDistance: Number(value)
7550
- })
7551
- })
9844
+ /*#__PURE__*/ jsx(NumberRow, {
9845
+ label: "列宽",
9846
+ ariaLabel: "当前列宽(厘米)",
9847
+ value: draft.column.width,
9848
+ unit: "厘米",
9849
+ min: 0.5,
9850
+ max: 30,
9851
+ step: 0.1,
9852
+ invalid: Boolean(errors.columnWidth),
9853
+ onValueChange: (width)=>setDraft((current)=>({
9854
+ ...current,
9855
+ column: {
9856
+ width
9857
+ }
9858
+ }))
7552
9859
  }),
7553
- /*#__PURE__*/ jsxs(WorkOfficeRibbonGroup, {
7554
- label: "图片",
9860
+ errors.columnWidth && /*#__PURE__*/ jsx("p", {
9861
+ role: "alert",
9862
+ children: errors.columnWidth
9863
+ })
9864
+ ]
9865
+ });
9866
+ }
9867
+ function CellSection({ draft, setDraft, errors }) {
9868
+ return /*#__PURE__*/ jsxs(Fragment, {
9869
+ children: [
9870
+ /*#__PURE__*/ jsxs("fieldset", {
9871
+ className: "work-document-table-properties-section",
7555
9872
  children: [
7556
- /*#__PURE__*/ jsx(PictureButton, {
7557
- label: "替代文字",
7558
- disabled: !imageSelected,
7559
- onClick: ()=>{
7560
- const imagePosition = selectedDocumentImagePosition(editor);
7561
- officeDialog.prompt({
7562
- title: '图片说明',
7563
- description: '简要描述图片中的关键信息,留空表示装饰性图片。',
7564
- fieldLabel: '图片替代文字',
7565
- initialValue: documentImageAlternativeText(editor),
7566
- multiline: true,
7567
- confirmLabel: '保存'
7568
- }).then((value)=>{
7569
- if (!restoreDocumentImageSelection(editor, imagePosition)) return;
7570
- if (null !== value) editor.commands.setDocumentImageAlternativeText(value, {
7571
- restoreFocus: false
7572
- });
7573
- });
7574
- },
7575
- children: /*#__PURE__*/ jsx(ScanText, {
7576
- size: 18
7577
- })
9873
+ /*#__PURE__*/ jsx("legend", {
9874
+ children: "垂直对齐"
7578
9875
  }),
7579
- /*#__PURE__*/ jsx(PictureButton, {
7580
- label: "删除图片",
7581
- disabled: !imageSelected,
7582
- onClick: ()=>editor.chain().focus().deleteSelection().run(),
7583
- children: /*#__PURE__*/ jsx(Trash2, {
7584
- size: 18
9876
+ /*#__PURE__*/ jsx("div", {
9877
+ className: "work-document-table-properties-choice-grid alignment",
9878
+ children: verticalAlignmentOptions.map((option)=>{
9879
+ const Icon = option.icon;
9880
+ return /*#__PURE__*/ jsxs("label", {
9881
+ children: [
9882
+ /*#__PURE__*/ jsx("input", {
9883
+ type: "radio",
9884
+ name: "table-properties-vertical-alignment",
9885
+ value: option.value,
9886
+ checked: draft.cell.verticalAlign === option.value,
9887
+ onChange: ()=>setDraft((current)=>({
9888
+ ...current,
9889
+ cell: {
9890
+ ...current.cell,
9891
+ verticalAlign: option.value
9892
+ }
9893
+ }))
9894
+ }),
9895
+ /*#__PURE__*/ jsxs("span", {
9896
+ children: [
9897
+ /*#__PURE__*/ jsx(Icon, {
9898
+ size: 16,
9899
+ "aria-hidden": "true"
9900
+ }),
9901
+ option.label
9902
+ ]
9903
+ })
9904
+ ]
9905
+ }, option.value);
7585
9906
  })
7586
9907
  })
7587
9908
  ]
7588
9909
  }),
7589
- officeDialog.dialog
9910
+ /*#__PURE__*/ jsxs("fieldset", {
9911
+ className: "work-document-table-properties-section",
9912
+ children: [
9913
+ /*#__PURE__*/ jsx("legend", {
9914
+ children: "单元格边距"
9915
+ }),
9916
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
9917
+ ariaLabel: "使用表格默认边距",
9918
+ checked: draft.cell.useTableMargins,
9919
+ onCheckedChange: (useTableMargins)=>setDraft((current)=>({
9920
+ ...current,
9921
+ cell: {
9922
+ ...current.cell,
9923
+ useTableMargins
9924
+ }
9925
+ })),
9926
+ children: "使用表格默认边距"
9927
+ }),
9928
+ /*#__PURE__*/ jsx("div", {
9929
+ className: "work-document-table-properties-margin-grid",
9930
+ children: document_table_properties_dialog_sections_marginFields.map(({ side, label, ariaLabel })=>/*#__PURE__*/ jsxs("div", {
9931
+ className: "work-document-table-properties-margin",
9932
+ children: [
9933
+ /*#__PURE__*/ jsx("span", {
9934
+ children: label
9935
+ }),
9936
+ /*#__PURE__*/ jsx(OfficeNumberField, {
9937
+ ariaLabel: ariaLabel,
9938
+ value: draft.cell.margins[side],
9939
+ min: 0,
9940
+ max: 5,
9941
+ step: 0.05,
9942
+ disabled: draft.cell.useTableMargins,
9943
+ validationInvalid: Boolean(errors.cellMargins[side]),
9944
+ onValueChange: (value)=>setDraft((current)=>({
9945
+ ...current,
9946
+ cell: {
9947
+ ...current.cell,
9948
+ margins: {
9949
+ ...current.cell.margins,
9950
+ [side]: value
9951
+ }
9952
+ }
9953
+ }))
9954
+ }),
9955
+ /*#__PURE__*/ jsx("small", {
9956
+ children: "厘米"
9957
+ }),
9958
+ errors.cellMargins[side] && /*#__PURE__*/ jsx("p", {
9959
+ role: "alert",
9960
+ children: errors.cellMargins[side]
9961
+ })
9962
+ ]
9963
+ }, side))
9964
+ })
9965
+ ]
9966
+ })
7590
9967
  ]
7591
9968
  });
7592
9969
  }
7593
- function selectedDocumentImagePosition(editor) {
7594
- const selection = editor.state.selection;
7595
- return selection instanceof NodeSelection && 'image' === selection.node.type.name ? selection.from : null;
7596
- }
7597
- function restoreDocumentImageSelection(editor, position) {
7598
- if (null === position || editor.isDestroyed || editor.state.doc.nodeAt(position)?.type.name !== 'image') return false;
7599
- return editor.commands.setNodeSelection(position);
7600
- }
7601
- function imageWrapDistanceOptionsForValue(value) {
7602
- if (imageWrapDistanceOptions.some((option)=>option.value === value)) return imageWrapDistanceOptions;
7603
- return [
7604
- ...imageWrapDistanceOptions,
7605
- {
7606
- value,
7607
- label: `${value} 毫米`
7608
- }
7609
- ];
9970
+ function NumberRow({ label, ariaLabel, value, unit, min, max, step, disabled = false, invalid = false, onValueChange }) {
9971
+ return /*#__PURE__*/ jsxs("div", {
9972
+ className: "work-document-table-properties-number-row",
9973
+ children: [
9974
+ /*#__PURE__*/ jsx("span", {
9975
+ children: label
9976
+ }),
9977
+ /*#__PURE__*/ jsx(OfficeNumberField, {
9978
+ ariaLabel: ariaLabel,
9979
+ value: value,
9980
+ min: min,
9981
+ max: max,
9982
+ step: step,
9983
+ disabled: disabled,
9984
+ validationInvalid: invalid,
9985
+ onValueChange: onValueChange
9986
+ }),
9987
+ /*#__PURE__*/ jsx("small", {
9988
+ children: unit
9989
+ })
9990
+ ]
9991
+ });
7610
9992
  }
7611
- function PictureButton({ label, active = false, disabled = false, onClick, children }) {
7612
- return /*#__PURE__*/ jsx(WorkOfficeRibbonButton, {
7613
- label: label,
7614
- displayLabel: true,
7615
- active: active,
7616
- disabled: disabled,
7617
- onClick: onClick,
7618
- children: children
9993
+ function DocumentTablePropertiesControl({ editor, renderedTableWidth, renderedRowHeight, renderedColumnWidth, renderedColumnWidths }) {
9994
+ const [source, setSource] = useState(null);
9995
+ const triggerRef = useRef(null);
9996
+ const openDialog = (event)=>{
9997
+ const sizing = documentTableSizing(editor.state);
9998
+ const cellFormat = documentTableCellFormat(editor.state);
9999
+ if (!sizing || !cellFormat) return;
10000
+ triggerRef.current = event.currentTarget;
10001
+ setSource({
10002
+ sizing,
10003
+ cellFormat,
10004
+ rowOptions: documentTableRowOptions(editor),
10005
+ canRepeatHeader: canSetDocumentTableRowRepeatHeader(editor),
10006
+ renderedTableWidth,
10007
+ renderedRowHeight,
10008
+ renderedColumnWidth,
10009
+ renderedColumnWidths
10010
+ });
10011
+ };
10012
+ return /*#__PURE__*/ jsxs(Fragment, {
10013
+ children: [
10014
+ /*#__PURE__*/ jsx(WorkOfficeRibbonButton, {
10015
+ label: "表格属性",
10016
+ visibleLabel: "表格属性",
10017
+ disabled: !documentTableSizing(editor.state),
10018
+ onClick: openDialog,
10019
+ children: /*#__PURE__*/ jsx(TableProperties, {
10020
+ size: 18
10021
+ })
10022
+ }),
10023
+ source && /*#__PURE__*/ jsx(DocumentTablePropertiesDialog, {
10024
+ editor: editor,
10025
+ source: source,
10026
+ restoreFocusTarget: ()=>triggerRef.current,
10027
+ onClose: ()=>setSource(null)
10028
+ })
10029
+ ]
7619
10030
  });
7620
10031
  }
7621
- function DocumentTableInsertPopover({ editor }) {
7622
- return /*#__PURE__*/ jsx(OfficeTableInsertPopover, {
7623
- className: "work-document-table-insert-popover",
7624
- label: "插入表格",
7625
- onInsert: (dimensions)=>editor.commands.insertDocumentTable(dimensions)
10032
+ function DocumentTablePropertiesDialog({ editor, source, restoreFocusTarget, onClose }) {
10033
+ const [initial] = useState(()=>createDocumentTablePropertiesDraft(source));
10034
+ const [draft, setDraft] = useState(initial);
10035
+ const [activeTab, setActiveTab] = useState('table');
10036
+ const formId = useId();
10037
+ const idBase = `document-table-properties-${useId().replaceAll(':', '')}`;
10038
+ const errors = documentTablePropertiesErrors(draft);
10039
+ const invalid = hasDocumentTablePropertiesErrors(errors);
10040
+ const submit = (event)=>{
10041
+ event?.preventDefault();
10042
+ if (invalid) return;
10043
+ const changes = documentTablePropertyChanges(initial, draft, source);
10044
+ if (!changes || editor.commands.setDocumentTablePropertyChanges(changes)) onClose();
10045
+ };
10046
+ return /*#__PURE__*/ jsxs(Dialog, {
10047
+ title: "表格属性",
10048
+ description: "设置当前表格、行、列和单元格。",
10049
+ className: "work-document-table-properties-dialog",
10050
+ restoreFocusTarget: restoreFocusTarget,
10051
+ onClose: onClose,
10052
+ footer: /*#__PURE__*/ jsxs(Fragment, {
10053
+ children: [
10054
+ /*#__PURE__*/ jsx(button_Button, {
10055
+ tone: "quiet",
10056
+ onClick: onClose,
10057
+ children: "取消"
10058
+ }),
10059
+ /*#__PURE__*/ jsx(button_Button, {
10060
+ tone: "primary",
10061
+ type: "submit",
10062
+ form: formId,
10063
+ disabled: invalid,
10064
+ children: "确定"
10065
+ })
10066
+ ]
10067
+ }),
10068
+ children: [
10069
+ /*#__PURE__*/ jsx(DocumentTablePropertiesTabs, {
10070
+ activeTab: activeTab,
10071
+ idBase: idBase,
10072
+ onTabChange: setActiveTab
10073
+ }),
10074
+ /*#__PURE__*/ jsx("form", {
10075
+ id: formId,
10076
+ onSubmit: submit,
10077
+ children: /*#__PURE__*/ jsx(DocumentTablePropertiesPanel, {
10078
+ activeTab: activeTab,
10079
+ idBase: idBase,
10080
+ draft: draft,
10081
+ setDraft: setDraft,
10082
+ errors: errors,
10083
+ source: source
10084
+ })
10085
+ })
10086
+ ]
7626
10087
  });
7627
10088
  }
7628
10089
  const borderOptions = [
@@ -7655,12 +10116,48 @@ const borderOptions = [
7655
10116
  label: '双线',
7656
10117
  style: 'double',
7657
10118
  width: 2
10119
+ }
10120
+ ];
10121
+ const borderTargetOptions = [
10122
+ {
10123
+ value: 'clear',
10124
+ label: '无框线'
10125
+ },
10126
+ {
10127
+ value: 'all',
10128
+ label: '所有框线'
10129
+ },
10130
+ {
10131
+ value: 'outside',
10132
+ label: '外侧框线'
10133
+ },
10134
+ {
10135
+ value: 'inside',
10136
+ label: '内部框线'
10137
+ },
10138
+ {
10139
+ value: 'top',
10140
+ label: '上框线'
7658
10141
  },
7659
10142
  {
7660
- value: 'none-0',
7661
- label: '无边框',
7662
- style: 'none',
7663
- width: 0
10143
+ value: 'bottom',
10144
+ label: '下框线'
10145
+ },
10146
+ {
10147
+ value: 'left',
10148
+ label: '左框线'
10149
+ },
10150
+ {
10151
+ value: 'right',
10152
+ label: '右框线'
10153
+ },
10154
+ {
10155
+ value: 'insideHorizontal',
10156
+ label: '内部横框线'
10157
+ },
10158
+ {
10159
+ value: 'insideVertical',
10160
+ label: '内部竖框线'
7664
10161
  }
7665
10162
  ];
7666
10163
  const tableLayoutOptions = [
@@ -7677,11 +10174,22 @@ const tableLayoutOptions = [
7677
10174
  label: '固定列宽'
7678
10175
  }
7679
10176
  ];
7680
- const PIXELS_PER_CENTIMETER = 96 / 2.54;
10177
+ const currentTableLayoutOption = {
10178
+ value: 'current',
10179
+ label: '当前宽度',
10180
+ disabled: true
10181
+ };
10182
+ const document_table_ribbon_PIXELS_PER_CENTIMETER = 96 / 2.54;
7681
10183
  function DocumentTableDesignRibbon({ editor }) {
7682
10184
  const format = documentTableCellFormat(editor.state) ?? DEFAULT_DOCUMENT_TABLE_CELL_FORMAT;
7683
- const borderValue = borderOptionValue(format.borderStyle, format.borderWidth);
7684
- const currentBorderOptions = borderOptionsForFormat(format.borderStyle, format.borderWidth);
10185
+ const [borderPen, setBorderPen] = useState(()=>({
10186
+ color: format.borderColor,
10187
+ style: 'none' === format.borderStyle ? 'solid' : format.borderStyle,
10188
+ width: 'none' === format.borderStyle || 0 === format.borderWidth ? 1 : format.borderWidth
10189
+ }));
10190
+ const [borderTarget, setBorderTarget] = useState('all');
10191
+ const borderValue = borderOptionValue(borderPen.style, borderPen.width);
10192
+ const currentBorderOptions = borderOptionsForFormat(borderPen.style, borderPen.width);
7685
10193
  return /*#__PURE__*/ jsxs(Fragment, {
7686
10194
  children: [
7687
10195
  /*#__PURE__*/ jsx(document_table_ribbon_RibbonGroup, {
@@ -7722,10 +10230,11 @@ function DocumentTableDesignRibbon({ editor }) {
7722
10230
  ariaLabel: "边框颜色",
7723
10231
  className: "work-document-table-color-picker",
7724
10232
  compact: true,
7725
- value: format.borderColor,
7726
- onValueChange: (borderColor)=>editor.chain().focus().setDocumentTableCellFormat({
7727
- borderColor
7728
- }).run()
10233
+ value: borderPen.color,
10234
+ onValueChange: (borderColor)=>setBorderPen((current)=>({
10235
+ ...current,
10236
+ color: borderColor
10237
+ }))
7729
10238
  }),
7730
10239
  /*#__PURE__*/ jsx(OfficeSelect, {
7731
10240
  ariaLabel: "边框样式",
@@ -7735,10 +10244,26 @@ function DocumentTableDesignRibbon({ editor }) {
7735
10244
  onValueChange: (value)=>{
7736
10245
  const option = currentBorderOptions.find((candidate)=>candidate.value === value);
7737
10246
  if (!option) return;
7738
- editor.chain().focus().setDocumentTableCellFormat({
7739
- borderStyle: option.style,
7740
- borderWidth: option.width
7741
- }).run();
10247
+ setBorderPen((current)=>({
10248
+ ...current,
10249
+ style: option.style,
10250
+ width: option.width
10251
+ }));
10252
+ }
10253
+ }),
10254
+ /*#__PURE__*/ jsx(OfficeSelect, {
10255
+ ariaLabel: "应用边框",
10256
+ className: "work-document-table-border-target-select",
10257
+ value: borderTarget,
10258
+ options: borderTargetOptions,
10259
+ onValueChange: (value)=>{
10260
+ setBorderTarget(value);
10261
+ const clear = 'clear' === value;
10262
+ editor.chain().focus().setDocumentTableBorders(clear ? 'all' : value, clear ? {
10263
+ color: borderPen.color,
10264
+ style: 'none',
10265
+ width: 0
10266
+ } : borderPen).run();
7742
10267
  }
7743
10268
  })
7744
10269
  ]
@@ -7847,6 +10372,21 @@ function DocumentTableLayoutRibbon({ editor }) {
7847
10372
  editor: editor,
7848
10373
  sizing: sizing
7849
10374
  }),
10375
+ /*#__PURE__*/ jsxs(document_table_ribbon_RibbonGroup, {
10376
+ label: "表格属性",
10377
+ children: [
10378
+ /*#__PURE__*/ jsx(DocumentTablePropertiesControl, {
10379
+ editor: editor,
10380
+ renderedTableWidth: measuredTableWidth(editor),
10381
+ renderedRowHeight: measuredCurrentTableDimension(editor, 'rows') ?? void 0,
10382
+ renderedColumnWidth: measuredCurrentTableDimension(editor, 'columns') ?? void 0,
10383
+ renderedColumnWidths: measuredTableColumnWidths(editor)
10384
+ }),
10385
+ /*#__PURE__*/ jsx(DocumentTableMarginsPopover, {
10386
+ editor: editor
10387
+ })
10388
+ ]
10389
+ }),
7850
10390
  /*#__PURE__*/ jsxs(document_table_ribbon_RibbonGroup, {
7851
10391
  label: "单元格对齐",
7852
10392
  children: [
@@ -7955,6 +10495,13 @@ function DocumentTableLayoutRibbon({ editor }) {
7955
10495
  function DocumentTableSizeRibbonGroup({ editor, sizing }) {
7956
10496
  const measuredRowSelection = measuredTableSelectionSize(editor, 'rows');
7957
10497
  const measuredColumnSelection = measuredTableSelectionSize(editor, 'columns');
10498
+ const measuredColumnWidth = measuredCurrentTableDimension(editor, 'columns');
10499
+ const displayedColumnWidth = sizing?.layoutAlgorithm === 'autofit' ? measuredColumnWidth ?? sizing.columnWidth : sizing?.columnWidth ?? measuredColumnWidth;
10500
+ const tableLayoutValue = sizing?.layoutAlgorithm === 'autofit' && 'fixed' === sizing.layoutMode ? 'current' : sizing?.layoutMode ?? 'window';
10501
+ const displayedTableLayoutOptions = 'current' === tableLayoutValue ? [
10502
+ currentTableLayoutOption,
10503
+ ...tableLayoutOptions
10504
+ ] : tableLayoutOptions;
7958
10505
  const canDistributeRows = editor.can().chain().focus().distributeDocumentTableRows(measuredRowSelection).run();
7959
10506
  const canDistributeColumns = editor.can().chain().focus().distributeDocumentTableColumns(measuredColumnSelection).run();
7960
10507
  return /*#__PURE__*/ jsxs(document_table_ribbon_RibbonGroup, {
@@ -7972,7 +10519,7 @@ function DocumentTableSizeRibbonGroup({ editor, sizing }) {
7972
10519
  /*#__PURE__*/ jsx(TableDimensionField, {
7973
10520
  label: "宽度",
7974
10521
  ariaLabel: "列宽(厘米)",
7975
- value: sizing?.columnWidth ?? measuredCurrentTableDimension(editor, 'columns'),
10522
+ value: displayedColumnWidth,
7976
10523
  onValueChange: (width)=>editor.chain().focus().setDocumentTableColumnWidth(width, measuredTableColumnWidths(editor)).run()
7977
10524
  })
7978
10525
  ]
@@ -7980,9 +10527,12 @@ function DocumentTableSizeRibbonGroup({ editor, sizing }) {
7980
10527
  /*#__PURE__*/ jsx(OfficeSelect, {
7981
10528
  ariaLabel: "表格自动调整",
7982
10529
  className: "work-document-table-layout-select",
7983
- value: sizing?.layoutMode ?? 'window',
7984
- options: tableLayoutOptions,
7985
- onValueChange: (layoutMode)=>editor.chain().focus().setDocumentTableLayoutMode(layoutMode, 'fixed' === layoutMode ? measuredTableWidth(editor) : void 0).run()
10530
+ value: tableLayoutValue,
10531
+ options: displayedTableLayoutOptions,
10532
+ onValueChange: (layoutMode)=>{
10533
+ if ('current' === layoutMode) return;
10534
+ editor.chain().focus().setDocumentTableLayoutMode(layoutMode, 'fixed' === layoutMode ? measuredTableWidth(editor) : void 0).run();
10535
+ }
7986
10536
  }),
7987
10537
  /*#__PURE__*/ jsx(RibbonButton, {
7988
10538
  label: "平均分布行",
@@ -8006,7 +10556,7 @@ function DocumentTableSizeRibbonGroup({ editor, sizing }) {
8006
10556
  });
8007
10557
  }
8008
10558
  function TableDimensionField({ label, ariaLabel, value, onValueChange }) {
8009
- const formattedValue = null === value ? '' : document_table_ribbon_formatCentimeters(value / PIXELS_PER_CENTIMETER);
10559
+ const formattedValue = null === value ? '' : document_table_ribbon_formatCentimeters(value / document_table_ribbon_PIXELS_PER_CENTIMETER);
8010
10560
  const { cancelDraft, dirty, draft, replaceDraft, setDraft, syncDraft } = useOfficeDraft(()=>formattedValue);
8011
10561
  useEffect(()=>syncDraft(formattedValue), [
8012
10562
  formattedValue,
@@ -8032,7 +10582,7 @@ function TableDimensionField({ label, ariaLabel, value, onValueChange }) {
8032
10582
  const centimeters = Number(nextValue);
8033
10583
  if (!Number.isFinite(centimeters) || centimeters < 0.5 || centimeters > 30) return void cancelDraft();
8034
10584
  const normalizedDraft = document_table_ribbon_formatCentimeters(centimeters);
8035
- const committed = onValueChange(Math.round(centimeters * PIXELS_PER_CENTIMETER * 100) / 100);
10585
+ const committed = onValueChange(Math.round(centimeters * document_table_ribbon_PIXELS_PER_CENTIMETER * 100) / 100);
8036
10586
  if (committed) replaceDraft(normalizedDraft);
8037
10587
  else cancelDraft();
8038
10588
  }
@@ -8334,13 +10884,14 @@ function DocumentToolbar({ editor, layoutOpen, layoutFonts = [], navigationOpen,
8334
10884
  if (event.defaultPrevented || event.repeat || event.altKey || isOfficeShortcutBlocked(event.target) || !(event.metaKey || event.ctrlKey)) return;
8335
10885
  const key = event.key.toLowerCase();
8336
10886
  const insideEditor = event.target instanceof Node && Boolean(editorDom?.contains(event.target));
8337
- if (insideEditor && 'z' === key) {
10887
+ const documentHistoryTarget = insideEditor || !isDocumentNativeTextUndoTarget(event.target);
10888
+ if (documentHistoryTarget && 'z' === key) {
8338
10889
  event.preventDefault();
8339
10890
  const command = event.shiftKey ? editor.chain().focus().redo() : editor.chain().focus().undo();
8340
10891
  command.run();
8341
10892
  return;
8342
10893
  }
8343
- if (insideEditor && 'y' === key && !event.shiftKey) {
10894
+ if (documentHistoryTarget && 'y' === key && !event.shiftKey) {
8344
10895
  event.preventDefault();
8345
10896
  editor.chain().focus().redo().run();
8346
10897
  return;
@@ -8822,6 +11373,24 @@ function DocumentToolbar({ editor, layoutOpen, layoutFonts = [], navigationOpen,
8822
11373
  ]
8823
11374
  });
8824
11375
  }
11376
+ function isDocumentNativeTextUndoTarget(target) {
11377
+ if (!(target instanceof HTMLElement)) return false;
11378
+ return target instanceof HTMLInputElement && documentNativeUndoInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.isContentEditable || Boolean(target.closest('[contenteditable="true"]'));
11379
+ }
11380
+ const documentNativeUndoInputTypes = new Set([
11381
+ 'date',
11382
+ 'datetime-local',
11383
+ 'email',
11384
+ 'month',
11385
+ 'number',
11386
+ 'password',
11387
+ 'search',
11388
+ 'tel',
11389
+ 'text',
11390
+ 'time',
11391
+ 'url',
11392
+ 'week'
11393
+ ]);
8825
11394
  function document_toolbar_ToolbarButton({ label, title, shortcut, ariaKeyShortcuts, active = false, disabled = false, displayLabel = false, onClick, children }) {
8826
11395
  return /*#__PURE__*/ jsx(WorkOfficeRibbonButton, {
8827
11396
  label: label,
@@ -9219,7 +11788,9 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
9219
11788
  delete editorDom.dataset.paginationLaidOutBlocks;
9220
11789
  delete editorDom.dataset.paginationPages;
9221
11790
  delete editorDom.dataset.paginationReusedBlocks;
11791
+ delete editorDom.dataset.paginationReusedPageChrome;
9222
11792
  delete editorDom.dataset.paginationReusedPages;
11793
+ delete editorDom.dataset.paginationDerivedPageChrome;
9223
11794
  delete editorDom.dataset.paginationShapedParagraphs;
9224
11795
  delete editorDom.dataset.paginationShapedRuns;
9225
11796
  delete editorDom.dataset.paginationTextCandidates;
@@ -9275,6 +11846,8 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
9275
11846
  activeRequest.current = controller;
9276
11847
  editor.commands.clearDocumentPagination(nextRevision);
9277
11848
  editorDom.dataset.paginationState = 'measuring';
11849
+ editorDom.dataset.paginationReusedPageChrome = '0';
11850
+ editorDom.dataset.paginationDerivedPageChrome = '0';
9278
11851
  delete editorDom.dataset.paginationError;
9279
11852
  const measurementStart = dirtyMeasurementFrom.current;
9280
11853
  dirtyMeasurementFrom.current = 1 / 0;
@@ -9349,7 +11922,10 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
9349
11922
  block.block.id,
9350
11923
  block
9351
11924
  ]));
9352
- const pages = documentPaginationPageDescriptors(layout, snapshot.blocks, editor.state.doc);
11925
+ const pageDescriptorDerivation = deriveDocumentPaginationPageDescriptors(layout, snapshot.blocks, editor.state.doc, previousPagination?.pages, layoutPlan.reusedPageCount);
11926
+ const pages = pageDescriptorDerivation.pages;
11927
+ editorDom.dataset.paginationReusedPageChrome = String(pageDescriptorDerivation.reusedPageCount);
11928
+ editorDom.dataset.paginationDerivedPageChrome = String(pageDescriptorDerivation.derivedPageCount);
9353
11929
  const pageByIndex = new Map(pages.map((descriptor)=>[
9354
11930
  descriptor.pageIndex,
9355
11931
  descriptor
@@ -9467,13 +12043,20 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
9467
12043
  selectionVersion
9468
12044
  ]);
9469
12045
  }
9470
- function documentPaginationPageDescriptors(layout, blocks, documentNode) {
12046
+ function deriveDocumentPaginationPageDescriptors(layout, blocks, documentNode, previousPages = [], requestedReusedPageCount = 0) {
9471
12047
  const blockById = new Map(blocks.map((candidate)=>[
9472
12048
  candidate.block.id,
9473
12049
  candidate
9474
12050
  ]));
12051
+ const previousPageByIndex = new Map(previousPages.map((page)=>[
12052
+ page.pageIndex,
12053
+ page
12054
+ ]));
12055
+ const reusablePhysicalPageCount = Number.isSafeInteger(requestedReusedPageCount) ? Math.max(0, Math.min(requestedReusedPageCount, layout.pages.length)) : 0;
9475
12056
  const sectionPages = new Map();
9476
12057
  const descriptors = [];
12058
+ let reuseOpen = reusablePhysicalPageCount > 0;
12059
+ let reusedPageCount = 0;
9477
12060
  for (const page of layout.pages){
9478
12061
  const pageBlocks = Array.from(new Map(page.placements.flatMap((placement)=>{
9479
12062
  const block = blockById.get(placement.blockId);
@@ -9501,6 +12084,13 @@ function documentPaginationPageDescriptors(layout, blocks, documentNode) {
9501
12084
  const previousPageNumber = descriptors.at(-1)?.pageNumber ?? 0;
9502
12085
  const firstSectionPage = 1 === sectionPage;
9503
12086
  const pageNumber = firstSectionPage && void 0 !== section.layout.pageNumberStart ? section.layout.pageNumberStart : Math.max(1, previousPageNumber + 1);
12087
+ const previousPage = previousPageByIndex.get(page.index);
12088
+ if (reuseOpen && page.index < reusablePhysicalPageCount && previousPage && reusableDocumentPageDescriptor(previousPage, physicalPage, pageNumber, sectionPage, section.id, section.index, section.layout)) {
12089
+ descriptors.push(previousPage);
12090
+ reusedPageCount += 1;
12091
+ continue;
12092
+ }
12093
+ if (page.index < reusablePhysicalPageCount) reuseOpen = false;
9504
12094
  descriptors.push({
9505
12095
  pageIndex: page.index,
9506
12096
  physicalPage,
@@ -9514,7 +12104,14 @@ function documentPaginationPageDescriptors(layout, blocks, documentNode) {
9514
12104
  pageChrome: resolveDocumentPageChrome(section.layout, sectionPage, physicalPage)
9515
12105
  });
9516
12106
  }
9517
- return descriptors;
12107
+ return {
12108
+ pages: descriptors,
12109
+ reusedPageCount,
12110
+ derivedPageCount: descriptors.length - reusedPageCount
12111
+ };
12112
+ }
12113
+ function reusableDocumentPageDescriptor(candidate, physicalPage, pageNumber, sectionPage, sectionId, sectionIndex, layout) {
12114
+ return candidate.pageIndex === physicalPage - 1 && candidate.physicalPage === physicalPage && candidate.pageNumber === pageNumber && candidate.sectionPage === sectionPage && candidate.sectionId === sectionId && candidate.sectionIndex === sectionIndex && JSON.stringify(candidate.layout) === JSON.stringify(layout);
9518
12115
  }
9519
12116
  function documentPagePreviewText(blocks, documentNode) {
9520
12117
  const measuredText = documentNode ? documentPageTextRanges(blocks).reduce((preview, range)=>{
@@ -10150,6 +12747,7 @@ function DocumentEditor({ artifactId, content, extensions = EMPTY_DOCUMENT_EXTEN
10150
12747
  const pageFooterRef = useRef(null);
10151
12748
  const workspaceRef = useRef(null);
10152
12749
  const reviewSurfaceRef = useRef(null);
12750
+ const pageSurfaceRef = useRef(null);
10153
12751
  const taskPaneInvokerRef = useRef(null);
10154
12752
  const commentsDraftFocusRef = useRef(null);
10155
12753
  const citationsDraftFocusRef = useRef(null);
@@ -10253,6 +12851,13 @@ function DocumentEditor({ artifactId, content, extensions = EMPTY_DOCUMENT_EXTEN
10253
12851
  const active = document.activeElement;
10254
12852
  if (active instanceof HTMLElement && active.isConnected) taskPaneInvokerRef.current = active;
10255
12853
  }, []);
12854
+ const taskPaneRestoreTarget = useCallback(()=>{
12855
+ const invoker = taskPaneInvokerRef.current;
12856
+ if (invoker?.isConnected && !invoker.matches(':disabled') && !invoker.closest('[hidden], [aria-hidden="true"]')) return invoker;
12857
+ return editor && !editor.isDestroyed ? editor.view.dom : null;
12858
+ }, [
12859
+ editor
12860
+ ]);
10256
12861
  const rememberTaskPaneDraftFocus = useCallback((event)=>{
10257
12862
  const target = event.target;
10258
12863
  if (!(target instanceof HTMLElement) || !target.matches('input:not([type="hidden"]), textarea, [contenteditable="true"], [role="combobox"]')) return;
@@ -10317,25 +12922,31 @@ function DocumentEditor({ artifactId, content, extensions = EMPTY_DOCUMENT_EXTEN
10317
12922
  taskPane,
10318
12923
  taskPaneDialog.confirm
10319
12924
  ]);
10320
- const closeTaskPane = useCallback(async ()=>{
10321
- if (!await requestEditorViewChange(null, false)) return;
10322
- if (!taskPaneModal) requestAnimationFrame(()=>{
10323
- if (editor && !editor.isDestroyed) editor.view.dom.focus();
12925
+ const restoreClosedTaskPaneFocus = useCallback(()=>{
12926
+ requestAnimationFrame(()=>{
12927
+ const target = taskPaneModal ? taskPaneRestoreTarget() : editor && !editor.isDestroyed ? editor.view.dom : null;
12928
+ target?.focus({
12929
+ preventScroll: true
12930
+ });
10324
12931
  });
10325
12932
  }, [
10326
12933
  editor,
12934
+ taskPaneModal,
12935
+ taskPaneRestoreTarget
12936
+ ]);
12937
+ const closeTaskPane = useCallback(async ()=>{
12938
+ if (!await requestEditorViewChange(null, false)) return;
12939
+ restoreClosedTaskPaneFocus();
12940
+ }, [
10327
12941
  requestEditorViewChange,
10328
- taskPaneModal
12942
+ restoreClosedTaskPaneFocus
10329
12943
  ]);
10330
12944
  const closeCommentsPanel = useCallback(async ()=>{
10331
12945
  if (!await requestEditorViewChange(null, true)) return;
10332
- if (!taskPaneModal) requestAnimationFrame(()=>{
10333
- if (editor && !editor.isDestroyed) editor.view.dom.focus();
10334
- });
12946
+ restoreClosedTaskPaneFocus();
10335
12947
  }, [
10336
- editor,
10337
12948
  requestEditorViewChange,
10338
- taskPaneModal
12949
+ restoreClosedTaskPaneFocus
10339
12950
  ]);
10340
12951
  const toggleTaskPane = useCallback(async (pane)=>{
10341
12952
  if (taskPane !== pane) rememberTaskPaneInvoker();
@@ -10398,11 +13009,7 @@ function DocumentEditor({ artifactId, content, extensions = EMPTY_DOCUMENT_EXTEN
10398
13009
  return pane?.querySelector('.ds-icon-button.close') ?? null;
10399
13010
  },
10400
13011
  getActiveScope: activeTaskPaneElement,
10401
- restoreFocusTarget: ()=>{
10402
- const invoker = taskPaneInvokerRef.current;
10403
- if (invoker?.isConnected && !invoker.matches(':disabled') && !invoker.closest('[hidden], [aria-hidden="true"]')) return invoker;
10404
- return editor && !editor.isDestroyed ? editor.view.dom : null;
10405
- }
13012
+ restoreFocusTarget: taskPaneRestoreTarget
10406
13013
  });
10407
13014
  useOfficeTaskPaneEscape(Boolean(taskPane) && !taskPaneModal, closeTaskPane);
10408
13015
  useOfficeTaskPaneEscape(documentComments.open && !documentComments.draft && !taskPaneModal, closeCommentsPanel);
@@ -10703,6 +13310,14 @@ function DocumentEditor({ artifactId, content, extensions = EMPTY_DOCUMENT_EXTEN
10703
13310
  currentPage: currentPage,
10704
13311
  editor: editor,
10705
13312
  modal: taskPaneModal,
13313
+ pageThumbnailSource: pageSurfaceRef.current && pagination.pageCount ? {
13314
+ element: pageSurfaceRef.current,
13315
+ pageCount: pagination.pageCount,
13316
+ pageGap: kernelPage.pageGap,
13317
+ pageHeight: kernelPage.height,
13318
+ pageWidth: kernelPage.width,
13319
+ revision: editorInput.sourceKey
13320
+ } : void 0,
10706
13321
  pages: (pagination.pages.length ? pagination.pages : [
10707
13322
  firstPageDescriptor
10708
13323
  ]).map((page)=>({
@@ -10748,6 +13363,7 @@ function DocumentEditor({ artifactId, content, extensions = EMPTY_DOCUMENT_EXTEN
10748
13363
  onLayoutChange: updateLayout
10749
13364
  }),
10750
13365
  /*#__PURE__*/ jsxs("article", {
13366
+ ref: pageSurfaceRef,
10751
13367
  className: `work-document-page${preview ? ' work-document-preview-page' : ''} ${layout.pageSize} ${layout.orientation}${pagination.pageCount ? ' paginated' : ''}${!preview && pageChromeEditing ? ' page-chrome-editing' : ''}`,
10752
13368
  "data-work-pdf-live-document": artifactId && pagination.pageCount ? '' : void 0,
10753
13369
  "data-pdf-orientation": artifactId && pagination.pageCount ? layout.orientation : void 0,