@a3s-lab/office 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +14 -1
  2. package/dist/0~4595.js +117 -11
  3. package/dist/0~5050.js +124 -113
  4. package/dist/0~9512.js +1 -0
  5. package/dist/0~document-editor.js +2836 -1429
  6. package/dist/0~work-document-extensions.js +2 -2
  7. package/dist/internal/design-system/primitives/navigation/tabs.d.ts +3 -1
  8. package/dist/internal/features/work/editors/document-command-catalog.d.ts +527 -0
  9. package/dist/internal/features/work/editors/document-editor-support.d.ts +7 -3
  10. package/dist/internal/features/work/editors/document-editor.d.ts +1 -1
  11. package/dist/internal/features/work/editors/document-format-clipboard.d.ts +6 -0
  12. package/dist/internal/features/work/editors/document-format-tools.d.ts +4 -0
  13. package/dist/internal/features/work/editors/document-layout-panel.d.ts +4 -1
  14. package/dist/internal/features/work/editors/document-page-chrome-wps-shortcuts.d.ts +2 -0
  15. package/dist/internal/features/work/editors/document-page-layout-ribbon.d.ts +14 -0
  16. package/dist/internal/features/work/editors/document-review-navigation.d.ts +9 -0
  17. package/dist/internal/features/work/editors/document-statistics-dialog.d.ts +7 -0
  18. package/dist/internal/features/work/editors/document-status-bar.d.ts +2 -1
  19. package/dist/internal/features/work/editors/document-toolbar.d.ts +12 -42
  20. package/dist/internal/features/work/editors/document-wps-shortcuts.d.ts +12 -0
  21. package/dist/internal/features/work/editors/document-zoom.d.ts +17 -0
  22. package/dist/internal/features/work/editors/work-office-chrome.d.ts +22 -3
  23. package/dist/internal/features/work/editors/work-office-ribbon-overflow.d.ts +2 -0
  24. package/dist/office-kernel.wasm +0 -0
  25. package/dist/styles.css +213 -1
  26. package/docs/latest/en/browser-editor-architecture.md +33 -0
  27. package/package.json +11 -1
package/README.md CHANGED
@@ -255,7 +255,19 @@ interaction model.
255
255
  long-document windows for page buttons, heading rows, full-text results, and
256
256
  tracked-revision and anchored-comment review that preserve native scroll
257
257
  distance, sparse current/selection/draft pins, and Home/End reachability;
258
- paragraph styles and phone-sized paragraph-spacing and pagination controls
258
+ a WPS-familiar command catalog, quick-access undo and redo, a priority-aware
259
+ adaptive and persistently collapsible ribbon whose tabs can temporarily
260
+ expose commands; WPS-oriented Insert groups and direct Page Layout presets
261
+ for margins, orientation, paper size, and columns, with matching advanced
262
+ Page Setup destinations; WPS-oriented References, Review, and View groups,
263
+ direct tracked-change navigation and decisions, contextual object tabs, and
264
+ viewport-calculated 100%, one-page, and page-width zoom; an actionable status
265
+ bar with live word-count details, WPS `Ctrl+Shift+G`, keyboard-traversable
266
+ view and zoom controls, and compact Web prioritization; editor-scoped WPS
267
+ formatting, paragraph, style, and review shortcuts, plus a permission-free
268
+ formatting clipboard and one-shot format painter shared with compatible
269
+ header and footer formatting, paragraph styles and phone-sized
270
+ paragraph-spacing and pagination controls
259
271
  with touch targets and exact invoker-focus restoration; stable compact-ribbon
260
272
  edge paging and viewport-bounded list galleries with touch-sized numbering
261
273
  actions, selection-preserving bullet and numbering commands, and explicit
@@ -523,6 +535,7 @@ without hard-coded return URLs.
523
535
 
