@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.
@@ -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
- ? Math.min(atIndex, workbook.sheetOrder.length)
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 && atIndex < workbook.sheetOrder.length) {
167
- workbook.sheetOrder.splice(atIndex, 0, sheetId);
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 = atIndex + 1; i < workbook.sheetOrder.length; 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
- if (currentIndex === toIndex) return;
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 = canEdit && !snapshot.selection.isCollapsed;
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 = {