@rowsncolumns/spreadsheet-state 29.2.0 → 29.3.2

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 (127) hide show
  1. package/dist/cjs/api/border.d.ts +1 -1
  2. package/dist/cjs/api/border.d.ts.map +1 -1
  3. package/dist/cjs/commands.d.ts +36 -3
  4. package/dist/cjs/commands.d.ts.map +1 -1
  5. package/dist/cjs/components/editors/cell-format/alignment-tab.d.ts +27 -0
  6. package/dist/cjs/components/editors/cell-format/alignment-tab.d.ts.map +1 -0
  7. package/dist/cjs/components/editors/cell-format/alignment-tab.js +83 -0
  8. package/dist/cjs/components/editors/cell-format/border-edges.d.ts +42 -0
  9. package/dist/cjs/components/editors/cell-format/border-edges.d.ts.map +1 -0
  10. package/dist/cjs/components/editors/cell-format/border-edges.js +118 -0
  11. package/dist/cjs/components/editors/cell-format/border-tab.d.ts +21 -0
  12. package/dist/cjs/components/editors/cell-format/border-tab.d.ts.map +1 -0
  13. package/dist/cjs/components/editors/cell-format/border-tab.js +123 -0
  14. package/dist/cjs/components/editors/cell-format/fill-tab.d.ts +14 -0
  15. package/dist/cjs/components/editors/cell-format/fill-tab.d.ts.map +1 -0
  16. package/dist/cjs/components/editors/cell-format/fill-tab.js +54 -0
  17. package/dist/cjs/components/editors/cell-format/font-tab.d.ts +18 -0
  18. package/dist/cjs/components/editors/cell-format/font-tab.d.ts.map +1 -0
  19. package/dist/cjs/components/editors/cell-format/font-tab.js +99 -0
  20. package/dist/cjs/components/editors/cell-format/list-box.d.ts +35 -0
  21. package/dist/cjs/components/editors/cell-format/list-box.d.ts.map +1 -0
  22. package/dist/cjs/components/editors/cell-format/list-box.js +37 -0
  23. package/dist/cjs/components/editors/cell-format/number-formats.d.ts +90 -0
  24. package/dist/cjs/components/editors/cell-format/number-formats.d.ts.map +1 -0
  25. package/dist/cjs/components/editors/cell-format/number-formats.js +403 -0
  26. package/dist/cjs/components/editors/cell-format/number-tab.d.ts +12 -0
  27. package/dist/cjs/components/editors/cell-format/number-tab.d.ts.map +1 -0
  28. package/dist/cjs/components/editors/cell-format/number-tab.js +194 -0
  29. package/dist/cjs/components/editors/cell-format/protection-tab.d.ts +13 -0
  30. package/dist/cjs/components/editors/cell-format/protection-tab.d.ts.map +1 -0
  31. package/dist/cjs/components/editors/cell-format/protection-tab.js +13 -0
  32. package/dist/cjs/components/editors/cell-format-editor.d.ts +31 -3
  33. package/dist/cjs/components/editors/cell-format-editor.d.ts.map +1 -1
  34. package/dist/cjs/components/editors/cell-format-editor.js +182 -636
  35. package/dist/cjs/components/editors/conditional-format-data-bar-options.d.ts +16 -0
  36. package/dist/cjs/components/editors/conditional-format-data-bar-options.d.ts.map +1 -0
  37. package/dist/cjs/components/editors/conditional-format-data-bar-options.js +166 -0
  38. package/dist/cjs/components/editors/conditional-format-editor.d.ts +7 -1
  39. package/dist/cjs/components/editors/conditional-format-editor.d.ts.map +1 -1
  40. package/dist/cjs/components/editors/conditional-format-editor.js +51 -9
  41. package/dist/cjs/components/editors/conditional-format-icon-set-options.d.ts +17 -0
  42. package/dist/cjs/components/editors/conditional-format-icon-set-options.d.ts.map +1 -0
  43. package/dist/cjs/components/editors/conditional-format-icon-set-options.js +163 -0
  44. package/dist/cjs/components/editors/conditional-format-rule-editor.d.ts +9 -0
  45. package/dist/cjs/components/editors/conditional-format-rule-editor.d.ts.map +1 -1
  46. package/dist/cjs/components/editors/conditional-format-rule-editor.js +97 -125
  47. package/dist/cjs/components/editors/sheet-filter.d.ts +20 -9
  48. package/dist/cjs/components/editors/sheet-filter.d.ts.map +1 -1
  49. package/dist/cjs/components/editors/sheet-filter.js +44 -12
  50. package/dist/cjs/hooks/use-conditional-formatting-rules.d.ts +2 -0
  51. package/dist/cjs/hooks/use-conditional-formatting-rules.d.ts.map +1 -1
  52. package/dist/cjs/hooks/use-conditional-formatting-rules.js +22 -1
  53. package/dist/cjs/hooks/use-spreadsheet-state.d.ts +2 -0
  54. package/dist/cjs/hooks/use-spreadsheet-state.d.ts.map +1 -1
  55. package/dist/cjs/hooks/use-spreadsheet-state.js +4 -2
  56. package/dist/cjs/interface/change-border.d.ts.map +1 -1
  57. package/dist/cjs/interface/change-border.js +8 -1
  58. package/dist/cjs/interface/conditional-formatting-rules.d.ts +11 -1
  59. package/dist/cjs/interface/conditional-formatting-rules.d.ts.map +1 -1
  60. package/dist/cjs/interface/conditional-formatting-rules.js +44 -1
  61. package/dist/cjs/sheet-cell.d.ts +8 -0
  62. package/dist/cjs/sheet-cell.d.ts.map +1 -1
  63. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  64. package/dist/esm/api/border.d.ts +1 -1
  65. package/dist/esm/api/border.d.ts.map +1 -1
  66. package/dist/esm/commands.d.ts +36 -3
  67. package/dist/esm/commands.d.ts.map +1 -1
  68. package/dist/esm/components/editors/cell-format/alignment-tab.d.ts +27 -0
  69. package/dist/esm/components/editors/cell-format/alignment-tab.d.ts.map +1 -0
  70. package/dist/esm/components/editors/cell-format/alignment-tab.js +81 -0
  71. package/dist/esm/components/editors/cell-format/border-edges.d.ts +42 -0
  72. package/dist/esm/components/editors/cell-format/border-edges.d.ts.map +1 -0
  73. package/dist/esm/components/editors/cell-format/border-edges.js +79 -0
  74. package/dist/esm/components/editors/cell-format/border-tab.d.ts +21 -0
  75. package/dist/esm/components/editors/cell-format/border-tab.d.ts.map +1 -0
  76. package/dist/esm/components/editors/cell-format/border-tab.js +103 -0
  77. package/dist/esm/components/editors/cell-format/fill-tab.d.ts +14 -0
  78. package/dist/esm/components/editors/cell-format/fill-tab.d.ts.map +1 -0
  79. package/dist/esm/components/editors/cell-format/fill-tab.js +49 -0
  80. package/dist/esm/components/editors/cell-format/font-tab.d.ts +18 -0
  81. package/dist/esm/components/editors/cell-format/font-tab.d.ts.map +1 -0
  82. package/dist/esm/components/editors/cell-format/font-tab.js +88 -0
  83. package/dist/esm/components/editors/cell-format/list-box.d.ts +35 -0
  84. package/dist/esm/components/editors/cell-format/list-box.d.ts.map +1 -0
  85. package/dist/esm/components/editors/cell-format/list-box.js +30 -0
  86. package/dist/esm/components/editors/cell-format/number-formats.d.ts +90 -0
  87. package/dist/esm/components/editors/cell-format/number-formats.d.ts.map +1 -0
  88. package/dist/esm/components/editors/cell-format/number-formats.js +383 -0
  89. package/dist/esm/components/editors/cell-format/number-tab.d.ts +12 -0
  90. package/dist/esm/components/editors/cell-format/number-tab.d.ts.map +1 -0
  91. package/dist/esm/components/editors/cell-format/number-tab.js +165 -0
  92. package/dist/esm/components/editors/cell-format/protection-tab.d.ts +13 -0
  93. package/dist/esm/components/editors/cell-format/protection-tab.d.ts.map +1 -0
  94. package/dist/esm/components/editors/cell-format/protection-tab.js +9 -0
  95. package/dist/esm/components/editors/cell-format-editor.d.ts +31 -3
  96. package/dist/esm/components/editors/cell-format-editor.d.ts.map +1 -1
  97. package/dist/esm/components/editors/cell-format-editor.js +148 -615
  98. package/dist/esm/components/editors/conditional-format-data-bar-options.d.ts +16 -0
  99. package/dist/esm/components/editors/conditional-format-data-bar-options.d.ts.map +1 -0
  100. package/dist/esm/components/editors/conditional-format-data-bar-options.js +145 -0
  101. package/dist/esm/components/editors/conditional-format-editor.d.ts +7 -1
  102. package/dist/esm/components/editors/conditional-format-editor.d.ts.map +1 -1
  103. package/dist/esm/components/editors/conditional-format-editor.js +39 -8
  104. package/dist/esm/components/editors/conditional-format-icon-set-options.d.ts +17 -0
  105. package/dist/esm/components/editors/conditional-format-icon-set-options.d.ts.map +1 -0
  106. package/dist/esm/components/editors/conditional-format-icon-set-options.js +139 -0
  107. package/dist/esm/components/editors/conditional-format-rule-editor.d.ts +9 -0
  108. package/dist/esm/components/editors/conditional-format-rule-editor.d.ts.map +1 -1
  109. package/dist/esm/components/editors/conditional-format-rule-editor.js +60 -85
  110. package/dist/esm/components/editors/sheet-filter.d.ts +20 -9
  111. package/dist/esm/components/editors/sheet-filter.d.ts.map +1 -1
  112. package/dist/esm/components/editors/sheet-filter.js +31 -11
  113. package/dist/esm/hooks/use-conditional-formatting-rules.d.ts +2 -0
  114. package/dist/esm/hooks/use-conditional-formatting-rules.d.ts.map +1 -1
  115. package/dist/esm/hooks/use-conditional-formatting-rules.js +19 -1
  116. package/dist/esm/hooks/use-spreadsheet-state.d.ts +2 -0
  117. package/dist/esm/hooks/use-spreadsheet-state.d.ts.map +1 -1
  118. package/dist/esm/hooks/use-spreadsheet-state.js +4 -2
  119. package/dist/esm/interface/change-border.d.ts.map +1 -1
  120. package/dist/esm/interface/change-border.js +8 -1
  121. package/dist/esm/interface/conditional-formatting-rules.d.ts +11 -1
  122. package/dist/esm/interface/conditional-formatting-rules.d.ts.map +1 -1
  123. package/dist/esm/interface/conditional-formatting-rules.js +29 -1
  124. package/dist/esm/sheet-cell.d.ts +8 -0
  125. package/dist/esm/sheet-cell.d.ts.map +1 -1
  126. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  127. package/package.json +6 -6
