@rowsncolumns/spreadsheet-state 28.0.67 → 28.0.69

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 (41) hide show
  1. package/dist/cjs/helpers/sheet-title.d.ts +3 -0
  2. package/dist/cjs/helpers/sheet-title.d.ts.map +1 -1
  3. package/dist/cjs/helpers/sheet-title.js +10 -1
  4. package/dist/cjs/hooks/use-on-copy.d.ts +8 -1
  5. package/dist/cjs/hooks/use-on-copy.d.ts.map +1 -1
  6. package/dist/cjs/hooks/use-on-copy.js +248 -153
  7. package/dist/cjs/hooks/use-on-rename-sheet.js +2 -2
  8. package/dist/cjs/hooks/use-spreadsheet-state.d.ts +1 -1
  9. package/dist/cjs/interface/delete-column.d.ts.map +1 -1
  10. package/dist/cjs/interface/delete-column.js +62 -60
  11. package/dist/cjs/interface/delete-table-column.d.ts.map +1 -1
  12. package/dist/cjs/interface/delete-table-column.js +4 -3
  13. package/dist/cjs/interface/delete-table-row.d.ts.map +1 -1
  14. package/dist/cjs/interface/delete-table-row.js +19 -8
  15. package/dist/cjs/interface/insert-column.d.ts.map +1 -1
  16. package/dist/cjs/interface/insert-column.js +5 -4
  17. package/dist/cjs/interface/insert-table-row.d.ts.map +1 -1
  18. package/dist/cjs/interface/insert-table-row.js +19 -9
  19. package/dist/cjs/interface/update-sheet.d.ts.map +1 -1
  20. package/dist/cjs/interface/update-sheet.js +4 -1
  21. package/dist/esm/helpers/sheet-title.d.ts +3 -0
  22. package/dist/esm/helpers/sheet-title.d.ts.map +1 -1
  23. package/dist/esm/helpers/sheet-title.js +9 -0
  24. package/dist/esm/hooks/use-on-copy.d.ts +8 -1
  25. package/dist/esm/hooks/use-on-copy.d.ts.map +1 -1
  26. package/dist/esm/hooks/use-on-copy.js +196 -145
  27. package/dist/esm/hooks/use-on-rename-sheet.js +2 -2
  28. package/dist/esm/hooks/use-spreadsheet-state.d.ts +1 -1
  29. package/dist/esm/interface/delete-column.d.ts.map +1 -1
  30. package/dist/esm/interface/delete-column.js +30 -18
  31. package/dist/esm/interface/delete-table-column.d.ts.map +1 -1
  32. package/dist/esm/interface/delete-table-column.js +4 -2
  33. package/dist/esm/interface/delete-table-row.d.ts.map +1 -1
  34. package/dist/esm/interface/delete-table-row.js +17 -8
  35. package/dist/esm/interface/insert-column.d.ts.map +1 -1
  36. package/dist/esm/interface/insert-column.js +5 -3
  37. package/dist/esm/interface/insert-table-row.d.ts.map +1 -1
  38. package/dist/esm/interface/insert-table-row.js +17 -9
  39. package/dist/esm/interface/update-sheet.d.ts.map +1 -1
  40. package/dist/esm/interface/update-sheet.js +4 -1
  41. package/package.json +24 -24
@@ -2,6 +2,7 @@ import { areaInsideArea, cellIdentifier, } from "@rowsncolumns/grid";
2
2
  import { getBandedCells, getStringifiedColor, } from "@rowsncolumns/spreadsheet";
3
3
  import { cellToAddress, getStyleId, getCellUserEnteredValue, getExtendedValueFormula, isNil, iterateCellsInRange, } from "@rowsncolumns/utils";
4
4
  import { useCallback, useMemo } from "react";
5
+ import { useShowError } from "../atoms/error-state.atom";
5
6
  import { isStyleReference } from "../sheet-cell";
6
7
  const escapeHtmlAttribute = (value) => value
7
8
  .replace(/&/g, "&")
