@beyondwork/docx-react-component 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -1
- package/src/api/public-types.ts +72 -0
- package/src/core/commands/formatting-commands.ts +742 -0
- package/src/core/commands/image-commands.ts +84 -2
- package/src/core/commands/structural-helpers.ts +309 -0
- package/src/core/commands/table-structure-commands.ts +721 -0
- package/src/core/commands/text-commands.ts +166 -1
- package/src/core/selection/review-anchors.ts +89 -0
- package/src/formats/xlsx/io/parse-sheet.ts +177 -7
- package/src/formats/xlsx/io/parse-styles.ts +2 -0
- package/src/formats/xlsx/io/xlsx-session.ts +18 -12
- package/src/formats/xlsx/model/sheet.ts +81 -1
- package/src/formats/xlsx/model/workbook.ts +10 -6
- package/src/runtime/document-runtime.ts +13 -0
- package/src/runtime/session-capabilities.ts +22 -1
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/table-commands.ts +79 -0
- package/src/runtime/table-schema.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +534 -8
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +8 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +7 -5
- package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +3 -0
|
@@ -15,6 +15,8 @@ import { Plugin, PluginKey } from "prosemirror-state";
|
|
|
15
15
|
import type { EditorState, Transaction } from "prosemirror-state";
|
|
16
16
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
17
17
|
|
|
18
|
+
import type { SearchOptions as PublicSearchOptions } from "../../api/public-types";
|
|
19
|
+
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// Public types
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
@@ -26,12 +28,14 @@ export interface SearchResult {
|
|
|
26
28
|
index: number;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
export interface SearchOptions {
|
|
31
|
+
export interface SearchOptions extends PublicSearchOptions {
|
|
30
32
|
caseSensitive?: boolean;
|
|
31
33
|
regex?: boolean;
|
|
32
34
|
highlightColor?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
export const DEFAULT_SEARCH_HIGHLIGHT_COLOR = "#fde68a";
|
|
38
|
+
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
// Plugin state
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
@@ -102,21 +106,8 @@ export function performSearch(
|
|
|
102
106
|
query: string,
|
|
103
107
|
options: SearchOptions = {},
|
|
104
108
|
): SearchResult[] {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const { caseSensitive = false, regex = false } = options;
|
|
108
|
-
|
|
109
|
-
let pattern: RegExp;
|
|
110
|
-
try {
|
|
111
|
-
if (regex) {
|
|
112
|
-
pattern = new RegExp(query, caseSensitive ? "g" : "gi");
|
|
113
|
-
} else {
|
|
114
|
-
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
115
|
-
pattern = new RegExp(escaped, caseSensitive ? "g" : "gi");
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
109
|
+
const pattern = buildSearchPattern(query, options);
|
|
110
|
+
if (!pattern) return [];
|
|
120
111
|
|
|
121
112
|
const results: SearchResult[] = [];
|
|
122
113
|
|
|
@@ -132,12 +123,81 @@ export function performSearch(
|
|
|
132
123
|
text: match[0],
|
|
133
124
|
index: results.length,
|
|
134
125
|
});
|
|
126
|
+
|
|
127
|
+
if (match[0].length === 0) {
|
|
128
|
+
pattern.lastIndex += 1;
|
|
129
|
+
}
|
|
135
130
|
}
|
|
136
131
|
});
|
|
137
132
|
|
|
138
133
|
return results;
|
|
139
134
|
}
|
|
140
135
|
|
|
136
|
+
export function findSearchMatches(
|
|
137
|
+
text: string,
|
|
138
|
+
query: string,
|
|
139
|
+
options: SearchOptions = {},
|
|
140
|
+
): SearchResult[] {
|
|
141
|
+
const pattern = buildSearchPattern(query, options);
|
|
142
|
+
if (!pattern) return [];
|
|
143
|
+
|
|
144
|
+
const results: SearchResult[] = [];
|
|
145
|
+
let match: RegExpExecArray | null;
|
|
146
|
+
pattern.lastIndex = 0;
|
|
147
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
148
|
+
results.push({
|
|
149
|
+
from: match.index,
|
|
150
|
+
to: match.index + match[0].length,
|
|
151
|
+
text: match[0],
|
|
152
|
+
index: results.length,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (match[0].length === 0) {
|
|
156
|
+
pattern.lastIndex += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createSearchExcerpt(
|
|
164
|
+
text: string,
|
|
165
|
+
from: number,
|
|
166
|
+
to: number,
|
|
167
|
+
radius = 24,
|
|
168
|
+
): string {
|
|
169
|
+
const safeFrom = Math.max(0, Math.min(from, text.length));
|
|
170
|
+
const safeTo = Math.max(safeFrom, Math.min(to, text.length));
|
|
171
|
+
const start = Math.max(0, safeFrom - radius);
|
|
172
|
+
const end = Math.min(text.length, safeTo + radius);
|
|
173
|
+
const prefix = start > 0 ? "…" : "";
|
|
174
|
+
const suffix = end < text.length ? "…" : "";
|
|
175
|
+
return `${prefix}${text.slice(start, end)}${suffix}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildSearchPattern(
|
|
179
|
+
query: string,
|
|
180
|
+
options: SearchOptions,
|
|
181
|
+
): RegExp | null {
|
|
182
|
+
if (!query) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const caseSensitive = options.matchCase ?? options.caseSensitive ?? false;
|
|
187
|
+
const regex = options.regex ?? false;
|
|
188
|
+
const wholeWord = options.wholeWord ?? false;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const source = regex
|
|
192
|
+
? query
|
|
193
|
+
: query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
194
|
+
const wrapped = wholeWord ? `\\b${source}\\b` : source;
|
|
195
|
+
return new RegExp(wrapped, caseSensitive ? "g" : "gi");
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
141
201
|
// ---------------------------------------------------------------------------
|
|
142
202
|
// Clear helper (ProseMirror Command signature)
|
|
143
203
|
// ---------------------------------------------------------------------------
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type FocusEventHandler,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react";
|
|
2
9
|
import { EditorView } from "prosemirror-view";
|
|
3
10
|
|
|
4
11
|
import type {
|
|
5
12
|
EditorUser,
|
|
6
13
|
RuntimeRenderSnapshot,
|
|
14
|
+
SearchOptions,
|
|
15
|
+
SearchResultSnapshot,
|
|
7
16
|
SelectionSnapshot,
|
|
8
17
|
} from "../../api/public-types";
|
|
18
|
+
import {
|
|
19
|
+
getTableSelectionDescriptor,
|
|
20
|
+
type TableSelectionDescriptor,
|
|
21
|
+
} from "../../runtime/table-commands.ts";
|
|
9
22
|
import {
|
|
10
23
|
createCommentDecorationModel,
|
|
11
24
|
type MarkupDisplay,
|
|
@@ -18,6 +31,14 @@ import {
|
|
|
18
31
|
} from "./pm-command-bridge";
|
|
19
32
|
import { buildDecorations } from "./pm-decorations";
|
|
20
33
|
import type { PositionMap } from "./pm-position-map";
|
|
34
|
+
import {
|
|
35
|
+
clearSearch as clearSearchPlugin,
|
|
36
|
+
createSearchExcerpt,
|
|
37
|
+
createSearchPlugin,
|
|
38
|
+
DEFAULT_SEARCH_HIGHLIGHT_COLOR,
|
|
39
|
+
performSearch,
|
|
40
|
+
searchPluginKey,
|
|
41
|
+
} from "./search-plugin";
|
|
21
42
|
import { tableNodeViews } from "./tw-table-node-view";
|
|
22
43
|
|
|
23
44
|
/**
|
|
@@ -43,7 +64,16 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
43
64
|
onRevisionActivated?: (revisionId: string) => void;
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
export
|
|
67
|
+
export interface TwProseMirrorSurfaceRef {
|
|
68
|
+
search(query: string, options?: SearchOptions): SearchResultSnapshot[];
|
|
69
|
+
clearSearch(): void;
|
|
70
|
+
getTableSelection(): TableSelectionDescriptor | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const TwProseMirrorSurface = forwardRef<
|
|
74
|
+
TwProseMirrorSurfaceRef,
|
|
75
|
+
TwProseMirrorSurfaceProps
|
|
76
|
+
>(function TwProseMirrorSurface(props, ref) {
|
|
47
77
|
const {
|
|
48
78
|
currentUser,
|
|
49
79
|
snapshot,
|
|
@@ -61,6 +91,7 @@ export function TwProseMirrorSurface(props: TwProseMirrorSurfaceProps) {
|
|
|
61
91
|
const viewRef = useRef<EditorView | null>(null);
|
|
62
92
|
const positionMapRef = useRef<PositionMap | null>(null);
|
|
63
93
|
const callbacksRef = useRef<CommandBridgeCallbacks | null>(null);
|
|
94
|
+
const activeSearchRef = useRef<{ query: string; options: SearchOptions } | null>(null);
|
|
64
95
|
|
|
65
96
|
// Keep callbacks ref up to date (avoids stale closures in PM plugins)
|
|
66
97
|
callbacksRef.current = {
|
|
@@ -91,18 +122,21 @@ export function TwProseMirrorSurface(props: TwProseMirrorSurfaceProps) {
|
|
|
91
122
|
|
|
92
123
|
// Create PM plugins (stable across renders — callbacks accessed via ref)
|
|
93
124
|
const plugins = useMemo(() => {
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
return [
|
|
126
|
+
...createCommandBridgePlugins({
|
|
127
|
+
onInsertText: (text) => callbacksRef.current?.onInsertText(text),
|
|
128
|
+
onDeleteBackward: () => callbacksRef.current?.onDeleteBackward(),
|
|
129
|
+
onDeleteForward: () => callbacksRef.current?.onDeleteForward(),
|
|
130
|
+
onSplitParagraph: () => callbacksRef.current?.onSplitParagraph(),
|
|
131
|
+
onInsertHardBreak: () => callbacksRef.current?.onInsertHardBreak(),
|
|
132
|
+
onInsertTab: () => callbacksRef.current?.onInsertTab(),
|
|
133
|
+
onUndo: () => callbacksRef.current?.onUndo(),
|
|
134
|
+
onRedo: () => callbacksRef.current?.onRedo(),
|
|
135
|
+
onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
|
|
136
|
+
getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
|
|
137
|
+
}),
|
|
138
|
+
createSearchPlugin(),
|
|
139
|
+
];
|
|
106
140
|
}, []);
|
|
107
141
|
|
|
108
142
|
// Create or update PM view whenever surface becomes available or changes.
|
|
@@ -148,6 +182,13 @@ export function TwProseMirrorSurface(props: TwProseMirrorSurfaceProps) {
|
|
|
148
182
|
});
|
|
149
183
|
viewRef.current.updateState(state);
|
|
150
184
|
}
|
|
185
|
+
|
|
186
|
+
if (activeSearchRef.current) {
|
|
187
|
+
applySearch(
|
|
188
|
+
activeSearchRef.current.query,
|
|
189
|
+
activeSearchRef.current.options,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
151
192
|
}, [snapshot.revisionToken, surface, commentModel, revisionModel, markupDisplay, canEdit]);
|
|
152
193
|
|
|
153
194
|
// Cleanup on unmount
|
|
@@ -158,6 +199,90 @@ export function TwProseMirrorSurface(props: TwProseMirrorSurfaceProps) {
|
|
|
158
199
|
};
|
|
159
200
|
}, []);
|
|
160
201
|
|
|
202
|
+
useImperativeHandle(
|
|
203
|
+
ref,
|
|
204
|
+
() => ({
|
|
205
|
+
search: (query, options = {}) => {
|
|
206
|
+
const normalizedQuery = query.trim();
|
|
207
|
+
if (!normalizedQuery) {
|
|
208
|
+
activeSearchRef.current = null;
|
|
209
|
+
clearLiveSearch();
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
activeSearchRef.current = { query: normalizedQuery, options };
|
|
214
|
+
return applySearch(normalizedQuery, options);
|
|
215
|
+
},
|
|
216
|
+
clearSearch: () => {
|
|
217
|
+
activeSearchRef.current = null;
|
|
218
|
+
clearLiveSearch();
|
|
219
|
+
},
|
|
220
|
+
getTableSelection: () => {
|
|
221
|
+
const view = viewRef.current;
|
|
222
|
+
if (!view) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return getTableSelectionDescriptor(view.state);
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
[snapshot.selection, snapshot.surface],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
|
|
232
|
+
const view = viewRef.current;
|
|
233
|
+
const positionMap = positionMapRef.current;
|
|
234
|
+
if (!view || !positionMap) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const rawResults = performSearch(view.state, query, options).slice(
|
|
239
|
+
0,
|
|
240
|
+
options.limit ?? Number.POSITIVE_INFINITY,
|
|
241
|
+
);
|
|
242
|
+
view.dispatch(
|
|
243
|
+
view.state.tr.setMeta(searchPluginKey, {
|
|
244
|
+
results: rawResults,
|
|
245
|
+
highlightColor: DEFAULT_SEARCH_HIGHLIGHT_COLOR,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const activeResultIndex = getActiveSearchResultIndex(
|
|
250
|
+
rawResults,
|
|
251
|
+
(position) => positionMap.pmToRuntime(position),
|
|
252
|
+
snapshot.selection,
|
|
253
|
+
);
|
|
254
|
+
const plainText = snapshot.surface?.plainText ?? "";
|
|
255
|
+
return rawResults.map((result, index) => {
|
|
256
|
+
const runtimeFrom = positionMap.pmToRuntime(result.from);
|
|
257
|
+
const runtimeTo = positionMap.pmToRuntime(result.to);
|
|
258
|
+
return {
|
|
259
|
+
resultId: `search-result-${index}`,
|
|
260
|
+
anchor: {
|
|
261
|
+
kind: "range",
|
|
262
|
+
from: runtimeFrom,
|
|
263
|
+
to: runtimeTo,
|
|
264
|
+
assoc: {
|
|
265
|
+
start: -1,
|
|
266
|
+
end: 1,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
excerpt: createSearchExcerpt(plainText, runtimeFrom, runtimeTo),
|
|
270
|
+
isActive: index === activeResultIndex,
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function clearLiveSearch(): void {
|
|
276
|
+
const view = viewRef.current;
|
|
277
|
+
if (!view) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
clearSearchPlugin(view.state, (tr) => {
|
|
282
|
+
view.dispatch(tr);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
161
286
|
const fontClass =
|
|
162
287
|
markupDisplay === "clean"
|
|
163
288
|
? "font-[family-name:var(--font-legal-sans)]"
|
|
@@ -212,4 +337,27 @@ export function TwProseMirrorSurface(props: TwProseMirrorSurfaceProps) {
|
|
|
212
337
|
) : null}
|
|
213
338
|
</section>
|
|
214
339
|
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
function getActiveSearchResultIndex(
|
|
343
|
+
results: Array<{ from: number; to: number }>,
|
|
344
|
+
toRuntimePosition: (position: number) => number,
|
|
345
|
+
selection: SelectionSnapshot,
|
|
346
|
+
): number {
|
|
347
|
+
if (results.length === 0) {
|
|
348
|
+
return -1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const selectionFrom = Math.min(selection.anchor, selection.head);
|
|
352
|
+
const selectionTo = Math.max(selection.anchor, selection.head);
|
|
353
|
+
const activeIndex = results.findIndex((result) => {
|
|
354
|
+
const from = toRuntimePosition(result.from);
|
|
355
|
+
const to = toRuntimePosition(result.to);
|
|
356
|
+
if (selectionFrom === selectionTo) {
|
|
357
|
+
return selectionFrom >= from && selectionFrom <= to;
|
|
358
|
+
}
|
|
359
|
+
return selectionFrom < to && selectionTo > from;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return activeIndex >= 0 ? activeIndex : 0;
|
|
215
363
|
}
|
|
@@ -27,6 +27,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
27
27
|
activeRevisionId?: string;
|
|
28
28
|
showTrackedChanges: boolean;
|
|
29
29
|
selectionPreview?: string | null;
|
|
30
|
+
addCommentDisabledReason?: string;
|
|
30
31
|
onViewModeChange: (value: ViewMode) => void;
|
|
31
32
|
onActiveRailTabChange: (value: ReviewRailTab) => void;
|
|
32
33
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
@@ -92,6 +93,8 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
|
|
|
92
93
|
<TwSelectionToolbar
|
|
93
94
|
selectionPreview={props.selectionPreview}
|
|
94
95
|
readOnly={snapshot.readOnly}
|
|
96
|
+
canAddComment={props.capabilities?.canAddComment}
|
|
97
|
+
disabledReason={props.addCommentDisabledReason}
|
|
95
98
|
onAddComment={props.onAddComment}
|
|
96
99
|
/>
|
|
97
100
|
</div>
|