@@ -1,635 +1,168 @@
1
- import { Direction } from "@rowsncolumns/grid";
2
- import { AiOutlineMergeCells, Cross2Icon, TextOverflowWrapIcon, } from "@rowsncolumns/icons";
3
- import { BorderColorSelector, BorderLineStyleOptions, BorderLocationOptions, ButtonBold, ButtonItalic, ButtonStrikethrough, ButtonUnderline, FontFamilySelector, FontSizeSelector, TextColorSelector, useFocusSheet, } from "@rowsncolumns/spreadsheet";
4
- import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogTitle, Input, Select, SelectContent, SelectItem, SelectPortal, SelectTrigger, SelectValue, Stack, } from "@rowsncolumns/ui";
5
- import { Tabs } from "@rowsncolumns/ui";
6
- import { TabsList } from "@rowsncolumns/ui";
7
- import { TabsTrigger } from "@rowsncolumns/ui";
8
- import { TabsContent } from "@rowsncolumns/ui";
9
- import { getDefaultDateFormat, getDefaultTimeFormat, ssfFormat, ssfFormatColor, supplant, } from "@rowsncolumns/utils";
10
- import { useFormik } from "formik";
11
- import React, { useCallback, useMemo, useState } from "react";
12
- import { addThousandSeparator, changeDecimals, getCurrentDecimalCountInPattern, hasThousandsSeparator, removeThousandSeparator, } from "../../api/decimals";
1
+ import { Cross2Icon } from "@rowsncolumns/icons";
2
+ import { useFocusSheet } from "@rowsncolumns/spreadsheet";
3
+ import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogTitle, Tabs, TabsContent, TabsList, TabsTrigger, } from "@rowsncolumns/ui";
4
+ import React, { useMemo, useState } from "react";
13
5
  import { useFormatCellsDialogState, useSetFormatCellsDialogState, } from "../../atoms/dialog-state.atom";
