@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* explicit style are stored. Row and column metadata is also sparse.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { CellAddress, CellKey, CellValue, ColIndex, RowIndex } from "./cell.ts";
|
|
8
|
+
import type { CellAddress, CellKey, CellValue, ColIndex, RowIndex, StyleRef } from "./cell.ts";
|
|
9
9
|
import { cellKey } from "./cell.ts";
|
|
10
10
|
|
|
11
11
|
export type { ColIndex, RowIndex } from "./cell.ts";
|
|
@@ -23,6 +23,8 @@ export interface RowProperties {
|
|
|
23
23
|
hidden?: boolean;
|
|
24
24
|
/** Whether the row height was set via customHeight attribute. */
|
|
25
25
|
customHeight?: boolean;
|
|
26
|
+
/** Style index reference applied at the row level, if present. */
|
|
27
|
+
styleRef?: StyleRef;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface ColProperties {
|
|
@@ -34,6 +36,8 @@ export interface ColProperties {
|
|
|
34
36
|
hidden?: boolean;
|
|
35
37
|
/** Whether the width was explicitly customized. */
|
|
36
38
|
customWidth?: boolean;
|
|
39
|
+
/** Style index reference applied at the column level, if present. */
|
|
40
|
+
styleRef?: StyleRef;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
@@ -164,6 +168,60 @@ export function getCell(
|
|
|
164
168
|
return sheet.cells.get(cellKey(row, col));
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Set sparse row metadata.
|
|
173
|
+
* Passing `undefined`, or only default fields, removes the row entry.
|
|
174
|
+
*/
|
|
175
|
+
export function setRowProperties(
|
|
176
|
+
sheet: CanonicalSheet,
|
|
177
|
+
rowIndex: RowIndex,
|
|
178
|
+
props: Omit<RowProperties, "rowIndex"> | undefined,
|
|
179
|
+
): void {
|
|
180
|
+
if (!props || isDefaultRowProperties(props)) {
|
|
181
|
+
sheet.rowProps.delete(rowIndex);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
sheet.rowProps.set(rowIndex, {
|
|
186
|
+
rowIndex,
|
|
187
|
+
...props,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function getRowProperties(
|
|
192
|
+
sheet: CanonicalSheet,
|
|
193
|
+
rowIndex: RowIndex,
|
|
194
|
+
): RowProperties | undefined {
|
|
195
|
+
return sheet.rowProps.get(rowIndex);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Set sparse column metadata.
|
|
200
|
+
* Passing `undefined`, or only default fields, removes the column entry.
|
|
201
|
+
*/
|
|
202
|
+
export function setColProperties(
|
|
203
|
+
sheet: CanonicalSheet,
|
|
204
|
+
colIndex: ColIndex,
|
|
205
|
+
props: Omit<ColProperties, "colIndex"> | undefined,
|
|
206
|
+
): void {
|
|
207
|
+
if (!props || isDefaultColProperties(props)) {
|
|
208
|
+
sheet.colProps.delete(colIndex);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
sheet.colProps.set(colIndex, {
|
|
213
|
+
colIndex,
|
|
214
|
+
...props,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getColProperties(
|
|
219
|
+
sheet: CanonicalSheet,
|
|
220
|
+
colIndex: ColIndex,
|
|
221
|
+
): ColProperties | undefined {
|
|
222
|
+
return sheet.colProps.get(colIndex);
|
|
223
|
+
}
|
|
224
|
+
|
|
167
225
|
/**
|
|
168
226
|
* Return the effective address bounds for all occupied cells.
|
|
169
227
|
* Returns `null` when the sheet has no cells.
|
|
@@ -244,3 +302,25 @@ export function findMergesContaining(
|
|
|
244
302
|
col <= m.endCol,
|
|
245
303
|
);
|
|
246
304
|
}
|
|
305
|
+
|
|
306
|
+
function isDefaultRowProperties(
|
|
307
|
+
props: Omit<RowProperties, "rowIndex">,
|
|
308
|
+
): boolean {
|
|
309
|
+
return (
|
|
310
|
+
props.heightPt === undefined &&
|
|
311
|
+
props.hidden === undefined &&
|
|
312
|
+
props.customHeight === undefined &&
|
|
313
|
+
props.styleRef === undefined
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isDefaultColProperties(
|
|
318
|
+
props: Omit<ColProperties, "colIndex">,
|
|
319
|
+
): boolean {
|
|
320
|
+
return (
|
|
321
|
+
props.widthChars === undefined &&
|
|
322
|
+
props.hidden === undefined &&
|
|
323
|
+
props.customWidth === undefined &&
|
|
324
|
+
props.styleRef === undefined
|
|
325
|
+
);
|
|
326
|
+
}
|
|
@@ -158,15 +158,15 @@ export function addSheet(
|
|
|
158
158
|
}
|
|
159
159
|
const orderIndex =
|
|
160
160
|
atIndex !== undefined
|
|
161
|
-
?
|
|
161
|
+
? clampSheetIndex(atIndex, workbook.sheetOrder.length)
|
|
162
162
|
: workbook.sheetOrder.length;
|
|
163
163
|
const sheet = createSheet(sheetId, name, orderIndex);
|
|
164
164
|
workbook.sheets.set(sheetId, sheet);
|
|
165
165
|
|
|
166
|
-
if (atIndex !== undefined &&
|
|
167
|
-
workbook.sheetOrder.splice(
|
|
166
|
+
if (atIndex !== undefined && orderIndex < workbook.sheetOrder.length) {
|
|
167
|
+
workbook.sheetOrder.splice(orderIndex, 0, sheetId);
|
|
168
168
|
// Reindex sheets after the insertion point
|
|
169
|
-
for (let i =
|
|
169
|
+
for (let i = orderIndex + 1; i < workbook.sheetOrder.length; i++) {
|
|
170
170
|
const id = workbook.sheetOrder[i];
|
|
171
171
|
const s = workbook.sheets.get(id);
|
|
172
172
|
if (s) s.orderIndex = i;
|
|
@@ -258,10 +258,10 @@ export function moveSheet(
|
|
|
258
258
|
): void {
|
|
259
259
|
const currentIndex = workbook.sheetOrder.indexOf(sheetId);
|
|
260
260
|
if (currentIndex === -1) throw new Error(`Sheet not found: ${sheetId}`);
|
|
261
|
-
|
|
261
|
+
const clampedTarget = clampSheetIndex(toIndex, workbook.sheetOrder.length - 1);
|
|
262
|
+
if (currentIndex === clampedTarget) return;
|
|
262
263
|
|
|
263
264
|
workbook.sheetOrder.splice(currentIndex, 1);
|
|
264
|
-
const clampedTarget = Math.min(toIndex, workbook.sheetOrder.length);
|
|
265
265
|
workbook.sheetOrder.splice(clampedTarget, 0, sheetId);
|
|
266
266
|
|
|
267
267
|
// Reindex all sheets
|
|
@@ -272,6 +272,10 @@ export function moveSheet(
|
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
function clampSheetIndex(index: number, maxIndex: number): number {
|
|
276
|
+
return Math.max(0, Math.min(index, maxIndex));
|
|
277
|
+
}
|
|
278
|
+
|
|
275
279
|
// ---------------------------------------------------------------------------
|
|
276
280
|
// Shared string table helpers
|
|
277
281
|
// ---------------------------------------------------------------------------
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
createRangeAnchor,
|
|
42
42
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
43
43
|
} from "../core/selection/mapping.ts";
|
|
44
|
+
import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors.ts";
|
|
44
45
|
import { createCommentSidebarProjection } from "../review/store/comment-store.ts";
|
|
45
46
|
import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
|
|
46
47
|
import {
|
|
@@ -212,6 +213,18 @@ export function createDocumentRuntime(
|
|
|
212
213
|
const anchor = params.anchor
|
|
213
214
|
? toInternalAnchorProjection(params.anchor)
|
|
214
215
|
: state.selection.activeRange;
|
|
216
|
+
if (!canCreateDocxCommentAnchor(state.document.content, anchor)) {
|
|
217
|
+
const message =
|
|
218
|
+
"DOCX comments must use a non-empty range that stays within a single paragraph.";
|
|
219
|
+
emitError({
|
|
220
|
+
errorId: createSessionId("comment-anchor", clock()),
|
|
221
|
+
code: "validation_failed",
|
|
222
|
+
isFatal: false,
|
|
223
|
+
message,
|
|
224
|
+
source: "runtime",
|
|
225
|
+
});
|
|
226
|
+
throw new Error(message);
|
|
227
|
+
}
|
|
215
228
|
const authorId = params.authorId ?? options.defaultAuthorId ?? "unknown";
|
|
216
229
|
const createdAt = clock();
|
|
217
230
|
const entries: CommentEntryRecord[] = [
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { RuntimeRenderSnapshot } from "../api/public-types";
|
|
2
|
+
import {
|
|
3
|
+
createDetachedAnchor,
|
|
4
|
+
createNodeAnchor,
|
|
5
|
+
createRangeAnchor,
|
|
6
|
+
} from "../core/selection/mapping.ts";
|
|
7
|
+
import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Session capabilities derived from the runtime snapshot.
|
|
@@ -80,7 +86,11 @@ export function deriveCapabilities(
|
|
|
80
86
|
const canEdit = isReady && !isReadOnly && !hasFatalError;
|
|
81
87
|
const canUndo = snapshot.commandState.canUndo && canEdit;
|
|
82
88
|
const canRedo = snapshot.commandState.canRedo && canEdit;
|
|
83
|
-
const canAddComment =
|
|
89
|
+
const canAddComment =
|
|
90
|
+
canEdit &&
|
|
91
|
+
!snapshot.selection.isCollapsed &&
|
|
92
|
+
Boolean(snapshot.surface) &&
|
|
93
|
+
canCreateDocxCommentAnchor(snapshot.surface, toRuntimeAnchor(snapshot.selection.activeRange));
|
|
84
94
|
const canExport = isReady && !exportBlocked && !hasFatalError;
|
|
85
95
|
|
|
86
96
|
// Revision capabilities
|
|
@@ -136,3 +146,14 @@ export function deriveCapabilities(
|
|
|
136
146
|
hasFatalError,
|
|
137
147
|
};
|
|
138
148
|
}
|
|
149
|
+
|
|
150
|
+
function toRuntimeAnchor(anchor: RuntimeRenderSnapshot["selection"]["activeRange"]) {
|
|
151
|
+
switch (anchor.kind) {
|
|
152
|
+
case "range":
|
|
153
|
+
return createRangeAnchor(anchor.from, anchor.to, anchor.assoc);
|
|
154
|
+
case "node":
|
|
155
|
+
return createNodeAnchor(anchor.at, anchor.assoc);
|
|
156
|
+
case "detached":
|
|
157
|
+
return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -226,6 +226,7 @@ function createTableBlock(
|
|
|
226
226
|
verticalMerge: cell.verticalMerge ?? null,
|
|
227
227
|
colspan: cell.gridSpan ?? 1,
|
|
228
228
|
rowspan: rowSpans.get(`${rowIndex}:${cellIndex}`) ?? 1,
|
|
229
|
+
...(cell.shading?.fill ? { backgroundColor: `#${cell.shading.fill}` } : {}),
|
|
229
230
|
content: cellContent,
|
|
230
231
|
});
|
|
231
232
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Command as PMCommand, EditorState } from "prosemirror-state";
|
|
13
13
|
import {
|
|
14
|
+
CellSelection,
|
|
14
15
|
TableMap,
|
|
15
16
|
addColumnAfter as pmAddColumnAfter,
|
|
16
17
|
addColumnBefore as pmAddColumnBefore,
|
|
@@ -21,10 +22,13 @@ import {
|
|
|
21
22
|
deleteTable,
|
|
22
23
|
goToNextCell,
|
|
23
24
|
mergeCells,
|
|
25
|
+
selectedRect,
|
|
26
|
+
selectionCell,
|
|
24
27
|
splitCell,
|
|
25
28
|
toggleHeaderCell,
|
|
26
29
|
toggleHeaderColumn,
|
|
27
30
|
toggleHeaderRow,
|
|
31
|
+
findTable,
|
|
28
32
|
} from "prosemirror-tables";
|
|
29
33
|
|
|
30
34
|
// prosemirror-tables does not export goToPreviousCell; replicate via direction -1.
|
|
@@ -33,6 +37,25 @@ const goToPreviousCell: PMCommand = (state, dispatch, view) =>
|
|
|
33
37
|
|
|
34
38
|
export type { Command as TableCommand } from "prosemirror-state";
|
|
35
39
|
|
|
40
|
+
export interface TableSelectionDescriptor {
|
|
41
|
+
tableBlockIndex: number;
|
|
42
|
+
selectionKind: "text" | "cell";
|
|
43
|
+
anchorCell: {
|
|
44
|
+
rowIndex: number;
|
|
45
|
+
columnIndex: number;
|
|
46
|
+
};
|
|
47
|
+
headCell: {
|
|
48
|
+
rowIndex: number;
|
|
49
|
+
columnIndex: number;
|
|
50
|
+
};
|
|
51
|
+
rect: {
|
|
52
|
+
top: number;
|
|
53
|
+
left: number;
|
|
54
|
+
bottom: number;
|
|
55
|
+
right: number;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
function tableAtSelection(state: EditorState) {
|
|
37
60
|
const { $head } = state.selection;
|
|
38
61
|
for (let depth = $head.depth; depth > 0; depth -= 1) {
|
|
@@ -92,3 +115,59 @@ export {
|
|
|
92
115
|
goToNextCell,
|
|
93
116
|
goToPreviousCell,
|
|
94
117
|
};
|
|
118
|
+
|
|
119
|
+
export function getTableSelectionDescriptor(
|
|
120
|
+
state: EditorState,
|
|
121
|
+
): TableSelectionDescriptor | null {
|
|
122
|
+
const tableInfo = findTable(state.selection.$from);
|
|
123
|
+
if (!tableInfo) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tableBlockIndex = resolveTopLevelTableIndex(state, tableInfo.pos);
|
|
128
|
+
if (tableBlockIndex < 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const rect = selectedRect(state);
|
|
133
|
+
const isCellSelection = state.selection instanceof CellSelection;
|
|
134
|
+
const anchorCellPos = (
|
|
135
|
+
isCellSelection ? (state.selection as CellSelection).$anchorCell : selectionCell(state)
|
|
136
|
+
).pos - rect.tableStart;
|
|
137
|
+
const headCellPos = (
|
|
138
|
+
isCellSelection ? (state.selection as CellSelection).$headCell : selectionCell(state)
|
|
139
|
+
).pos - rect.tableStart;
|
|
140
|
+
const anchorRect = rect.map.findCell(anchorCellPos);
|
|
141
|
+
const headRect = rect.map.findCell(headCellPos);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
tableBlockIndex,
|
|
145
|
+
selectionKind: isCellSelection ? "cell" : "text",
|
|
146
|
+
anchorCell: {
|
|
147
|
+
rowIndex: anchorRect.top,
|
|
148
|
+
columnIndex: anchorRect.left,
|
|
149
|
+
},
|
|
150
|
+
headCell: {
|
|
151
|
+
rowIndex: headRect.top,
|
|
152
|
+
columnIndex: headRect.left,
|
|
153
|
+
},
|
|
154
|
+
rect: {
|
|
155
|
+
top: rect.top,
|
|
156
|
+
left: rect.left,
|
|
157
|
+
bottom: rect.bottom,
|
|
158
|
+
right: rect.right,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveTopLevelTableIndex(state: EditorState, tablePos: number): number {
|
|
164
|
+
let index = -1;
|
|
165
|
+
|
|
166
|
+
state.doc.forEach((node, offset, currentIndex) => {
|
|
167
|
+
if (offset === tablePos && node.type.spec.tableRole === "table") {
|
|
168
|
+
index = currentIndex;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
@@ -17,6 +17,7 @@ type TableCellAttrs = {
|
|
|
17
17
|
colwidth?: number[] | null;
|
|
18
18
|
gridSpan?: number | null;
|
|
19
19
|
verticalMerge?: "restart" | "continue" | null;
|
|
20
|
+
backgroundColor?: string | null;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
function resolveRenderedColspan(attrs: {
|
|
@@ -52,6 +53,8 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
|
|
|
52
53
|
const gridSpanAttr = dom.getAttribute("data-grid-span");
|
|
53
54
|
const verticalMergeAttr = dom.getAttribute("data-vertical-merge");
|
|
54
55
|
const gridSpan = gridSpanAttr ? Number.parseInt(gridSpanAttr, 10) : colspan;
|
|
56
|
+
const backgroundColor =
|
|
57
|
+
dom.getAttribute("data-cell-background") ?? dom.style.backgroundColor ?? null;
|
|
55
58
|
|
|
56
59
|
return {
|
|
57
60
|
colspan,
|
|
@@ -62,6 +65,7 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
|
|
|
62
65
|
verticalMergeAttr === "restart" || verticalMergeAttr === "continue"
|
|
63
66
|
? verticalMergeAttr
|
|
64
67
|
: null,
|
|
68
|
+
backgroundColor,
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -85,6 +89,10 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
|
|
|
85
89
|
if (nodeAttrs.verticalMerge) {
|
|
86
90
|
attrs["data-vertical-merge"] = nodeAttrs.verticalMerge;
|
|
87
91
|
}
|
|
92
|
+
if (nodeAttrs.backgroundColor) {
|
|
93
|
+
attrs["data-cell-background"] = nodeAttrs.backgroundColor;
|
|
94
|
+
attrs.style = `background-color: ${nodeAttrs.backgroundColor}`;
|
|
95
|
+
}
|
|
88
96
|
|
|
89
97
|
return attrs;
|
|
90
98
|
}
|
|
@@ -117,6 +125,7 @@ const tableCellSpecAttrs = {
|
|
|
117
125
|
colspan: { default: 1, validate: "number" },
|
|
118
126
|
rowspan: { default: 1, validate: "number" },
|
|
119
127
|
colwidth: { default: null, validate: validateColwidth },
|
|
128
|
+
backgroundColor: { default: null },
|
|
120
129
|
} as const;
|
|
121
130
|
|
|
122
131
|
export const tableNodeSpec: NodeSpec = {
|