@@ -9,6 +10,46 @@ const escapeHtmlAttribute = (value) => value
9
10
  .replace(/'/g, "'")
10
11
  .replace(/</g, "&lt;")
11
12
  .replace(/>/g, "&gt;");
13
+ const MULTIPLE_SELECTIONS_ERROR = "This action won't work on multiple selections.";
14
+ /**
15
+ * Excel/Sheets copy a multi-area selection only when every area spans the same
16
+ * rows (areas sit side by side, left to right) or the same columns (areas stack,
17
+ * top to bottom). Returns the row and column indexes of the merged rectangle, or
18
+ * undefined for any other shape.
19
+ */
20
+ export const getCopiedRowsAndColumns = (copiedSelections) => {
21
+ const ranges = copiedSelections.map(({ range }) => range);
22
+ const [first] = ranges;
23
+ if (!first) {
24
+ return [[], []];
25
+ }
26
+ const sameRows = ranges.every((range) => range.startRowIndex === first.startRowIndex &&
27
+ range.endRowIndex === first.endRowIndex);
28
+ const sameColumns = ranges.every((range) => range.startColumnIndex === first.startColumnIndex &&
29
+ range.endColumnIndex === first.endColumnIndex);
30
+ if (!sameRows && !sameColumns) {
31
+ return undefined;
32
+ }
33
+ const rowRanges = sameRows
34
+ ? [first]
35
+ : [...ranges].sort((a, b) => a.startRowIndex - b.startRowIndex);
36
+ const columnRanges = sameColumns
37
+ ? [first]
38
+ : [...ranges].sort((a, b) => a.startColumnIndex - b.startColumnIndex);
39
+ const rows = [];
40
+ for (const { startRowIndex, endRowIndex } of rowRanges) {
41
+ for (let rowIndex = startRowIndex; rowIndex <= endRowIndex; rowIndex++) {
42
+ rows.push(rowIndex);
43
+ }
44
+ }
45
+ const columns = [];
46
+ for (const { startColumnIndex, endColumnIndex } of columnRanges) {
47
+ for (let columnIndex = startColumnIndex; columnIndex <= endColumnIndex; columnIndex++) {
48
+ columns.push(columnIndex);
49
+ }
50
+ }
51
+ return [rows, columns];
52
+ };
12
53
  /**
13
54
  * Called when user copies a selection from sheet
14
55
  * 1. User can copy from any cell
@@ -19,7 +60,18 @@ const escapeHtmlAttribute = (value) => value
19
60
  export const useOnCopy = ({ theme, merges, tables: sheetTables, bandedRanges, getFormattedValue, getCellData, getColumnWidth, getRowHeight, cellXfs, sharedStrings, getEffectiveFormat, isHiddenRow, isColumnHidden, }) => {
20
61
  // Merge cell map
21
62
  const mergedCellMap = useMergedCellMap(merges);
22
- return useCallback((e, sheetId, copiedSelections) => {
63
+ const showError = useShowError();
64
+ return useCallback((e, sheetId, copiedSelections, isCutOperation) => {
65
+ // Excel refuses to cut a multi-area selection, and only copies one whose
66
+ // areas share the same rows or the same columns.
67
+ const rowsAndColumns = isCutOperation && copiedSelections.length > 1
68
+ ? undefined
69
+ : getCopiedRowsAndColumns(copiedSelections);
70
+ if (!rowsAndColumns) {
71
+ showError({ message: MULTIPLE_SELECTIONS_ERROR });
72
+ return false;
73
+ }
74
+ const [rows, columns] = rowsAndColumns;
23
75
  const isRowHidden = isHiddenRow ?? (() => false);
24
76
  const isColHidden = isColumnHidden ?? (() => false);
25
77
  // Table styling (header fill, banded row/column fills, table borders)
@@ -70,15 +122,12 @@ export const useOnCopy = ({ theme, merges, tables: sheetTables, bandedRanges, ge
70
122
  }
71
123
  };
72
124
  let colGroups = `<colgroup>`;
73
- copiedSelections.forEach(({ range }) => {
74
- const { startColumnIndex, endColumnIndex } = range;
75
- for (let columnIndex = startColumnIndex; columnIndex <= endColumnIndex; columnIndex++) {
76
- if (isColHidden(sheetId, columnIndex))
77
- continue;
78
- const cellWidth = getColumnWidth(sheetId, columnIndex);
79
- colGroups += `<col width="${cellWidth}" />`;
80
- }
81
- });
125
+ for (const columnIndex of columns) {
126
+ if (isColHidden(sheetId, columnIndex))
127
+ continue;
128
+ const cellWidth = getColumnWidth(sheetId, columnIndex);
129
+ colGroups += `<col width="${cellWidth}" />`;
130
+ }
82
131
  colGroups += `</colgroup>`;
83
132
  let htmlString = `<html xmlns:v="urn:schemas-microsoft-com:vml"
84
133
  xmlns:o="urn:schemas-microsoft-com:office:office"
@@ -90,9 +139,9 @@ export const useOnCopy = ({ theme, merges, tables: sheetTables, bandedRanges, ge
90
139
  </style>
91
140
  <style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><body>
92
141
  <table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none">${colGroups}<tbody>`;
93
- for (const { range } of copiedSelections) {
94
- const { startRowIndex, startColumnIndex, endColumnIndex, endRowIndex } = range;
95
- /* Check if user is copying a table */
142
+ /* Check if user is copying a table */
143
+ if (copiedSelections.length === 1) {
144
+ const { range } = copiedSelections[0];
96
145
  const table = sheetTables?.find((table) => areaInsideArea(table.range, range) && table.sheetId === sheetId);
97
146
  if (table) {
98
147
  // We need to set a rowDelta and colDelta
@@ -105,147 +154,147 @@ export const useOnCopy = ({ theme, merges, tables: sheetTables, bandedRanges, ge
105
154
  colDelta,
106
155
  });
107
156
  }
108
- /* Iterate over all cells */
109
- for (let rowIndex = startRowIndex; rowIndex <= endRowIndex; rowIndex++) {
110
- if (isRowHidden(sheetId, rowIndex)) {
157
+ }
158
+ /* Iterate over all cells */
159
+ for (const rowIndex of rows) {
160
+ if (isRowHidden(sheetId, rowIndex)) {
161
+ continue;
162
+ }
163
+ const rowHeight = getRowHeight(sheetId, rowIndex);
164
+ htmlString += `<tr style="height:${rowHeight}px">`;
165
+ const csvRow = [];
166
+ const jsonRow = [];
167
+ for (const columnIndex of columns) {
168
+ if (isColHidden(sheetId, columnIndex)) {
111
169
  continue;
112
170
  }
113
- const rowHeight = getRowHeight(sheetId, rowIndex);
114
- htmlString += `<tr style="height:${rowHeight}px">`;
115
- const csvRow = [];
116
- const jsonRow = [];
117
- for (let columnIndex = startColumnIndex; columnIndex <= endColumnIndex; columnIndex++) {
118
- if (isColHidden(sheetId, columnIndex)) {
119
- continue;
120
- }
121
- const mergedCellInfo = mergedCellMap.get(cellIdentifier(rowIndex, columnIndex));
122
- const isMasterCell = mergedCellInfo ? mergedCellInfo[0] : true;
123
- if (!isMasterCell) {
124
- jsonRow.push({
125
- data: undefined,
126
- });
127
- continue;
128
- }
129
- const [rowspan, colspan] = mergedCellInfo
130
- ? mergedCellInfo[1]
131
- : [1, 1];
132
- const formattedValue = getFormattedValue(sheetId, rowIndex, columnIndex) ?? "";
133
- const cellData = getCellData(sheetId, rowIndex, columnIndex);
134
- let normalizedCellData = cellData;
135
- if (cellData && typeof cellData === "object") {
136
- normalizedCellData = { ...cellData };
137
- if (!isNil(normalizedCellData.ss)) {
138
- normalizedCellData.ss = collectSharedString(String(normalizedCellData.ss));
139
- }
140
- collectCellXfs(normalizedCellData.uf);
141
- collectCellXfs(normalizedCellData.ef);
142
- }
143
- const formula = getExtendedValueFormula(getCellUserEnteredValue(cellData));
144
- const origin = cellToAddress({ rowIndex, columnIndex }) ??
145
- `${rowIndex}:${columnIndex}`;
146
- const formulaAttr = formula
147
- ? ` data-sheets-formula="${escapeHtmlAttribute(formula)}" data-formula="${escapeHtmlAttribute(formula)}" data-sheets-formula-origin="${escapeHtmlAttribute(origin)}" data-formula-origin="${escapeHtmlAttribute(origin)}"`
148
- : "";
149
- const effectiveFormat = getEffectiveFormat?.(sheetId, rowIndex, columnIndex);
150
- const bandedFormat = bandedCells?.get(cellIdentifier(rowIndex, columnIndex));
151
- const numberFormatType = effectiveFormat?.numberFormat?.type;
152
- const numberFormatPattern = effectiveFormat?.numberFormat?.pattern;
153
- const numberFormatAttr = numberFormatType
154
- ? ` data-sheets-numberformat-type="${escapeHtmlAttribute(numberFormatType)}"${numberFormatPattern
155
- ? ` data-sheets-numberformat-pattern="${escapeHtmlAttribute(numberFormatPattern)}"`
156
- : ""}`
157
- : "";
158
- // Alignment is encoded as data attributes so paste doesn't have
159
- // to trust `getComputedStyle().textAlign`, which fabricates a
160
- // CSS-spec default of `"start"` even when the value was never set.
161
- const horizontalAlignment = effectiveFormat?.horizontalAlignment;
162
- const verticalAlignment = effectiveFormat?.verticalAlignment;
163
- const alignmentAttr = `${horizontalAlignment
164
- ? ` data-sheets-horizontal-alignment="${escapeHtmlAttribute(horizontalAlignment)}"`
165
- : ""}${verticalAlignment
166
- ? ` data-sheets-vertical-alignment="${escapeHtmlAttribute(verticalAlignment)}"`
167
- : ""}`;
168
- // Csv
169
- csvRow.push(formattedValue);
170
- // Style
171
- const isItalic = effectiveFormat?.textFormat?.italic;
172
- const isBold = effectiveFormat?.textFormat?.bold;
173
- const isUnderline = effectiveFormat?.textFormat?.underline;
174
- const isStrikeThrough = effectiveFormat?.textFormat?.strikethrough;
175
- const baselineOffset = effectiveFormat?.textFormat?.baselineOffset;
176
- // Cell-level formatting wins over the banded (table style) layer,
177
- // matching the canvas grid's merge order.
178
- const cellBorders = effectiveFormat?.borders ?? bandedFormat?.borders;
179
- const borderTop = cellBorders?.top;
180
- const borderBottom = cellBorders?.bottom;
181
- const borderLeft = cellBorders?.left;
182
- const borderRight = cellBorders?.right;
183
- const textDecoration = `${isUnderline ? "underline" : ""} ${isStrikeThrough ? "line-through" : ""}`.trim();
184
- const wordWrap = effectiveFormat?.wrapStrategy === "wrap";
185
- const style = theme
186
- ? {
187
- "background-color": getStringifiedColor(effectiveFormat?.backgroundColor ??
188
- bandedFormat?.backgroundColor, theme),
189
- color: getStringifiedColor(effectiveFormat?.textFormat?.color ??
190
- bandedFormat?.textFormat?.color, theme),
191
- padding: "2px 3px 2px 3px",
192
- overflow: "hidden",
193
- "font-weight": isBold ? "700" : "normal",
194
- "font-family": effectiveFormat?.textFormat?.fontFamily,
195
- "font-style": isItalic ? "italic" : "normal",
196
- "text-decoration": textDecoration
197
- ? textDecoration
198
- : undefined,
199
- "font-size": `${effectiveFormat?.textFormat?.fontSize ?? 10}pt`,
200
- "text-align": effectiveFormat?.horizontalAlignment,
201
- "vertical-align": effectiveFormat?.verticalAlignment,
202
- "border-top": borderTop
203
- ? `${borderTop.width}px solid ${getStringifiedColor(borderTop.color, theme)}`
204
- : undefined,
205
- "border-left": borderLeft
206
- ? `${borderLeft.width}px solid ${getStringifiedColor(borderLeft.color, theme)}`
207
- : undefined,
208
- "border-right": borderRight
209
- ? `${borderRight.width}px solid ${getStringifiedColor(borderRight.color, theme)}`
210
- : undefined,
211
- "border-bottom": borderBottom
212
- ? `${borderBottom.width}px solid ${getStringifiedColor(borderBottom.color, theme)}`
213
- : undefined,
214
- "text-wrap": wordWrap ? "wrap" : undefined,
215
- }
216
- : {};
217
- // html
218
- const innerHtml = baselineOffset === "superscript"
219
- ? `<sup>${formattedValue}</sup>`
220
- : baselineOffset === "subscript"
221
- ? `<sub>${formattedValue}</sub>`
222
- : formattedValue;
223
- htmlString +=
224
- `<td${formulaAttr}${numberFormatAttr}${alignmentAttr} ${rowspan > 1 ? `rowspan="${rowspan}"` : ""} ${colspan > 1 ? `colspan="${colspan}"` : ""} style='${Object.entries(style)
225
- .filter(([_, v]) => !isNil(v))
226
- .map(([k, v]) => `${k}:${v}`)
227
- .join(";")}'>` +
228
- innerHtml +
229
- "</td>";
230
- // json
171
+ const mergedCellInfo = mergedCellMap.get(cellIdentifier(rowIndex, columnIndex));
172
+ const isMasterCell = mergedCellInfo ? mergedCellInfo[0] : true;
173
+ if (!isMasterCell) {
231
174
  jsonRow.push({
232
- data: normalizedCellData,
233
- sourceCell: { rowIndex, columnIndex },
234
- rowspan,
235
- colspan,
175
+ data: undefined,
236
176
  });
177
+ continue;
237
178
  }
238
- /* Join with ',' for csv */
239
- csv.push(csvRow.join(","));
240
- // Plain text
241
- plainText.push(csvRow.join("\t"));
179
+ const [rowspan, colspan] = mergedCellInfo
180
+ ? mergedCellInfo[1]
181
+ : [1, 1];
182
+ const formattedValue = getFormattedValue(sheetId, rowIndex, columnIndex) ?? "";
183
+ const cellData = getCellData(sheetId, rowIndex, columnIndex);
184
+ let normalizedCellData = cellData;
185
+ if (cellData && typeof cellData === "object") {
186
+ normalizedCellData = { ...cellData };
187
+ if (!isNil(normalizedCellData.ss)) {
188
+ normalizedCellData.ss = collectSharedString(String(normalizedCellData.ss));
189
+ }
190
+ collectCellXfs(normalizedCellData.uf);
191
+ collectCellXfs(normalizedCellData.ef);
192
+ }
193
+ const formula = getExtendedValueFormula(getCellUserEnteredValue(cellData));
194
+ const origin = cellToAddress({ rowIndex, columnIndex }) ??
195
+ `${rowIndex}:${columnIndex}`;
196
+ const formulaAttr = formula
197
+ ? ` data-sheets-formula="${escapeHtmlAttribute(formula)}" data-formula="${escapeHtmlAttribute(formula)}" data-sheets-formula-origin="${escapeHtmlAttribute(origin)}" data-formula-origin="${escapeHtmlAttribute(origin)}"`
198
+ : "";
199
+ const effectiveFormat = getEffectiveFormat?.(sheetId, rowIndex, columnIndex);
200
+ const bandedFormat = bandedCells?.get(cellIdentifier(rowIndex, columnIndex));
201
+ const numberFormatType = effectiveFormat?.numberFormat?.type;
202
+ const numberFormatPattern = effectiveFormat?.numberFormat?.pattern;
203
+ const numberFormatAttr = numberFormatType
204
+ ? ` data-sheets-numberformat-type="${escapeHtmlAttribute(numberFormatType)}"${numberFormatPattern
205
+ ? ` data-sheets-numberformat-pattern="${escapeHtmlAttribute(numberFormatPattern)}"`
206
+ : ""}`
207
+ : "";
208
+ // Alignment is encoded as data attributes so paste doesn't have
209
+ // to trust `getComputedStyle().textAlign`, which fabricates a
210
+ // CSS-spec default of `"start"` even when the value was never set.
211
+ const horizontalAlignment = effectiveFormat?.horizontalAlignment;
212
+ const verticalAlignment = effectiveFormat?.verticalAlignment;
213
+ const alignmentAttr = `${horizontalAlignment
214
+ ? ` data-sheets-horizontal-alignment="${escapeHtmlAttribute(horizontalAlignment)}"`
215
+ : ""}${verticalAlignment
216
+ ? ` data-sheets-vertical-alignment="${escapeHtmlAttribute(verticalAlignment)}"`
217
+ : ""}`;
218
+ // Csv
219
+ csvRow.push(formattedValue);
220
+ // Style
221
+ const isItalic = effectiveFormat?.textFormat?.italic;
222
+ const isBold = effectiveFormat?.textFormat?.bold;
223
+ const isUnderline = effectiveFormat?.textFormat?.underline;
224
+ const isStrikeThrough = effectiveFormat?.textFormat?.strikethrough;
225
+ const baselineOffset = effectiveFormat?.textFormat?.baselineOffset;
226
+ // Cell-level formatting wins over the banded (table style) layer,
227
+ // matching the canvas grid's merge order.
228
+ const cellBorders = effectiveFormat?.borders ?? bandedFormat?.borders;
229
+ const borderTop = cellBorders?.top;
230
+ const borderBottom = cellBorders?.bottom;
231
+ const borderLeft = cellBorders?.left;
232
+ const borderRight = cellBorders?.right;
233
+ const textDecoration = `${isUnderline ? "underline" : ""} ${isStrikeThrough ? "line-through" : ""}`.trim();
234
+ const wordWrap = effectiveFormat?.wrapStrategy === "wrap";
235
+ const style = theme
236
+ ? {
237
+ "background-color": getStringifiedColor(effectiveFormat?.backgroundColor ??
238
+ bandedFormat?.backgroundColor, theme),
239
+ color: getStringifiedColor(effectiveFormat?.textFormat?.color ??
240
+ bandedFormat?.textFormat?.color, theme),
241
+ padding: "2px 3px 2px 3px",
242
+ overflow: "hidden",
243
+ "font-weight": isBold ? "700" : "normal",
244
+ "font-family": effectiveFormat?.textFormat?.fontFamily,
245
+ "font-style": isItalic ? "italic" : "normal",
246
+ "text-decoration": textDecoration
247
+ ? textDecoration
248
+ : undefined,
249
+ "font-size": `${effectiveFormat?.textFormat?.fontSize ?? 10}pt`,
250
+ "text-align": effectiveFormat?.horizontalAlignment,
251
+ "vertical-align": effectiveFormat?.verticalAlignment,
252
+ "border-top": borderTop
253
+ ? `${borderTop.width}px solid ${getStringifiedColor(borderTop.color, theme)}`
254
+ : undefined,
255
+ "border-left": borderLeft
256
+ ? `${borderLeft.width}px solid ${getStringifiedColor(borderLeft.color, theme)}`
257
+ : undefined,
258
+ "border-right": borderRight
259
+ ? `${borderRight.width}px solid ${getStringifiedColor(borderRight.color, theme)}`
260
+ : undefined,
261
+ "border-bottom": borderBottom
262
+ ? `${borderBottom.width}px solid ${getStringifiedColor(borderBottom.color, theme)}`
263
+ : undefined,
264
+ "text-wrap": wordWrap ? "wrap" : undefined,
265
+ }
266
+ : {};
242
267
  // html
243
- htmlString += "</tr>";
268
+ const innerHtml = baselineOffset === "superscript"
269
+ ? `<sup>${formattedValue}</sup>`
270
+ : baselineOffset === "subscript"
271
+ ? `<sub>${formattedValue}</sub>`
272
+ : formattedValue;
273
+ htmlString +=
274
+ `<td${formulaAttr}${numberFormatAttr}${alignmentAttr} ${rowspan > 1 ? `rowspan="${rowspan}"` : ""} ${colspan > 1 ? `colspan="${colspan}"` : ""} style='${Object.entries(style)
275
+ .filter(([_, v]) => !isNil(v))
276
+ .map(([k, v]) => `${k}:${v}`)
277
+ .join(";")}'>` +
278
+ innerHtml +
279
+ "</td>";
244
280
  // json
245
- json.push(jsonRow);
281
+ jsonRow.push({
282
+ data: normalizedCellData,
283
+ sourceCell: { rowIndex, columnIndex },
284
+ rowspan,
285
+ colspan,
286
+ });
246
287
  }
247
- htmlString += "<tbody></table></body></html>";
288
+ /* Join with ',' for csv */
289
+ csv.push(csvRow.join(","));
290
+ // Plain text
291
+ plainText.push(csvRow.join("\t"));
292
+ // html
293
+ htmlString += "</tr>";
294
+ // json
295
+ json.push(jsonRow);
248
296
  }
297
+ htmlString += "<tbody></table></body></html>";
249
298
  const csvStr = csv.join("\n");
250
299
  const plainTextStr = plainText.join("\n");
251
300
  if (copiedSharedStrings.size) {
@@ -259,7 +308,9 @@ export const useOnCopy = ({ theme, merges, tables: sheetTables, bandedRanges, ge
259
308
  e?.clipboardData?.setData("text/plain", plainTextStr);
260
309
  e?.clipboardData?.setData("text/csv", csvStr);
261
310
  e?.clipboardData?.setData("application/json", JSON.stringify(copiedSchema));
311
+ return true;
262
312
  }, [
313
+ showError,
263
314
  theme,
264
315
  sheetTables,
265
316
  bandedRanges,
@@ -1,14 +1,14 @@
1
1
  import { produceWithPatches } from "immer";
2
2
  import { useCallback } from "react";
3
3
  import { updateFormulaSheetReferences } from "../api/rename-sheet";
4
- import { truncateSheetTitle } from "../helpers/sheet-title";
4
+ import { isValidSheetTitle, truncateSheetTitle } from "../helpers/sheet-title";
5
5
  import { useCanEditSheet, useCanEditWorkbook } from "./use-can-edit-cellrange";
6
6
  export const useOnRenameSheet = ({ locale, onChangeSheetData, onChangeSheets, onChangeActiveCell, getActiveCell, getGraphNodes, getPrecedents, createHistory, onCommand, }) => {
7
7
  const canEditSheet = useCanEditSheet();
8
8
  const canEditWorkbook = useCanEditWorkbook();
9
9
  return useCallback((sheetId, name, oldName) => {
10
10
  // Worksheet names cannot be empty or whitespace-only
11
- if (name.trim().length === 0) {
11
+ if (!isValidSheetTitle(name)) {
12
12
  console.debug("Sheet name cannot be empty.");
13
13
  return;
14
14
  }
@@ -525,7 +525,7 @@ export declare const useSpreadsheetState: <T extends CellData = CellData, S exte
525
525
  onInsertTableRow: (tableView: TableView, relativeRowIndex?: number, rowData?: Record<string, string | undefined | null>[]) => void;
526
526
  onDeleteTableColumn: (tableView: TableView, dimensionIndex: number) => void;
527
527
  onDeleteTableRow: (tableView: TableView, rowIndexes: number[]) => void;
528
- onCopy: (e: ClipboardEvent | undefined, sheetId: number, copiedSelections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[]) => void;
528
+ onCopy: (e: ClipboardEvent | undefined, sheetId: number, copiedSelections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[], isCutOperation?: boolean) => boolean;
529
529
  onPaste: (e: ClipboardEvent | undefined, sourceSheetId: number | undefined, destinationSheetId: number, copiedSelections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[] | undefined, activeCell: import("@rowsncolumns/grid").CellInterface, selections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[] | undefined, finalSelections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[] | undefined, isCutOperation: boolean) => Promise<void>;
530
530
  onCreateBasicFilter: (sheetId: number, activeCell: import("@rowsncolumns/grid").CellInterface, selections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[], basicFilterSpec?: Partial<import("@rowsncolumns/spreadsheet").FilterView>) => void;
531
531
  onCreateTable: (sheetId: number, activeCell: import("@rowsncolumns/grid").CellInterface, selections: import("@rowsncolumns/grid").SelectionArea<SelectionAttributes>[], tableSpec?: Partial<TableView>, theme?: import("@rowsncolumns/spreadsheet").TableTheme, bandedRange?: BandedDefinition) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"delete-column.d.ts","sourceRoot":"","sources":["../../../interface/delete-column.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AASjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAG5E,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,qbAyB/D,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,MACrB,SAAS,MAAM,EAAE,eAAe,MAAM,EAAE,SA8TjD,CAAC"}
1
+ {"version":3,"file":"delete-column.d.ts","sourceRoot":"","sources":["../../../interface/delete-column.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AASjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAG5E,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,qbAyB/D,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,MACrB,SAAS,MAAM,EAAE,eAAe,MAAM,EAAE,SAuUjD,CAAC"}
@@ -71,33 +71,45 @@ export const deleteColumn = ({ rowCount, columnCount, locale, cellXfsRegistry, s
71
71
  // and table-column splice for partially-overlapped tables.
72
72
  onChangeTables?.((prevTables) => {
73
73
  const [nextState, patches, inversePatches] = produceWithPatches(prevTables, (draft) => {
74
- for (const table of draft) {
74
+ for (let idx = draft.length - 1; idx >= 0; idx--) {
75
+ const table = draft[idx];
75
76
  if (table.sheetId !== sheetId)
76
77
  continue;
77
- if (minColumnIndex < table.range.startColumnIndex) {
78
+ const { startColumnIndex, endColumnIndex } = table.range;
79
+ if (maxColumnIndex < startColumnIndex) {
78
80
  moveFilterView(table, count, Direction.Left);
81
+ continue;
79
82
  }
80
- if (minColumnIndex >= table.range.startColumnIndex &&
81
- minColumnIndex <= table.range.endColumnIndex) {
82
- if (table.filterSpecs) {
83
- for (const spec of table.filterSpecs) {
84
- if (spec.columnIndex >= minColumnIndex) {
85
- spec.columnIndex -= count;
86
- }
83
+ if (minColumnIndex > endColumnIndex)
84
+ continue;
85
+ if (minColumnIndex <= startColumnIndex &&
86
+ maxColumnIndex >= endColumnIndex) {
87
+ draft.splice(idx, 1);
88
+ continue;
89
+ }
90
+ if (table.filterSpecs) {
91
+ for (const spec of table.filterSpecs) {
92
+ if (spec.columnIndex >= minColumnIndex) {
93
+ spec.columnIndex -= count;
87
94
  }
88
95
  }
89
- if (table.sortSpecs) {
90
- for (const spec of table.sortSpecs) {
91
- if (spec.dimensionIndex >= minColumnIndex) {
92
- spec.dimensionIndex -= count;
93
- }
96
+ }
97
+ if (table.sortSpecs) {
98
+ for (const spec of table.sortSpecs) {
99
+ if (spec.dimensionIndex >= minColumnIndex) {
100
+ spec.dimensionIndex -= count;
94
101
  }
95
102
  }
96
- const columnArrayIndex = minColumnIndex - table.range.startColumnIndex;
97
- table.columns?.splice(columnArrayIndex, count);
98
- const delta = Math.min(table.range.endColumnIndex - minColumnIndex + 1, count);
99
- table.range.endColumnIndex -= delta;
100
103
  }
104
+ const overlapStart = Math.max(minColumnIndex, startColumnIndex);
105
+ const overlapEnd = Math.min(maxColumnIndex, endColumnIndex);
106
+ const removed = overlapEnd - overlapStart + 1;
107
+ table.columns?.splice(overlapStart - startColumnIndex, removed);
108
+ table.range.startColumnIndex = Math.min(startColumnIndex, minColumnIndex);
109
+ table.range.endColumnIndex =
110
+ table.range.startColumnIndex +
111
+ (endColumnIndex - startColumnIndex) -
112
+ removed;
101
113
  }
102
114
  });
103
115
  saveHistory({ tables: { patches, inversePatches } });
@@ -1 +1 @@
1
- {"version":3,"file":"delete-table-column.d.ts","sourceRoot":"","sources":["../../../interface/delete-table-column.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAY5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,6OAepE,IAAI,CACL,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EACnB,aAAa,GACb,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,sBAAsB,GACtB,cAAc,GACd,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,oBAAoB,GACpB,wBAAwB,GACxB,uBAAuB,GACvB,eAAe,CAClB,MACS,WAAW,SAAS,EAAE,gBAAgB,MAAM,SA4VrD,CAAC"}
1
+ {"version":3,"file":"delete-table-column.d.ts","sourceRoot":"","sources":["../../../interface/delete-table-column.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAY5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,6OAepE,IAAI,CACL,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EACnB,aAAa,GACb,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,sBAAsB,GACtB,cAAc,GACd,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,oBAAoB,GACpB,wBAAwB,GACxB,uBAAuB,GACvB,eAAe,CAClB,MACS,WAAW,SAAS,EAAE,gBAAgB,MAAM,SA8VrD,CAAC"}
@@ -121,9 +121,12 @@ export const deleteTableColumn = ({ columnCount, rowCount, locale, cellXfsRegist
121
121
  // Sheet data
122
122
  onChangeSheetData?.((prevSheetData) => {
123
123
  const [nextState, patches, inversePatches] = produceWithPatches(prevSheetData, (draft) => {
124
+ // The moved region is every cell after the deleted column within the table's rows,
125
+ // including any table to the right that sits inside that row band.
124
126
  const sourceRange = {
125
127
  ...range,
126
128
  startColumnIndex: referenceColumnIndex + count,
129
+ endColumnIndex: Number.MAX_SAFE_INTEGER,
127
130
  };
128
131
  const movedKeys = new Set();
129
132
  /**
@@ -233,8 +236,7 @@ export const deleteTableColumn = ({ columnCount, rowCount, locale, cellXfsRegist
233
236
  continue;
234
237
  }
235
238
  const startCol = Math.min(Math.max(range.startColumnIndex, 1), dataColumnCount);
236
- const endCol = Math.min(range.endColumnIndex, dataColumnCount);
237
- for (let columnIndex = startCol; columnIndex < endCol; columnIndex++) {
239
+ for (let columnIndex = startCol; columnIndex < dataColumnCount; columnIndex++) {
238
240
  const cellData = rowValues?.[columnIndex];
239
241
  const hasFormula = !!getExtendedValueFormula(getCellUserEnteredValue(cellData));
240
242
  if (!hasFormula) {
@@ -1 +1 @@
1
- {"version":3,"file":"delete-table-row.d.ts","sourceRoot":"","sources":["../../../interface/delete-table-row.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAgB5E,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAIjF,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,kSAiBjE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,MACvB,WAAW,SAAS,EAAE,YAAY,MAAM,EAAE,SAyXnD,CAAC"}
1
+ {"version":3,"file":"delete-table-row.d.ts","sourceRoot":"","sources":["../../../interface/delete-table-row.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAgB5E,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAIjF,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,kSAiBjE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,MACvB,WAAW,SAAS,EAAE,YAAY,MAAM,EAAE,SAoYnD,CAAC"}
@@ -15,6 +15,9 @@ export const deleteTableRow = ({ rowCount, columnCount, locale, cellXfsRegistry,
15
15
  const saveHistory = createHistory();
16
16
  const { 0: minRowIndex, [rowIndexes.length - 1]: maxRowIndex } = rowIndexes;
17
17
  const count = rowIndexes.length;
18
+ const { startColumnIndex, endColumnIndex } = tableView.range;
19
+ const isInsideColumnBand = (range) => range.startColumnIndex >= startColumnIndex &&
20
+ range.endColumnIndex <= endColumnIndex;
18
21
  enqueueStructureChange?.({
19
22
  kind: "delete-table-row",
20
23
  sheetId,
@@ -30,14 +33,15 @@ export const deleteTableRow = ({ rowCount, columnCount, locale, cellXfsRegistry,
30
33
  onChangeTables?.((prevTables) => {
31
34
  const [nextState, patches, inversePatches] = produceWithPatches(prevTables, (draft) => {
32
35
  for (const table of draft) {
33
- const startRowIndex = table.range.startRowIndex;
34
- const endRowIndex = table.range.endRowIndex;
35
- if (minRowIndex < startRowIndex) {
36
- moveFilterView(table, count, Direction.Up);
36
+ if (table.sheetId !== sheetId || !isInsideColumnBand(table.range)) {
37
+ continue;
37
38
  }
38
- if (minRowIndex >= startRowIndex && minRowIndex <= endRowIndex) {
39
+ if (table.id === tableView.id) {
39
40
  table.range.endRowIndex -= count;
40
41
  }
42
+ else if (maxRowIndex < table.range.startRowIndex) {
43
+ moveFilterView(table, count, Direction.Up);
44
+ }
41
45
  }
42
46
  });
43
47
  // Save history
@@ -52,7 +56,9 @@ export const deleteTableRow = ({ rowCount, columnCount, locale, cellXfsRegistry,
52
56
  for (let idx = draft.length - 1; idx >= 0; idx--) {
53
57
  const rule = draft[idx];
54
58
  const ranges = rule.ranges
55
- ?.map((range) => updateSheetRangeOnDelete(range, "row", minRowIndex, count, sheetId))
59
+ ?.map((range) => isInsideColumnBand(range)
60
+ ? updateSheetRangeOnDelete(range, "row", minRowIndex, count, sheetId)
61
+ : range)
56
62
  .filter((range) => !!range) ?? [];
57
63
  if (ranges.length) {
58
64
  rule.ranges = ranges;
@@ -73,7 +79,9 @@ export const deleteTableRow = ({ rowCount, columnCount, locale, cellXfsRegistry,
73
79
  for (let idx = draft.length - 1; idx >= 0; idx--) {
74
80
  const rule = draft[idx];
75
81
  const ranges = rule.ranges
76
- ?.map((range) => updateSheetRangeOnDelete(range, "row", minRowIndex, count, sheetId))
82
+ ?.map((range) => isInsideColumnBand(range)
83
+ ? updateSheetRangeOnDelete(range, "row", minRowIndex, count, sheetId)
84
+ : range)
77
85
  .filter((range) => !!range) ?? [];
78
86
  if (ranges.length) {
79
87
  rule.ranges = ranges;
@@ -96,7 +104,8 @@ export const deleteTableRow = ({ rowCount, columnCount, locale, cellXfsRegistry,
96
104
  if (sheet.rowMetadata) {
97
105
  sheet.rowMetadata.splice(minRowIndex, count);
98
106
  }
99
- if (sheet.basicFilter) {
107
+ if (sheet.basicFilter &&
108
+ isInsideColumnBand(sheet.basicFilter.range)) {
100
109
  if (minRowIndex < sheet.basicFilter.range.startRowIndex) {
101
110
  moveFilterView(sheet.basicFilter, count, Direction.Up);
102
111
  }
@@ -1 +1 @@
1
- {"version":3,"file":"insert-column.d.ts","sourceRoot":"","sources":["../../../interface/insert-column.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoB5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,qaAyB/D,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,MACrB,SAAS,MAAM,EAAE,sBAAsB,MAAM,EAAE,mBAAc,SA0TtE,CAAC;AAMF,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,qQAiBpE,IAAI,CACL,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EACnB,QAAQ,GACR,aAAa,GACb,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,sBAAsB,GACtB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,oBAAoB,GACpB,wBAAwB,GACxB,uBAAuB,GACvB,eAAe,GACf,cAAc,GACd,gBAAgB,CACnB,MAEG,WAAW,SAAS,EACpB,gBAAgB,MAAM,EACtB,WAAW,SAAS,CAAC,IAAI,GAAG,SAAS,CAAC,KAAK,SA4V9C,CAAC"}
1
+ {"version":3,"file":"insert-column.d.ts","sourceRoot":"","sources":["../../../interface/insert-column.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoB5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,qaAyB/D,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,MACrB,SAAS,MAAM,EAAE,sBAAsB,MAAM,EAAE,mBAAc,SA0TtE,CAAC;AAMF,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,KAAK,EAAE,qQAiBpE,IAAI,CACL,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EACnB,QAAQ,GACR,aAAa,GACb,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,sBAAsB,GACtB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,oBAAoB,GACpB,wBAAwB,GACxB,uBAAuB,GACvB,eAAe,GACf,cAAc,GACd,gBAAgB,CACnB,MAEG,WAAW,SAAS,EACpB,gBAAgB,MAAM,EACtB,WAAW,SAAS,CAAC,IAAI,GAAG,SAAS,CAAC,KAAK,SA2V9C,CAAC"}
@@ -259,7 +259,7 @@ export const insertTableColumn = ({ tables, columnCount, rowCount, locale, cellX
259
259
  onChangeTables?.((prevTables) => {
260
260
  const [nextState, patches, inversePatches] = produceWithPatches(prevTables, (draft) => {
261
261
  for (const table of draft) {
262
- if (referenceColumnIndex < table.range.startColumnIndex &&
262
+ if (referenceColumnIndex <= table.range.startColumnIndex &&
263
263
  table.range.startRowIndex >= tableView.range.startRowIndex &&
264
264
  table.range.endRowIndex <= tableView.range.endRowIndex &&
265
265
  table.id !== tableView.id) {
@@ -342,9 +342,12 @@ export const insertTableColumn = ({ tables, columnCount, rowCount, locale, cellX
342
342
  const [nextState, patches, inversePatches] = produceWithPatches(prevSheetData, (draft) => {
343
343
  const { range } = tableView;
344
344
  const headerRow = tableView.headerRow ?? true;
345
+ // The moved region is every cell at/after the insertion point within the table's
346
+ // rows, including any table to the right that sits inside that row band.
345
347
  const sourceRange = {
346
348
  ...range,
347
349
  startColumnIndex: referenceColumnIndex,
350
+ endColumnIndex: Number.MAX_SAFE_INTEGER,
348
351
  };
349
352
  const movedKeys = new Set();
350
353
  // PASS 1
@@ -427,8 +430,7 @@ export const insertTableColumn = ({ tables, columnCount, rowCount, locale, cellX
427
430
  for (let rowIndex = range.startRowIndex; rowIndex <= range.endRowIndex; rowIndex++) {
428
431
  const dataColumnCount = draft[sheetId]?.[rowIndex]?.values?.length ?? 0;
429
432
  const startCol = Math.min(Math.max(referenceColumnIndex, 1), dataColumnCount);
430
- const endCol = Math.min(range.endColumnIndex + numColumns, dataColumnCount);
431
- for (let columnIndex = startCol; columnIndex < endCol; columnIndex++) {
433
+ for (let columnIndex = startCol; columnIndex < dataColumnCount; columnIndex++) {
432
434
  moveFormulaAtPosition({
433
435
  draft,
434
436
  position: { sheetId, rowIndex, columnIndex },