524
536
  - [Live Playground](https://a3s-lab.github.io/Office/)
525
537
  - [Documentation center](https://a3s-lab.github.io/Office/docs/)
538
+ - [A3S Office 0.3.0 documentation](https://a3s-lab.github.io/Office/docs/0.3.0/)
526
539
  - [A3S Office 0.2.0 documentation](https://a3s-lab.github.io/Office/docs/0.2.0/)
527
540
  - [A3S Office 0.1.0 documentation](https://a3s-lab.github.io/Office/docs/0.1.0/)
528
541
  - [React, Vue, and Web Component integration](https://a3s-lab.github.io/Office/docs/components/)
package/dist/0~4595.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Fragment as jsx_runtime_Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Fragment, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
- import { Check, ChevronDown, ChevronLeft, ChevronRight, Eye, Minus, Plus, X } from "lucide-react";
4
+ import { Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Eye, Minus, Plus, X } from "lucide-react";
5
5
  import { moveOfficeMenuFocus, useDialogFocusScope, icon_button_IconButton, Popover, matchesAriaKeyShortcuts, officeOverlayPortalRoot } from "./0~9512.js";
6
6
  const CONTEXT_MENU_MARGIN = 8;
7
7
  const CONTEXT_MENU_TAB_STOP_SELECTOR = 'a[href]:not([tabindex="-1"]), button:not(:disabled):not([tabindex="-1"]), input:not(:disabled):not([type="hidden"]):not([tabindex="-1"]), select:not(:disabled):not([tabindex="-1"]), textarea:not(:disabled):not([tabindex="-1"]), [contenteditable="true"]:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])';
@@ -542,7 +542,7 @@ function useTabNavigation({ items, onChange }) {
542
542
  setTabElement
543
543
  };
544
544
  }
545
- function Tabs({ ariaLabel, value, items, onChange, variant = 'segment', size = 'standard', className = '', containerRef }) {
545
+ function Tabs({ ariaLabel, value, items, onChange, onTabDoubleClick, variant = 'segment', size = 'standard', className = '', containerRef }) {
546
546
  const tabNavigation = useTabNavigation({
547
547
  items,
548
548
  onChange
@@ -562,12 +562,14 @@ function Tabs({ ariaLabel, value, items, onChange, variant = 'segment', size = '
562
562
  role: "tab",
563
563
  id: item.tabId,
564
564
  "data-tab-id": item.id,
565
+ "data-contextual": item.contextual ? 'true' : void 0,
565
566
  "aria-label": item.label,
566
567
  "aria-selected": selected,
567
568
  "aria-controls": item.panelId,
568
569
  tabIndex: selected ? 0 : -1,
569
570
  disabled: item.disabled,
570
571
  onClick: ()=>onChange(item.id),
572
+ onDoubleClick: ()=>onTabDoubleClick?.(item.id),
571
573
  onKeyDown: (event)=>tabNavigation.handleTabKeyDown(event, item.id),
572
574
  children: [
573
575
  item.icon && /*#__PURE__*/ jsx("span", {
@@ -663,6 +665,11 @@ function roundSliderValue(value, step) {
663
665
  return Number((Math.round(value / step) * step).toFixed(decimalPlaces));
664
666
  }
665
667
  const WORK_OFFICE_RIBBON_SCROLL_INSET = 34;
668
+ function calculateRibbonDensity(clientWidth) {
669
+ if (clientWidth <= 0 || clientWidth >= 1120) return 'comfortable';
670
+ if (clientWidth >= 760) return 'compact-low';
671
+ return 'compact-normal';
672
+ }
666
673
  function calculateRibbonOverflow({ clientWidth, items, scrollLeft }) {
667
674
  const lastItem = items.at(-1);
668
675
  if (!lastItem || clientWidth <= 0) return {
@@ -689,9 +696,12 @@ function calculateRibbonScrollTarget({ clientWidth, direction, items, scrollLeft
689
696
  if (!target) return scrollLeft;
690
697
  return Math.max(0, target.left - WORK_OFFICE_RIBBON_SCROLL_INSET);
691
698
  }
692
- function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange, panels, fileActions, className = '', toolbarClassName = '' }) {
699
+ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange, panels, fileActions, quickAccessActions, adaptive = false, collapsible = false, collapsed: controlledCollapsed, defaultCollapsed = false, onCollapsedChange, className = '', toolbarClassName = '' }) {
693
700
  const reactId = useId().replaceAll(':', '');
694
701
  const [internalTab, setInternalTab] = useState(defaultTab);
702
+ const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);
703
+ const [temporarilyExpanded, setTemporarilyExpanded] = useState(false);
704
+ const [ribbonDensity, setRibbonDensity] = useState('comfortable');
695
705
  const [ribbonOverflow, setRibbonOverflow] = useState({
696
706
  backward: false,
697
707
  forward: false
@@ -702,14 +712,20 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
702
712
  });
703
713
  const tabListRef = useRef(null);
704
714
  const toolbarRef = useRef(null);
715
+ const ribbonRef = useRef(null);
705
716
  const selectedTab = activeTab ?? internalTab;
706
717
  const selectedLabel = tabs.find((tab)=>tab.id === selectedTab)?.label ?? tabs[0]?.label ?? '';
718
+ const contextualTabActive = Boolean(tabs.find((tab)=>tab.id === selectedTab)?.contextual);
707
719
  const selectedPanel = panels[selectedTab];
708
720
  const hasSelectedPanel = null != selectedPanel;
721
+ const collapsed = collapsible && (controlledCollapsed ?? internalCollapsed);
722
+ const panelExpanded = hasSelectedPanel && (!collapsed || temporarilyExpanded);
709
723
  const hasRibbonOverflow = ribbonOverflow.backward || ribbonOverflow.forward;
710
724
  const updateRibbonOverflow = useCallback(()=>{
711
725
  const toolbar = toolbarRef.current;
712
726
  if (!toolbar) return;
727
+ const nextDensity = adaptive ? calculateRibbonDensity(toolbar.clientWidth) : 'comfortable';
728
+ setRibbonDensity((current)=>current === nextDensity ? current : nextDensity);
713
729
  const { backward, forward } = calculateRibbonOverflow({
714
730
  clientWidth: toolbar.clientWidth,
715
731
  items: ribbonItemGeometry(toolbar),
@@ -720,7 +736,9 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
720
736
  backward,
721
737
  forward
722
738
  });
723
- }, []);
739
+ }, [
740
+ adaptive
741
+ ]);
724
742
  const scrollRibbon = (direction)=>{
725
743
  const toolbar = toolbarRef.current;
726
744
  if (!toolbar) return;
@@ -759,8 +777,29 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
759
777
  };
760
778
  const selectTab = (tab)=>{
761
779
  if (void 0 === activeTab) setInternalTab(tab);
780
+ if (collapsed) setTemporarilyExpanded(true);
762
781
  onTabChange?.(tab);
763
782
  };
783
+ const changeCollapsed = useCallback((nextCollapsed)=>{
784
+ if (void 0 === controlledCollapsed) setInternalCollapsed(nextCollapsed);
785
+ setTemporarilyExpanded(false);
786
+ onCollapsedChange?.(nextCollapsed);
787
+ }, [
788
+ controlledCollapsed,
789
+ onCollapsedChange
790
+ ]);
791
+ useEffect(()=>{
792
+ if (!collapsed || !temporarilyExpanded) return;
793
+ const closeTemporaryPanel = (event)=>{
794
+ if (event.target instanceof Node && ribbonRef.current?.contains(event.target)) return;
795
+ setTemporarilyExpanded(false);
796
+ };
797
+ document.addEventListener('pointerdown', closeTemporaryPanel, true);
798
+ return ()=>document.removeEventListener('pointerdown', closeTemporaryPanel, true);
799
+ }, [
800
+ collapsed,
801
+ temporarilyExpanded
802
+ ]);
764
803
  useLayoutEffect(()=>{
765
804
  const toolbar = toolbarRef.current;
766
805
  if (!toolbar) return;
@@ -768,12 +807,12 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
768
807
  updateRibbonOverflow();
769
808
  }, [
770
809
  selectedTab,
771
- hasSelectedPanel,
810
+ panelExpanded,
772
811
  updateRibbonOverflow
773
812
  ]);
774
813
  useLayoutEffect(()=>{
775
814
  const toolbar = toolbarRef.current;
776
- if (!toolbar) return;
815
+ if (!toolbar || !panelExpanded) return;
777
816
  let frame = requestAnimationFrame(updateRibbonOverflow);
778
817
  const scheduleOverflowUpdate = ()=>{
779
818
  cancelAnimationFrame(frame);
@@ -803,7 +842,7 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
803
842
  window.removeEventListener('resize', scheduleOverflowUpdate);
804
843
  };
805
844
  }, [
806
- hasSelectedPanel,
845
+ panelExpanded,
807
846
  updateRibbonOverflow
808
847
  ]);
809
848
  useLayoutEffect(()=>{
@@ -848,8 +887,11 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
848
887
  updateTabOverflow
849
888
  ]);
850
889
  return /*#__PURE__*/ jsxs("section", {
890
+ ref: ribbonRef,
851
891
  className: `work-office-ribbon ${className}`.trim(),
852
892
  "aria-label": ariaLabel,
893
+ "data-collapsed": collapsed ? 'true' : void 0,
894
+ "data-contextual-active": contextualTabActive ? 'true' : void 0,
853
895
  children: [
854
896
  /*#__PURE__*/ jsxs("div", {
855
897
  className: "work-office-ribbon-tabs-row",
@@ -857,6 +899,9 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
857
899
  fileActions?.length ? /*#__PURE__*/ jsx(WorkOfficeFileMenu, {
858
900
  actions: fileActions
859
901
  }) : null,
902
+ quickAccessActions?.length ? /*#__PURE__*/ jsx(WorkOfficeQuickAccessToolbar, {
903
+ actions: quickAccessActions
904
+ }) : null,
860
905
  /*#__PURE__*/ jsxs("div", {
861
906
  className: "work-office-ribbon-tab-strip",
862
907
  children: [
@@ -872,7 +917,10 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
872
917
  tabId: `${reactId}-tab-${tab.id}`,
873
918
  panelId: `${reactId}-panel`
874
919
  })),
875
- onChange: selectTab
920
+ onChange: selectTab,
921
+ onTabDoubleClick: (tab)=>{
922
+ if (collapsible && tab === selectedTab) changeCollapsed(!collapsed);
923
+ }
876
924
  }),
877
925
  (tabOverflow.backward || tabOverflow.forward) && /*#__PURE__*/ jsxs("nav", {
878
926
  className: "work-office-ribbon-tab-navigation",
@@ -899,6 +947,22 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
899
947
  ]
900
948
  })
901
949
  ]
950
+ }),
951
+ collapsible && /*#__PURE__*/ jsx("button", {
952
+ type: "button",
953
+ className: "work-office-ribbon-collapse",
954
+ "aria-label": collapsed ? '展开功能区' : '折叠功能区',
955
+ title: collapsed ? '展开功能区' : '折叠功能区',
956
+ "aria-controls": `${reactId}-panel`,
957
+ "aria-expanded": panelExpanded,
958
+ onClick: ()=>changeCollapsed(!collapsed),
959
+ children: collapsed ? /*#__PURE__*/ jsx(ChevronDown, {
960
+ size: 14,
961
+ "aria-hidden": "true"
962
+ }) : /*#__PURE__*/ jsx(ChevronUp, {
963
+ size: 14,
964
+ "aria-hidden": "true"
965
+ })
902
966
  })
903
967
  ]
904
968
  }),
@@ -906,6 +970,7 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
906
970
  id: `${reactId}-panel`,
907
971
  className: "work-office-ribbon-panel",
908
972
  "data-empty": hasSelectedPanel ? void 0 : 'true',
973
+ hidden: !panelExpanded,
909
974
  role: "tabpanel",
910
975
  "aria-labelledby": `${reactId}-tab-${selectedTab}`,
911
976
  children: hasSelectedPanel && /*#__PURE__*/ jsxs(jsx_runtime_Fragment, {
@@ -923,6 +988,7 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
923
988
  /*#__PURE__*/ jsx("div", {
924
989
  ref: toolbarRef,
925
990
  className: `work-office-toolbar ${toolbarClassName}`.trim(),
991
+ "data-density": ribbonDensity,
926
992
  "data-has-overflow": hasRibbonOverflow ? 'true' : void 0,
927
993
  role: "toolbar",
928
994
  "aria-label": `${selectedLabel}工具栏`,
@@ -945,6 +1011,25 @@ function WorkOfficeRibbon({ ariaLabel, tabs, defaultTab, activeTab, onTabChange,
945
1011
  ]
946
1012
  });
947
1013
  }
1014
+ function WorkOfficeQuickAccessToolbar({ actions }) {
1015
+ return /*#__PURE__*/ jsx("div", {
1016
+ className: "work-office-quick-access",
1017
+ role: "toolbar",
1018
+ "aria-label": "快速访问工具栏",
1019
+ children: actions.map((action)=>/*#__PURE__*/ jsx("button", {
1020
+ type: "button",
1021
+ "aria-label": action.label,
1022
+ "aria-keyshortcuts": action.ariaKeyShortcuts,
1023
+ title: action.shortcut ? `${action.label}(${action.shortcut})` : action.label,
1024
+ disabled: action.disabled,
1025
+ onClick: ()=>void action.onSelect(),
1026
+ children: /*#__PURE__*/ jsx("span", {
1027
+ "aria-hidden": "true",
1028
+ children: action.icon
1029
+ })
1030
+ }, action.id))
1031
+ });
1032
+ }
948
1033
  function ribbonItemGeometry(toolbar) {
949
1034
  return Array.from(toolbar.children).flatMap((element)=>element instanceof HTMLElement ? [
950
1035
  ribbonItemBounds(toolbar, element)
@@ -1073,10 +1158,11 @@ function WorkOfficeFileMenu({ actions }) {
1073
1158
  })
1074
1159
  });
1075
1160
  }
1076
- function WorkOfficeRibbonGroup({ label, children }) {
1161
+ function WorkOfficeRibbonGroup({ label, priority = 'normal', children }) {
1077
1162
  return /*#__PURE__*/ jsxs("section", {
1078
1163
  className: "work-office-ribbon-group",
1079
1164
  "aria-label": label,
1165
+ "data-priority": priority,
1080
1166
  children: [
1081
1167
  /*#__PURE__*/ jsx("div", {
1082
1168
  children: children
@@ -1108,9 +1194,10 @@ function WorkOfficeRibbonButton({ label, visibleLabel = label, badge, active, di
1108
1194
  ]
1109
1195
  });
1110
1196
  }
1111
- function WorkOfficeStatusBar({ children, controls, className = '' }) {
1112
- return /*#__PURE__*/ jsxs("footer", {
1197
+ function WorkOfficeStatusBar({ ariaLabel = '编辑器状态栏', children, controls, controlsLabel = '视图与缩放', className = '' }) {
1198
+ return /*#__PURE__*/ jsxs("section", {
1113
1199
  className: `work-office-status ${className}`.trim(),
1200
+ "aria-label": ariaLabel,
1114
1201
  children: [
1115
1202
  /*#__PURE__*/ jsx("div", {
1116
1203
  className: "work-office-status-info",
@@ -1118,11 +1205,30 @@ function WorkOfficeStatusBar({ children, controls, className = '' }) {
1118
1205
  }),
1119
1206
  controls && /*#__PURE__*/ jsx("div", {
1120
1207
  className: "work-office-status-view",
1208
+ role: "toolbar",
1209
+ "aria-label": controlsLabel,
1210
+ onKeyDown: moveOfficeToolbarFocus,
1121
1211
  children: controls
1122
1212
  })
1123
1213
  ]
1124
1214
  });
1125
1215
  }
1216
+ function moveOfficeToolbarFocus(event) {
1217
+ if (![
1218
+ 'ArrowLeft',
1219
+ 'ArrowRight',
1220
+ 'Home',
1221
+ 'End'
1222
+ ].includes(event.key) || event.target instanceof HTMLElement && 'slider' === event.target.getAttribute('role')) return;
1223
+ const controls = [
1224
+ ...event.currentTarget.querySelectorAll('button:not(:disabled), [role="slider"]:not([aria-disabled="true"])')
1225
+ ];
1226
+ if (!controls.length) return;
1227
+ const currentIndex = controls.indexOf(document.activeElement);
1228
+ const nextIndex = 'Home' === event.key ? 0 : 'End' === event.key ? controls.length - 1 : 'ArrowRight' === event.key ? (currentIndex + 1 + controls.length) % controls.length : (currentIndex - 1 + controls.length) % controls.length;
1229
+ event.preventDefault();
1230
+ controls[nextIndex]?.focus();
1231
+ }
1126
1232
  function WorkOfficeZoomControls({ zoom, minimum = 50, maximum = 200, step = 10, decreaseLabel, increaseLabel, outputLabel, sliderLabel, onChange }) {
1127
1233
  const clamp = (value)=>Math.min(maximum, Math.max(minimum, Math.round(value)));
1128
1234
  return /*#__PURE__*/ jsxs(jsx_runtime_Fragment, {
package/dist/0~5050.js CHANGED
@@ -1,8 +1,114 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { Copy, Languages, MessageSquareText, Sparkles, TextQuote, WandSparkles } from "lucide-react";
3
3
  import { showToast, createAgentProposalRequest as createWorkAgentProposalRequest } from "./6489.js";
4
- const MIN_DOCUMENT_ZOOM = 50;
5
- const MAX_DOCUMENT_ZOOM = 200;
4
+ const SURROUNDING_TEXT_LIMIT = 2000;
5
+ function createWorkDocumentSelectionSnapshot(editor, content) {
6
+ const { from, to, empty } = editor.state.selection;
7
+ if (empty) return null;
8
+ const rawText = editor.state.doc.textBetween(from, to, '\n');
9
+ const text = rawText.trim();
10
+ if (!text) return null;
11
+ const beforeText = editor.state.doc.textBetween(0, from, '\n').slice(-SURROUNDING_TEXT_LIMIT);
12
+ const afterText = editor.state.doc.textBetween(to, editor.state.doc.content.size, '\n').slice(0, SURROUNDING_TEXT_LIMIT);
13
+ const selectedContent = editor.state.selection.content().content.toJSON();
14
+ return {
15
+ selection: {
16
+ from,
17
+ to,
18
+ text,
19
+ rawText,
20
+ beforeText,
21
+ afterText,
22
+ model: {
23
+ type: 'doc',
24
+ ...selectedContent?.length ? {
25
+ content: selectedContent
26
+ } : {}
27
+ }
28
+ },
29
+ document: {
30
+ content,
31
+ html: editor.getHTML(),
32
+ text: editor.state.doc.textBetween(0, editor.state.doc.content.size, '\n')
33
+ }
34
+ };
35
+ }
36
+ function createWorkDocumentSelectionAction(editor, snapshot, getTrackChanges) {
37
+ let from = snapshot.selection.from;
38
+ let to = snapshot.selection.to;
39
+ let stale = false;
40
+ let disposed = false;
41
+ const handleTransaction = ({ transaction })=>{
42
+ if (!transaction.docChanged || stale || disposed) return;
43
+ const mappedFrom = transaction.mapping.mapResult(from, 1);
44
+ const mappedTo = transaction.mapping.mapResult(to, -1);
45
+ from = mappedFrom.pos;
46
+ to = mappedTo.pos;
47
+ stale = mappedFrom.deletedAcross || mappedTo.deletedAcross || from > to || transaction.doc.textBetween(from, to, '\n') !== snapshot.selection.rawText;
48
+ };
49
+ editor.on('transaction', handleTransaction);
50
+ const currentRange = ()=>{
51
+ if (disposed || editor.isDestroyed) return 'editor-unavailable';
52
+ if (!editor.isEditable) return 'read-only';
53
+ if (stale || from < 0 || to < from || to > editor.state.doc.content.size || editor.state.doc.textBetween(from, to, '\n') !== snapshot.selection.rawText) return 'stale-selection';
54
+ return {
55
+ from,
56
+ to
57
+ };
58
+ };
59
+ const editText = (text, placement)=>{
60
+ if ('string' != typeof text || 'replace' !== placement && !text) return {
61
+ applied: false,
62
+ reason: 'invalid-text'
63
+ };
64
+ const range = currentRange();
65
+ if ('string' == typeof range) return {
66
+ applied: false,
67
+ reason: range
68
+ };
69
+ const editRange = 'before' === placement ? {
70
+ from: range.from,
71
+ to: range.from
72
+ } : 'after' === placement ? {
73
+ from: range.to,
74
+ to: range.to
75
+ } : range;
76
+ const applied = getTrackChanges() ? editor.commands.replaceDocumentTextWithTrackedChange(editRange.from, editRange.to, text) : editor.chain().focus().setTextSelection(editRange).insertContent(documentPlainTextAsHtml(text)).run();
77
+ return applied ? {
78
+ applied: true
79
+ } : {
80
+ applied: false,
81
+ reason: 'command-rejected'
82
+ };
83
+ };
84
+ return {
85
+ context: {
86
+ ...snapshot,
87
+ commands: {
88
+ copyText: async ()=>{
89
+ try {
90
+ if ("u" < typeof navigator || !navigator.clipboard?.writeText) return false;
91
+ await navigator.clipboard.writeText(snapshot.selection.rawText);
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ },
97
+ insertAfter: (text)=>editText(text, 'after'),
98
+ insertBefore: (text)=>editText(text, 'before'),
99
+ replaceText: (text)=>editText(text, 'replace')
100
+ }
101
+ },
102
+ dispose: ()=>{
103
+ if (disposed) return;
104
+ disposed = true;
105
+ editor.off('transaction', handleTransaction);
106
+ }
107
+ };
108
+ }
109
+ function documentPlainTextAsHtml(value) {
110
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;').replace(/\r?\n/g, '<br>');
111
+ }
6
112
  function documentAgentMenuItems(selection, onAgentRequest, proposalOptions) {
7
113
  return [
8
114
  {
@@ -173,8 +279,21 @@ function documentCurrentPage(editor) {
173
279
  function documentWordCount(value) {
174
280
  return Array.from(value.matchAll(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]|[\p{L}\p{N}]+/gu)).length;
175
281
  }
176
- function clampDocumentZoom(zoom) {
177
- return Math.min(MAX_DOCUMENT_ZOOM, Math.max(MIN_DOCUMENT_ZOOM, Math.round(zoom)));
282
+ function documentTextStatistics(editor) {
283
+ const text = editor.getText({
284
+ blockSeparator: '\n'
285
+ });
286
+ const characters = Array.from(text).filter((character)=>'\n' !== character && '\r' !== character);
287
+ let paragraphCount = 0;
288
+ editor.state.doc.descendants((node)=>{
289
+ if (node.isTextblock) paragraphCount += 1;
290
+ });
291
+ return {
292
+ characterCountWithSpaces: characters.length,
293
+ characterCountWithoutSpaces: characters.filter((character)=>!/\s/u.test(character)).length,
294
+ paragraphCount,
295
+ wordCount: documentWordCount(text)
296
+ };
178
297
  }
179
298
  async function copyDocumentSelection(selection) {
180
299
  try {
@@ -185,114 +304,6 @@ async function copyDocumentSelection(selection) {
185
304
  showToast('无法访问剪贴板,请使用系统复制快捷键。', 'error');
186
305
  }
187
306
  }
188
- const SURROUNDING_TEXT_LIMIT = 2000;
189
- function createWorkDocumentSelectionSnapshot(editor, content) {
190
- const { from, to, empty } = editor.state.selection;
191
- if (empty) return null;
192
- const rawText = editor.state.doc.textBetween(from, to, '\n');
193
- const text = rawText.trim();
194
- if (!text) return null;
195
- const beforeText = editor.state.doc.textBetween(0, from, '\n').slice(-SURROUNDING_TEXT_LIMIT);
196
- const afterText = editor.state.doc.textBetween(to, editor.state.doc.content.size, '\n').slice(0, SURROUNDING_TEXT_LIMIT);
197
- const selectedContent = editor.state.selection.content().content.toJSON();
198
- return {
199
- selection: {
200
- from,
201
- to,
202
- text,
203
- rawText,
204
- beforeText,
205
- afterText,
206
- model: {
207
- type: 'doc',
208
- ...selectedContent?.length ? {
209
- content: selectedContent
210
- } : {}
211
- }
212
- },
213
- document: {
214
- content,
215
- html: editor.getHTML(),
216
- text: editor.state.doc.textBetween(0, editor.state.doc.content.size, '\n')
217
- }
218
- };
219
- }
220
- function createWorkDocumentSelectionAction(editor, snapshot, getTrackChanges) {
221
- let from = snapshot.selection.from;
222
- let to = snapshot.selection.to;
223
- let stale = false;
224
- let disposed = false;
225
- const handleTransaction = ({ transaction })=>{
226
- if (!transaction.docChanged || stale || disposed) return;
227
- const mappedFrom = transaction.mapping.mapResult(from, 1);
228
- const mappedTo = transaction.mapping.mapResult(to, -1);
229
- from = mappedFrom.pos;
230
- to = mappedTo.pos;
231
- stale = mappedFrom.deletedAcross || mappedTo.deletedAcross || from > to || transaction.doc.textBetween(from, to, '\n') !== snapshot.selection.rawText;
232
- };
233
- editor.on('transaction', handleTransaction);
234
- const currentRange = ()=>{
235
- if (disposed || editor.isDestroyed) return 'editor-unavailable';
236
- if (!editor.isEditable) return 'read-only';
237
- if (stale || from < 0 || to < from || to > editor.state.doc.content.size || editor.state.doc.textBetween(from, to, '\n') !== snapshot.selection.rawText) return 'stale-selection';
238
- return {
239
- from,
240
- to
241
- };
242
- };
243
- const editText = (text, placement)=>{
244
- if ('string' != typeof text || 'replace' !== placement && !text) return {
245
- applied: false,
246
- reason: 'invalid-text'
247
- };
248
- const range = currentRange();
249
- if ('string' == typeof range) return {
250
- applied: false,
251
- reason: range
252
- };
253
- const editRange = 'before' === placement ? {
254
- from: range.from,
255
- to: range.from
256
- } : 'after' === placement ? {
257
- from: range.to,
258
- to: range.to
259
- } : range;
260
- const applied = getTrackChanges() ? editor.commands.replaceDocumentTextWithTrackedChange(editRange.from, editRange.to, text) : editor.chain().focus().setTextSelection(editRange).insertContent(documentPlainTextAsHtml(text)).run();
261
- return applied ? {
262
- applied: true
263
- } : {
264
- applied: false,
265
- reason: 'command-rejected'
266
- };
267
- };
268
- return {
269
- context: {
270
- ...snapshot,
271
- commands: {
272
- copyText: async ()=>{
273
- try {
274
- if ("u" < typeof navigator || !navigator.clipboard?.writeText) return false;
275
- await navigator.clipboard.writeText(snapshot.selection.rawText);
276
- return true;
277
- } catch {
278
- return false;
279
- }
280
- },
281
- insertAfter: (text)=>editText(text, 'after'),
282
- insertBefore: (text)=>editText(text, 'before'),
283
- replaceText: (text)=>editText(text, 'replace')
284
- }
285
- },
286
- dispose: ()=>{
287
- if (disposed) return;
288
- disposed = true;
289
- editor.off('transaction', handleTransaction);
290
- }
291
- };
292
- }
293
- function documentPlainTextAsHtml(value) {
294
- return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;').replace(/\r?\n/g, '<br>');
295
- }
296
307
  function mergeOfficeTiptapExtensions(editorName, builtInExtensions, additionalExtensions) {
297
308
  const names = new Set();
298
309
  for (const extension of builtInExtensions)names.add(extension.name);
@@ -305,4 +316,4 @@ function mergeOfficeTiptapExtensions(editorName, builtInExtensions, additionalEx
305
316
  ...additionalExtensions
306
317
  ];
307
318
  }
308
- export { clampDocumentZoom, createWorkDocumentSelectionAction, createWorkDocumentSelectionSnapshot, customSelectionMenuItems, documentAgentMenuItems, documentCurrentPage, documentCustomSelectionMenuItems, documentPageCount, documentPlainTextAsHtml, documentWordCount, mergeOfficeTiptapExtensions };
319
+ export { createWorkDocumentSelectionAction, createWorkDocumentSelectionSnapshot, customSelectionMenuItems, documentAgentMenuItems, documentCurrentPage, documentCustomSelectionMenuItems, documentPageCount, documentPlainTextAsHtml, documentTextStatistics, mergeOfficeTiptapExtensions };
package/dist/0~9512.js CHANGED
@@ -42,6 +42,7 @@ function useDialogFocusScope({ active = true, onEscape, escapeDisabled = false,
42
42
  if (!isTopScope(activeScope)) return;
43
43
  const currentScope = optionsRef.current.getActiveScope?.() ?? scopeRef.current;
44
44
  if (!currentScope) return;
45
+ if (event.target instanceof Node && currentScope.contains(event.target) && 'Escape' !== event.key && 'Tab' !== event.key) return;
45
46
  handleScopedKeyDown(event, currentScope, optionsRef.current);
46
47
  };
47
48
  const handleDocumentFocus = (event)=>{