14
- const getNumberFormatCategories = (locale) => [
15
- {
16
- name: "General",
17
- value: "GENERAL",
18
- description: "General format cells have no specific number format.",
19
- },
20
- {
21
- name: "Number",
22
- value: "NUMBER",
23
- description: "Number is used for general display of numbers.",
24
- pattern: "#.00",
25
- options: ["Decimal", "ThousandSeparator"],
26
- },
27
- {
28
- name: "Currency",
29
- value: "CURRENCY",
30
- description: "Currency formats are used for general monetary values.",
31
- pattern: `"{currencySymbol}"* #,##0.00;"{currencySymbol}"* (#,##0.00);"{currencySymbol}"* "-"??;@`,
32
- options: ["CurrencySymbol", "Decimal"],
33
- },
34
- {
35
- name: "Accounting",
36
- value: "ACCOUNTING",
37
- pattern: `"{currencySymbol}"* #,##0.00;"{currencySymbol}"* (#,##0.00);"{currencySymbol}"* "-"??;@`,
38
- options: ["CurrencySymbol", "Decimal"],
39
- },
40
- {
41
- name: "Date",
42
- value: "DATE",
43
- description: "Date formats display date and time serial numbers as date values.",
44
- pattern: getDefaultDateFormat(locale),
45
- },
46
- {
47
- name: "Time",
48
- value: "DATE_TIME",
49
- description: "Time formats display date and time serial numbers as date values.",
50
- pattern: getDefaultTimeFormat(locale),
51
- },
52
- {
53
- name: "Percentage",
54
- value: "PERCENT",
55
- pattern: "0%",
56
- options: ["Decimal"],
57
- },
58
- {
59
- name: "Fraction",
60
- value: "FRACTION",
61
- pattern: "#/#",
62
- },
63
- {
64
- name: "Scientific",
65
- value: "SCIENTIFIC",
66
- pattern: "0.00E+00",
67
- options: ["Decimal"],
68
- },
69
- {
70
- name: "Text",
71
- value: "TEXT",
72
- },
73
- {
74
- name: "Custom",
75
- value: "CUSTOM",
76
- pattern: "General",
77
- },
78
- ];
79
- const horizontalAlignments = [
80
- {
81
- name: "General",
82
- value: undefined,
83
- },
84
- {
85
- name: "Left",
86
- value: "left",
87
- },
88
- {
89
- name: "Center",
90
- value: "center",
91
- },
92
- {
93
- name: "Right",
94
- value: "right",
95
- },
96
- ];
97
- const verticalAlignments = [
98
- {
99
- name: "Top",
100
- value: "top",
101
- },
102
- {
103
- name: "Center",
104
- value: "middle",
105
- },
106
- {
107
- name: "Bottom",
108
- value: "bottom",
109
- },
110
- ];
111
- const customFormats = [
112
- {
113
- type: "GENERAL",
114
- },
115
- {
116
- type: "NUMBER",
117
- pattern: "0",
118
- },
119
- {
120
- type: "NUMBER",
121
- pattern: "##.00",
122
- },
123
- {
124
- type: "NUMBER",
125
- pattern: "#,##0",
126
- },
127
- {
128
- type: "NUMBER",
129
- pattern: "#,##0.00",
130
- },
131
- {
132
- type: "NUMBER",
133
- pattern: "#,##0;(#,##0)",
134
- },
135
- {
136
- type: "NUMBER",
137
- pattern: "#,##0;[Red](#,##0)",
138
- },
139
- {
140
- type: "NUMBER",
141
- pattern: "#,##0.00;(#,##0.00)",
142
- },
143
- {
144
- type: "NUMBER",
145
- pattern: "#,##0.00;[Red](#,##0.00)",
146
- },
147
- {
148
- type: "CURRENCY",
149
- pattern: '"{currencySymbol}"#,##0;("{currencySymbol}"#,##0)',
150
- },
151
- {
152
- type: "CURRENCY",
153
- pattern: '"{currencySymbol}"#,##0;[Red]("{currencySymbol}"#,##0)',
154
- },
155
- {
156
- type: "CURRENCY",
157
- pattern: '"{currencySymbol}"#,##0.00;("{currencySymbol}"#,##0.00)',
158
- },
159
- {
160
- type: "CURRENCY",
161
- pattern: '"{currencySymbol}"#,##0.00;[Red]("{currencySymbol}"#,##0.00)',
162
- },
163
- {
164
- type: "PERCENT",
165
- pattern: "0%",
166
- },
167
- {
168
- type: "PERCENT",
169
- pattern: "0.00%",
170
- },
171
- {
172
- type: "SCIENTIFIC",
173
- pattern: "0.00E+00",
174
- },
175
- {
176
- type: "SCIENTIFIC",
177
- pattern: "##0.0E+0",
178
- },
179
- {
180
- type: "FRACTION",
181
- pattern: "# ?/?",
182
- },
183
- {
184
- type: "FRACTION",
185
- pattern: "# ??/??",
186
- },
187
- {
188
- type: "DATE",
189
- pattern: "d/m/yy",
190
- },
191
- {
192
- type: "DATE",
193
- pattern: "d-mmm-yy",
194
- },
195
- {
196
- type: "DATE",
197
- pattern: "d-mmm",
198
- },
199
- {
200
- type: "DATE",
201
- pattern: "mmm-yy",
202
- },
203
- {
204
- type: "DATE_TIME",
205
- pattern: "h:mm AM/PM",
206
- },
207
- {
208
- type: "DATE_TIME",
209
- pattern: "h:mm:ss AM/PM",
210
- },
211
- {
212
- type: "DATE_TIME",
213
- pattern: "h:mm",
214
- },
215
- {
216
- type: "DATE_TIME",
217
- pattern: "h:mm:ss",
218
- },
219
- {
220
- type: "DATE",
221
- pattern: "d/m/yy h:mm",
222
- },
223
- {
224
- type: "DATE_TIME",
225
- pattern: "mm:ss",
226
- },
227
- {
228
- type: "DATE_TIME",
229
- pattern: "mm:ss.0",
230
- },
231
- {
232
- type: "TEXT",
233
- pattern: "@",
234
- },
235
- {
236
- type: "DATE_TIME",
237
- pattern: "[h]:mm:ss",
238
- },
239
- {
240
- type: "CURRENCY",
241
- pattern: '"{currencySymbol}"* #,##0;"{currencySymbol}"* (#,##0);"{currencySymbol}"* "-";@',
242
- },
243
- {
244
- type: "ACCOUNTING",
245
- pattern: '* #,##0;* (#,##0);* "-";@',
246
- },
247
- {
248
- type: "CURRENCY",
249
- pattern: `"{currencySymbol}"* #,##0.00;"{currencySymbol}"* (#,##0.00);"{currencySymbol}"* "-"??;@`,
250
- },
251
- {
252
- type: "ACCOUNTING",
253
- pattern: '* #,##0.00;* (#,##0.00);* "-"??;@',
254
- },
255
- {
256
- type: "DATE_TIME",
257
- pattern: "[$-en-US]h:mm:ss AM/PM",
258
- },
259
- {
260
- type: "DATE_TIME",
261
- pattern: "[$-en-US]hh:mm:ss AM/PM;@",
262
- },
263
- {
264
- type: "DATE",
265
- pattern: "[$-en-US]m/d/yy h:mm AM/PM;@",
266
- },
267
- {
268
- type: "DATE_TIME",
269
- pattern: "[$-en-US]h:mm AM/PM;@",
270
- },
271
- {
272
- type: "DATE",
273
- pattern: "d/m/yyyy",
274
- },
275
- {
276
- type: "NUMBER",
277
- pattern: "00000",
278
- },
279
- ];
280
- const dateFormats = [
281
- {
282
- pattern: "dd/m/yy",
283
- type: "DATE",
284
- },
285
- {
286
- pattern: "dddd, dd mmmm yyyy",
287
- type: "DATE",
288
- },
289
- {
290
- pattern: "dd/mm/yyyy",
291
- type: "DATE",
292
- },
293
- {
294
- pattern: "dd/mm/yy",
295
- type: "DATE",
296
- },
297
- {
298
- pattern: "dd\\.mm\\.yyyy",
299
- type: "DATE",
300
- },
301
- {
302
- pattern: "yyyy-mm-dd",
303
- type: "DATE",
304
- },
305
- {
306
- pattern: "dd mmmm yyyy",
307
- type: "DATE",
308
- },
309
- {
310
- pattern: "dd-mmm",
311
- type: "DATE",
312
- },
313
- {
314
- pattern: "dd-mmm-yy",
315
- type: "DATE",
316
- },
317
- {
318
- pattern: "mmm-dd",
319
- type: "DATE",
320
- },
321
- {
322
- pattern: "mmmm-dd",
323
- type: "DATE",
324
- },
325
- {
326
- pattern: "mmm-dd, yyyy",
327
- type: "DATE",
328
- },
329
- ];
330
- const timeFormats = [
331
- {
332
- pattern: "hh:mm",
333
- type: "DATE_TIME",
334
- },
335
- {
336
- pattern: "h:mm AM/PM",
337
- type: "DATE_TIME",
338
- },
339
- {
340
- pattern: "hh:mm:ss",
341
- type: "DATE_TIME",
342
- },
343
- {
344
- pattern: "h:mm:ss AM/PM",
345
- type: "DATE_TIME",
346
- },
347
- {
348
- pattern: "d/m/yy h:mm AM/PM",
349
- type: "DATE",
350
- },
351
- {
352
- pattern: "d/m/yy hh:mm",
353
- type: "DATE",
354
- },
355
- ];
356
- export const CellFormatEditor = ({ locale, cellFormat, sheetId, activeCell, selections, currencySymbol = "$", theme, isDarkMode, onMergeCells, onChangeFormatting, onChangeBorder, onCancel, getEffectiveValue, }) => {
357
- const [isCustom, setIsCustom] = useState(false);
358
- const [color, setColorState] = useState(undefined);
359
- const [borderStyle, setStyleState] = useState("solid");
360
- const [borderLocation, setBorderLocationState] = useState();
6
+ import { AlignmentTab } from "./cell-format/alignment-tab";
7
+ import { edgesFromBorders, toBorderCommands, } from "./cell-format/border-edges";
8
+ import { BorderTab } from "./cell-format/border-tab";
9
+ import { FillTab } from "./cell-format/fill-tab";
10
+ import { FontTab } from "./cell-format/font-tab";
11
+ import { NumberTab } from "./cell-format/number-tab";
12
+ import { ProtectionTab, } from "./cell-format/protection-tab";
13
+ const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
14
+ /**
15
+ * The properties the dialog changed, and only those — Excel's Format Cells
16
+ * applies what you edited to every selected cell and leaves the rest of each
17
+ * cell's format alone. A removed value is sent as `undefined` (the engines'
18
+ * clear for that property).
19
+ */
20
+ export const diffCellFormat = (initial, draft) => {
21
+ const patch = {};
22
+ const keys = new Set([...Object.keys(initial), ...Object.keys(draft)]);
23
+ for (const key of keys) {
24
+ if (key === "textFormat" || key === "borders")
25
+ continue;
26
+ const before = initial[key];
27
+ const after = draft[key];
28
+ if (!same(before, after))
29
+ patch[key] = after;
30
+ }
31
+ const tfBefore = (initial.textFormat ?? {});
32
+ const tfAfter = (draft.textFormat ?? {});
33
+ const textFormat = {};
34
+ for (const key of new Set([
35
+ ...Object.keys(tfBefore),
36
+ ...Object.keys(tfAfter),
37
+ ])) {
38
+ if (!same(tfBefore[key], tfAfter[key]))
39
+ textFormat[key] = tfAfter[key];
40
+ }
41
+ if (Object.keys(textFormat).length)
42
+ patch.textFormat = textFormat;
43
+ return patch;
44
+ };
45
+ const selectionSpans = (activeCell, selections) => {
46
+ const ranges = selections.length
47
+ ? selections.map((s) => s.range)
48
+ : [
49
+ {
50
+ startRowIndex: activeCell.rowIndex,
51
+ endRowIndex: activeCell.rowIndex,
52
+ startColumnIndex: activeCell.columnIndex,
53
+ endColumnIndex: activeCell.columnIndex,
54
+ },
55
+ ];
56
+ return {
57
+ rows: ranges.some((r) => r.endRowIndex > r.startRowIndex),
58
+ columns: ranges.some((r) => r.endColumnIndex > r.startColumnIndex),
59
+ };
60
+ };
61
+ /**
62
+ * Excel's Format Cells dialog (Ctrl+1 / ⌘1): Number, Alignment, Font,
63
+ * Border, Fill and Protection. Every tab edits a draft; OK applies the
64
+ * edits to the selection, Cancel drops them.
65
+ */
66
+ export const CellFormatEditor = ({ locale, cellFormat, sheetId, activeCell, selections, currencySymbol = "$", theme, isDarkMode, onMergeCells, onUnMergeCells, isMerged = false, cellProtection, onChangeCellProtection, onChangeFormatting, onChangeBorder, onCancel, getEffectiveValue, defaultTab = "number", }) => {
361
67
  const setFormatCellDialogState = useSetFormatCellsDialogState();
362
- // close dialog
363
68
  const onRequestCloseDialog = () => setFormatCellDialogState(false);
69
+ const initial = useMemo(() => cellFormat ?? {}, [cellFormat]);
70
+ const [draft, setDraft] = useState(initial);
71
+ const patchDraft = (patch) => setDraft((prev) => ({ ...prev, ...patch }));
72
+ const patchText = (patch) => setDraft((prev) => ({
73
+ ...prev,
74
+ textFormat: { ...(prev.textFormat ?? {}), ...patch },
75
+ }));
76
+ const [merged, setMerged] = useState(isMerged);
77
+ const initialProtection = {
78
+ locked: cellProtection?.locked !== false,
79
+ hidden: cellProtection?.hidden === true,
80
+ };
81
+ const [protection, setProtection] = useState(initialProtection);
82
+ // Border: the edges the user edited, on top of the active cell's own.
83
+ const initialEdges = useMemo(() => edgesFromBorders(cellFormat?.borders), [cellFormat?.borders]);
84
+ const [edgeEdits, setEdgeEdits] = useState({});
85
+ const [clearedAll, setClearedAll] = useState(false);
86
+ const [lineStyle, setLineStyle] = useState("solid");
87
+ const [borderColor, setBorderColor] = useState(() => ({
88
+ theme: 1,
89
+ tint: isDarkMode ? 1 : 0,
90
+ }));
91
+ const shownEdges = clearedAll
92
+ ? { ...edgeEdits }
93
+ : { ...initialEdges, ...edgeEdits };
94
+ const spans = selectionSpans(activeCell, selections);
364
95
  const effectiveValue = getEffectiveValue(sheetId, activeCell.rowIndex, activeCell.columnIndex);
365
- const [customFormatValue, setCustomFormatValue] = useState();
366
- const numberCategories = useMemo(() => getNumberFormatCategories(locale), [locale]);
367
- const getDefaultBorderColor = useCallback(() => {
368
- return { theme: 1, tint: isDarkMode ? 1 : 0 };
369
- }, [isDarkMode]);
370
- const overflowDivClass = "grid grid-cols-1 max-h-[200px] overflow-auto border-rnc-border border-solid rounded-sm border";
371
- const sampleDate = new Date();
372
- const { handleSubmit, resetForm, setFieldValue, values } = useFormik({
373
- initialValues: cellFormat ?? {},
374
- onSubmit(values) {
375
- // Formatting
376
- onChangeFormatting?.(sheetId, activeCell, selections, values);
377
- // Border TBD
378
- // Merge cells TBD
379
- onRequestCloseDialog();
380
- },
381
- });
382
- const type = values?.numberFormat?.type;
383
- const pattern = values?.numberFormat?.pattern;
384
- const formattedValue = ssfFormat(pattern ?? "General", effectiveValue) || "\u00A0";
385
- const formattedColor = ssfFormatColor(pattern ?? "General", effectiveValue);
386
- const selectedNumberFormat = type ?? "GENERAL";
387
- const selectedOption = numberCategories.find((opt) => opt.value === selectedNumberFormat);
388
- const decimalCount = getCurrentDecimalCountInPattern(pattern ?? undefined);
389
- const thousandsSeparator = hasThousandsSeparator(pattern ?? undefined);
390
- // Cancel
96
+ const fill = {
97
+ backgroundColor: draft.backgroundColor,
98
+ fillPattern: draft.fillPattern,
99
+ foregroundColor: draft.foregroundColor,
100
+ gradient: draft.gradient,
101
+ };
102
+ const onSave = () => {
103
+ const patch = diffCellFormat(initial, draft);
104
+ if (Object.keys(patch).length) {
105
+ onChangeFormatting?.(sheetId, activeCell, selections, patch);
106
+ }
107
+ for (const command of toBorderCommands(edgeEdits, { clearedAll })) {
108
+ onChangeBorder?.(sheetId, activeCell, selections, command.location, command.color, command.style);
109
+ }
110
+ if (merged !== isMerged) {
111
+ if (merged)
112
+ onMergeCells?.(sheetId, activeCell, selections);
113
+ else
114
+ onUnMergeCells?.(sheetId, activeCell, selections);
115
+ }
116
+ if (protection.locked !== initialProtection.locked ||
117
+ protection.hidden !== initialProtection.hidden) {
118
+ onChangeCellProtection?.(sheetId, activeCell, selections, {
119
+ ...(protection.locked !== initialProtection.locked
120
+ ? { locked: protection.locked }
121
+ : {}),
122
+ ...(protection.hidden !== initialProtection.hidden
123
+ ? { hidden: protection.hidden }
124
+ : {}),
125
+ });
126
+ }
127
+ onRequestCloseDialog();
128
+ };
391
129
  const onCancelForm = () => {
392
- resetForm();
393
130
  onRequestCloseDialog();
394
131
  onCancel?.();
395
132
  };
396
- return (React.createElement("form", { onSubmit: handleSubmit },
397
- React.createElement(Tabs, { defaultValue: "number" },
133
+ return (React.createElement("form", { onSubmit: (event) => {
134
+ event.preventDefault();
135
+ onSave();
136
+ } },
137
+ React.createElement(Tabs, { defaultValue: defaultTab },
398
138
  React.createElement(TabsList, null,
399
139
  React.createElement(TabsTrigger, { value: "number" }, "Number"),
400
140
  React.createElement(TabsTrigger, { value: "alignment" }, "Alignment"),
401
141
  React.createElement(TabsTrigger, { value: "font" }, "Font"),
402
- React.createElement(TabsTrigger, { value: "border" }, "Border")),
142
+ React.createElement(TabsTrigger, { value: "border" }, "Border"),
143
+ React.createElement(TabsTrigger, { value: "fill" }, "Fill"),
144
+ React.createElement(TabsTrigger, { value: "protection" }, "Protection")),
403
145
  React.createElement(TabsContent, { value: "number" },
404
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3" },
405
- React.createElement("label", { className: "font-system text-sm" }, "Sample"),
406
- React.createElement("div", { className: "text-rnc-foreground p-1 px-2 bg-rnc-muted rounded-sm", style: {
407
- color: formattedColor,
408
- } }, formattedValue)),
409
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3" },
410
- React.createElement("label", { className: "font-system text-sm" }, "Choose category"),
411
- React.createElement(Select, { value: isCustom ? "CUSTOM" : selectedNumberFormat, onValueChange: (value) => {
412
- const format = numberCategories.find((n) => n.value === value);
413
- if (format?.value === "CUSTOM") {
414
- // Show custom options
415
- setIsCustom(true);
416
- setCustomFormatValue(values.numberFormat);
417
- }
418
- else if (format) {
419
- // Toggle custom
420
- setIsCustom(false);
421
- // Apply formatting
422
- setFieldValue("numberFormat", {
423
- pattern: format.pattern
424
- ? supplant(format.pattern, { currencySymbol })
425
- : undefined,
426
- type: format.value,
427
- });
428
- }
429
- } },
430
- React.createElement(SelectTrigger, { className: "mb-1" },
431
- React.createElement(SelectValue, { placeholder: "None" })),
432
- React.createElement(SelectPortal, null,
433
- React.createElement(SelectContent, null, numberCategories.map(({ name, value }) => {
434
- return (React.createElement(SelectItem, { value: value, key: name }, name));
435
- }))))),
436
- isCustom ? null : (React.createElement(Stack, { className: "grid grid-cols-2 gap-2" }, selectedOption?.options?.map((opt) => {
437
- return opt === "Decimal" ? (React.createElement(Stack, { direction: "vertical", key: opt, className: "gap-1 pb-3" },
438
- React.createElement("label", { className: "font-system text-sm" }, "Decimal places"),
439
- React.createElement(Input, { min: 0, max: 10, type: "number", className: "text-xs", value: decimalCount, onChange: (event) => {
440
- const numDecimals = parseInt(event.target.value, 10);
441
- if (type) {
442
- setFieldValue("numberFormat", {
443
- type,
444
- pattern: changeDecimals(pattern ?? "#,##0.00", numDecimals, false),
445
- });
446
- }
447
- } }))) : opt === "ThousandSeparator" ? (React.createElement(Stack, { direction: "vertical", key: opt, className: "gap-1 pb-3" },
448
- React.createElement("label", { className: "font-system text-sm" }, "Thousands separator"),
449
- React.createElement("label", { className: "font-system text-xs flex items-center gap-1", key: opt },
450
- React.createElement("input", { type: "checkbox", checked: thousandsSeparator, onChange: (e) => {
451
- const checked = e.target.checked;
452
- const newPattern = checked
453
- ? addThousandSeparator(pattern ?? undefined)
454
- : removeThousandSeparator(pattern ?? undefined);
455
- setFieldValue("numberFormat", {
456
- type: type ?? "NUMBER",
457
- pattern: newPattern,
458
- });
459
- } }),
460
- "Use 1000 Separator (,)"))) : opt === "CurrencySymbol" ? (React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3", key: opt },
461
- React.createElement("label", { className: "font-system text-sm" }, "Currency Symbol"),
462
- React.createElement(Input, { type: "text", defaultValue: currencySymbol, className: "text-xs", onChange: (event) => {
463
- const currencyPattern = numberCategories.find((n) => n.value === "CURRENCY");
464
- if (currencyPattern && currencyPattern.pattern) {
465
- setFieldValue("numberFormat", {
466
- type: "CURRENCY",
467
- pattern: supplant(currencyPattern.pattern, {
468
- currencySymbol: event.target.value,
469
- }),
470
- });
471
- }
472
- } }))) : null;
473
- }))),
474
- isCustom ? (React.createElement("div", { className: "flex flex-col gap-2" },
475
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-1" },
476
- React.createElement("label", { className: "font-system text-sm" }, "Type"),
477
- React.createElement(Input, { type: "text", className: "text-xs", value: customFormatValue?.pattern ?? undefined, onChange: (e) => {
478
- setCustomFormatValue((prev) => {
479
- return {
480
- type: prev?.type ?? "GENERAL",
481
- pattern: e.target.value,
482
- };
483
- });
484
- setFieldValue("numberFormat", {
485
- type: customFormatValue?.type ?? "GENERAL",
486
- pattern: e.target.value,
487
- });
488
- } })),
489
- React.createElement("div", { className: overflowDivClass }, customFormats.map((format, idx) => {
490
- return (React.createElement(Button, { key: idx, className: "text-xs py-1 px-1.5", onClick: () => {
491
- setFieldValue("numberFormat", {
492
- type: format.type,
493
- pattern: supplant(format.pattern ?? "General", {
494
- currencySymbol,
495
- }),
496
- });
497
- // Set current format type
498
- setCustomFormatValue({
499
- type: format.type,
500
- pattern: supplant(format.pattern ?? "General", {
501
- currencySymbol,
502
- }),
503
- });
504
- } }, supplant(format.pattern ?? format.type, {
505
- currencySymbol,
506
- })));
507
- })))) : null,
508
- type === "DATE" && !isCustom ? (React.createElement("div", { className: overflowDivClass }, dateFormats.map((format, idx) => {
509
- return (React.createElement(Button, { key: idx, className: "text-xs py-1 px-1.5", onClick: () => {
510
- setFieldValue("numberFormat", {
511
- type: format.type,
512
- pattern: supplant(format.pattern ?? "General", {
513
- currencySymbol,
514
- }),
515
- });
516
- } }, ssfFormat(format.pattern ?? getDefaultDateFormat(locale), sampleDate)));
517
- }))) : null,
518
- type === "DATE_TIME" && !isCustom ? (React.createElement("div", { className: overflowDivClass }, timeFormats.map((format, idx) => {
519
- return (React.createElement(Button, { key: idx, className: "text-xs py-1 px-1.5", onClick: () => {
520
- setFieldValue("numberFormat", {
521
- type: format.type,
522
- pattern: supplant(format.pattern ?? "General", {
523
- currencySymbol,
524
- }),
525
- });
526
- } }, ssfFormat(format.pattern ?? getDefaultDateFormat(locale), sampleDate)));
527
- }))) : null),
146
+ React.createElement(NumberTab, { numberFormat: draft.numberFormat, onChange: (numberFormat) => patchDraft({ numberFormat }), sampleValue: effectiveValue, locale: locale, currencySymbol: currencySymbol })),
528
147
  React.createElement(TabsContent, { value: "alignment" },
529
- React.createElement(Stack, { className: "grid grid-cols-2 gap-2 pb-3" },
530
- React.createElement(Stack, { direction: "vertical", className: "gap-1" },
531
- React.createElement("label", { className: "font-system text-sm" }, "Horizontal Alignment"),
532
- React.createElement(Select, { value: values?.horizontalAlignment ?? "General", onValueChange: (value) => setFieldValue("horizontalAlignment", value) },
533
- React.createElement(SelectTrigger, { className: "mb-1" },
534
- React.createElement(SelectValue, { placeholder: "General" })),
535
- React.createElement(SelectPortal, null,
536
- React.createElement(SelectContent, null, horizontalAlignments.map(({ name, value = "General" }) => {
537
- return (React.createElement(SelectItem, { value: value, key: value }, name));
538
- }))))),
539
- React.createElement(Stack, { direction: "vertical", className: "gap-1" },
540
- React.createElement("label", { className: "font-system text-sm" }, "Indent"),
541
- React.createElement(Input, { value: values?.indent ?? 0, min: 0, max: 10, type: "number", className: "text-xs", onChange: (event) => {
542
- setFieldValue("indent", Number(event.target.value));
543
- } }))),
544
- React.createElement(Stack, { className: "grid grid-cols-2 gap-2" },
545
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3" },
546
- React.createElement("label", { className: "font-system text-sm" }, "Vertical Alignment"),
547
- React.createElement(Select, { value: values?.verticalAlignment ?? "bottom", onValueChange: (value) => setFieldValue("verticalAlignment", value) },
548
- React.createElement(SelectTrigger, { className: "mb-1" },
549
- React.createElement(SelectValue, { placeholder: "General" })),
550
- React.createElement(SelectPortal, null,
551
- React.createElement(SelectContent, null, verticalAlignments.map(({ name, value }) => {
552
- return (React.createElement(SelectItem, { value: value, key: value }, name));
553
- })))))),
554
- React.createElement("label", { className: "font-system text-sm pb-3" }, "Text control"),
555
- React.createElement(Stack, { className: "grid grid-cols-2 gap-2" },
556
- React.createElement(Button, { className: "flex items-center gap-2", onClick: () => {
557
- setFieldValue("wrapStrategy", "wrap");
558
- }, variant: "ghost" },
559
- React.createElement("span", { className: "text-lg leading-none" },
560
- React.createElement(TextOverflowWrapIcon, null)),
561
- "Wrap text"),
562
- React.createElement(Button, { className: "flex items-center gap-2", onClick: () => {
563
- onMergeCells?.(sheetId, activeCell, selections, Direction.Right);
564
- }, variant: "ghost" },
565
- React.createElement("span", { className: "text-lg leading-none" },
566
- React.createElement(AiOutlineMergeCells, null)),
567
- "Merge"))),
148
+ React.createElement(AlignmentTab, { format: draft, onChange: patchDraft, merged: merged, onChangeMerged: setMerged })),
568
149
  React.createElement(TabsContent, { value: "font" },
569
- React.createElement(Stack, { className: "grid grid-cols-2 gap-2 pb-3" },
570
- React.createElement(Stack, { direction: "vertical", className: "gap-1" },
571
- React.createElement("label", { className: "font-system text-sm" }, "Font"),
572
- React.createElement(FontFamilySelector, { theme: theme, value: values?.textFormat?.fontFamily, onChange: (fontFamily) => {
573
- setFieldValue("textFormat.fontFamily", fontFamily);
574
- }, triggerVariant: "outline" })),
575
- React.createElement(Stack, { direction: "vertical", className: "gap-1" },
576
- React.createElement("label", { className: "font-system text-sm" }, "Font size"),
577
- React.createElement(FontSizeSelector, { value: values?.textFormat?.fontSize, onChange: (fontSize) => {
578
- setFieldValue("textFormat.fontSize", fontSize);
579
- } }))),
580
- React.createElement(Stack, { className: "grid grid-cols-2 gap-2 pb-3" },
581
- React.createElement(Stack, { direction: "vertical", className: "gap-1" },
582
- React.createElement("label", { className: "font-system text-sm" }, "Effects"),
583
- React.createElement(Stack, { direction: "horizontal", className: "gap-2" },
584
- React.createElement(ButtonBold, { isActive: values?.textFormat?.bold, onClick: () => {
585
- setFieldValue("textFormat.bold", !values?.textFormat?.bold);
586
- } }),
587
- React.createElement(ButtonItalic, { isActive: values?.textFormat?.italic, onClick: () => {
588
- setFieldValue("textFormat.italic", !values?.textFormat?.italic);
589
- } }),
590
- React.createElement(ButtonUnderline, { isActive: values?.textFormat?.underline, onClick: () => {
591
- setFieldValue("textFormat.underline", !values?.textFormat?.underline);
592
- } }),
593
- React.createElement(ButtonStrikethrough, { isActive: values?.textFormat?.strikethrough, onClick: () => {
594
- setFieldValue("textFormat.strikethrough", !values?.textFormat?.strikethrough);
595
- } }))),
596
- React.createElement(Stack, { direction: "vertical", className: "gap-1" },
597
- React.createElement("label", { className: "font-system text-sm" }, "Color"),
598
- React.createElement("div", null,
599
- React.createElement(TextColorSelector, { color: values?.textFormat?.color, theme: theme, isDarkMode: isDarkMode, onChange: (color) => {
600
- setFieldValue("textFormat.color", color);
601
- } }))))),
150
+ React.createElement(FontTab, { textFormat: draft.textFormat ?? {}, onChange: patchText, theme: theme, isDarkMode: isDarkMode })),
602
151
  React.createElement(TabsContent, { value: "border" },
603
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3" },
604
- React.createElement("label", { className: "font-system text-sm" }, "Border style"),
605
- React.createElement(Stack, { direction: "horizontal", className: "flex flex-wrap gap-2 pb-3" },
606
- React.createElement(BorderLocationOptions, { borderLocation: borderLocation, onChange: (value) => {
607
- setBorderLocationState(value);
608
- // Update border
609
- onChangeBorder?.(sheetId, activeCell, selections, value, color ?? getDefaultBorderColor(), borderStyle);
610
- }, onClear: () => {
611
- // Update border
612
- onChangeBorder?.(sheetId, activeCell, selections, "all", undefined, undefined);
613
- } }))),
614
- React.createElement(Stack, { className: "grid grid-cols-3 gap-2" },
615
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3 col-span-2" },
616
- React.createElement("label", { className: "font-system text-sm" }, "Border Style"),
617
- React.createElement("div", { className: "flex flex-wrap gap-2" },
618
- React.createElement(BorderLineStyleOptions, { borderStyle: borderStyle, onChange: (value) => {
619
- setStyleState(value);
620
- // Update border
621
- onChangeBorder?.(sheetId, activeCell, selections, borderLocation ?? "all", color ?? getDefaultBorderColor(), value);
622
- } }))),
623
- React.createElement(Stack, { direction: "vertical", className: "gap-1 pb-3" },
624
- React.createElement("label", { className: "font-system text-sm" }, "Border color"),
625
- React.createElement("div", null,
626
- React.createElement(BorderColorSelector, { color: color ?? getDefaultBorderColor(), onChange: (value) => {
627
- setColorState(value);
628
- // Update border
629
- onChangeBorder?.(sheetId, activeCell, selections, borderLocation ?? "all", value, borderStyle);
630
- }, theme: theme })))))),
152
+ React.createElement(BorderTab, { edges: shownEdges, onEdit: (edits, options) => {
153
+ if (options?.clearedAll) {
154
+ setClearedAll(true);
155
+ setEdgeEdits({});
156
+ return;
157
+ }
158
+ setEdgeEdits((prev) => ({ ...prev, ...edits }));
159
+ }, lineStyle: lineStyle, onLineStyle: setLineStyle, color: borderColor, onColor: setBorderColor, hasInsideRows: spans.rows, hasInsideColumns: spans.columns, theme: theme })),
160
+ React.createElement(TabsContent, { value: "fill" },
161
+ React.createElement(FillTab, { fill: fill, onChange: (next) => patchDraft(next), theme: theme })),
162
+ React.createElement(TabsContent, { value: "protection" },
163
+ React.createElement(ProtectionTab, { protection: protection, onChange: setProtection }))),
631
164
  React.createElement("div", { className: "flex gap-1 py-2" },
632
- React.createElement(Button, { type: "submit", variant: "primary", size: "xs" }, "Save"),
165
+ React.createElement(Button, { type: "submit", variant: "primary", size: "xs" }, "OK"),
633
166
  React.createElement(Button, { size: "xs", onClick: onCancelForm, type: "button" }, "Cancel"))));
634
167
  };
635
168
  export const CellFormatEditorDialog = ({ children, }) => {