@cj-tech-master/excelts 4.0.4 → 4.1.0-canary.20260110033117.75bcac1

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 (92) hide show
  1. package/dist/browser/index.browser.d.ts +2 -0
  2. package/dist/browser/index.browser.js +1 -0
  3. package/dist/browser/index.d.ts +2 -0
  4. package/dist/browser/index.js +1 -0
  5. package/dist/browser/modules/excel/cell.js +39 -1
  6. package/dist/browser/modules/excel/enums.d.ts +2 -1
  7. package/dist/browser/modules/excel/enums.js +2 -1
  8. package/dist/browser/modules/excel/form-control.d.ts +157 -0
  9. package/dist/browser/modules/excel/form-control.js +267 -0
  10. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +1 -0
  11. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +19 -1
  12. package/dist/browser/modules/excel/table.d.ts +6 -2
  13. package/dist/browser/modules/excel/table.js +33 -5
  14. package/dist/browser/modules/excel/types.d.ts +5 -1
  15. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +4 -0
  16. package/dist/browser/modules/excel/utils/ooxml-paths.js +12 -2
  17. package/dist/browser/modules/excel/worksheet.d.ts +32 -0
  18. package/dist/browser/modules/excel/worksheet.js +44 -1
  19. package/dist/browser/modules/excel/xlsx/rel-type.d.ts +2 -0
  20. package/dist/browser/modules/excel/xlsx/rel-type.js +3 -1
  21. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +24 -1
  22. package/dist/browser/modules/excel/xlsx/xform/core/feature-property-bag-xform.d.ts +8 -0
  23. package/dist/browser/modules/excel/xlsx/xform/core/feature-property-bag-xform.js +36 -0
  24. package/dist/browser/modules/excel/xlsx/xform/drawing/ctrl-prop-xform.d.ts +22 -0
  25. package/dist/browser/modules/excel/xlsx/xform/drawing/ctrl-prop-xform.js +52 -0
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +44 -0
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +181 -0
  28. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.js +5 -0
  29. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +24 -1
  30. package/dist/browser/modules/excel/xlsx/xform/style/style-xform.d.ts +2 -0
  31. package/dist/browser/modules/excel/xlsx/xform/style/style-xform.js +11 -0
  32. package/dist/browser/modules/excel/xlsx/xform/style/styles-xform.d.ts +2 -0
  33. package/dist/browser/modules/excel/xlsx/xform/style/styles-xform.js +28 -4
  34. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +3 -0
  35. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +43 -5
  36. package/dist/cjs/index.js +3 -1
  37. package/dist/cjs/modules/excel/cell.js +39 -1
  38. package/dist/cjs/modules/excel/enums.js +2 -1
  39. package/dist/cjs/modules/excel/form-control.js +270 -0
  40. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +19 -1
  41. package/dist/cjs/modules/excel/table.js +33 -5
  42. package/dist/cjs/modules/excel/utils/ooxml-paths.js +14 -2
  43. package/dist/cjs/modules/excel/worksheet.js +44 -1
  44. package/dist/cjs/modules/excel/xlsx/rel-type.js +3 -1
  45. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +23 -0
  46. package/dist/cjs/modules/excel/xlsx/xform/core/feature-property-bag-xform.js +39 -0
  47. package/dist/cjs/modules/excel/xlsx/xform/drawing/ctrl-prop-xform.js +55 -0
  48. package/dist/cjs/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +184 -0
  49. package/dist/cjs/modules/excel/xlsx/xform/sheet/cell-xform.js +5 -0
  50. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +23 -0
  51. package/dist/cjs/modules/excel/xlsx/xform/style/style-xform.js +11 -0
  52. package/dist/cjs/modules/excel/xlsx/xform/style/styles-xform.js +28 -4
  53. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +42 -4
  54. package/dist/esm/index.browser.js +1 -0
  55. package/dist/esm/index.js +1 -0
  56. package/dist/esm/modules/excel/cell.js +39 -1
  57. package/dist/esm/modules/excel/enums.js +2 -1
  58. package/dist/esm/modules/excel/form-control.js +267 -0
  59. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +19 -1
  60. package/dist/esm/modules/excel/table.js +33 -5
  61. package/dist/esm/modules/excel/utils/ooxml-paths.js +12 -2
  62. package/dist/esm/modules/excel/worksheet.js +44 -1
  63. package/dist/esm/modules/excel/xlsx/rel-type.js +3 -1
  64. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +24 -1
  65. package/dist/esm/modules/excel/xlsx/xform/core/feature-property-bag-xform.js +36 -0
  66. package/dist/esm/modules/excel/xlsx/xform/drawing/ctrl-prop-xform.js +52 -0
  67. package/dist/esm/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +181 -0
  68. package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +5 -0
  69. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +24 -1
  70. package/dist/esm/modules/excel/xlsx/xform/style/style-xform.js +11 -0
  71. package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +28 -4
  72. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +43 -5
  73. package/dist/iife/excelts.iife.js +629 -40
  74. package/dist/iife/excelts.iife.js.map +1 -1
  75. package/dist/iife/excelts.iife.min.js +30 -30
  76. package/dist/types/index.browser.d.ts +2 -0
  77. package/dist/types/index.d.ts +2 -0
  78. package/dist/types/modules/excel/enums.d.ts +2 -1
  79. package/dist/types/modules/excel/form-control.d.ts +157 -0
  80. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +1 -0
  81. package/dist/types/modules/excel/table.d.ts +6 -2
  82. package/dist/types/modules/excel/types.d.ts +5 -1
  83. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +4 -0
  84. package/dist/types/modules/excel/worksheet.d.ts +32 -0
  85. package/dist/types/modules/excel/xlsx/rel-type.d.ts +2 -0
  86. package/dist/types/modules/excel/xlsx/xform/core/feature-property-bag-xform.d.ts +8 -0
  87. package/dist/types/modules/excel/xlsx/xform/drawing/ctrl-prop-xform.d.ts +22 -0
  88. package/dist/types/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +44 -0
  89. package/dist/types/modules/excel/xlsx/xform/style/style-xform.d.ts +2 -0
  90. package/dist/types/modules/excel/xlsx/xform/style/styles-xform.d.ts +2 -0
  91. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +3 -0
  92. package/package.json +9 -9
@@ -0,0 +1,267 @@
1
+ import { colCache } from "./utils/col-cache.js";
2
+ /**
3
+ * Form Control Checkbox - Legacy checkbox control compatible with Office 2007+ and WPS/LibreOffice
4
+ *
5
+ * Unlike the modern In-Cell Checkbox (which only works in Microsoft 365),
6
+ * Form Control Checkboxes are floating controls that work in virtually all
7
+ * spreadsheet applications.
8
+ */
9
+ // ============================================================================
10
+ // Constants (exported for use by xforms)
11
+ // ============================================================================
12
+ /** EMU (English Metric Units) to pixels conversion factor at 96 DPI */
13
+ export const EMU_PER_PIXEL = 9525;
14
+ /** EMU to points conversion factor */
15
+ export const EMU_PER_POINT = 12700;
16
+ /** Default column offset in EMUs (~15 pixels) */
17
+ const DEFAULT_COL_OFF = 142875;
18
+ /** Default row offset in EMUs (~3 pixels) */
19
+ const DEFAULT_ROW_OFF = 28575;
20
+ /** Default end column offset in EMUs (~29 pixels) */
21
+ const DEFAULT_END_COL_OFF = 276225;
22
+ /** Default end row offset in EMUs (~20 pixels) */
23
+ const DEFAULT_END_ROW_OFF = 190500;
24
+ // ============================================================================
25
+ // FormCheckbox Class
26
+ // ============================================================================
27
+ class FormCheckbox {
28
+ constructor(worksheet, range, options) {
29
+ this.worksheet = worksheet;
30
+ // Parse range to get anchors
31
+ const { tl, br } = this._parseRange(range);
32
+ // Generate shape ID (starting from 1025)
33
+ const existingCount = worksheet.formControls?.length || 0;
34
+ const shapeId = 1025 + existingCount;
35
+ // Parse link cell reference
36
+ let link;
37
+ if (options?.link) {
38
+ // Ensure absolute reference format
39
+ link = this._toAbsoluteRef(options.link);
40
+ }
41
+ // Note: ctrlPropId is set later in worksheet-xform.ts prepare() for global uniqueness
42
+ this.model = {
43
+ shapeId,
44
+ ctrlPropId: 0, // Placeholder, set during prepare()
45
+ tl,
46
+ br,
47
+ link,
48
+ checked: options?.checked ? "Checked" : "Unchecked",
49
+ text: options?.text ?? "",
50
+ noThreeD: options?.noThreeD ?? true,
51
+ print: options?.print ?? false
52
+ };
53
+ }
54
+ /**
55
+ * Get the checked state
56
+ */
57
+ get checked() {
58
+ return this.model.checked === "Checked";
59
+ }
60
+ /**
61
+ * Set the checked state
62
+ */
63
+ set checked(value) {
64
+ this.model.checked = value ? "Checked" : "Unchecked";
65
+ }
66
+ /**
67
+ * Get the linked cell address
68
+ */
69
+ get link() {
70
+ return this.model.link;
71
+ }
72
+ /**
73
+ * Set the linked cell address
74
+ */
75
+ set link(value) {
76
+ this.model.link = value ? this._toAbsoluteRef(value) : undefined;
77
+ }
78
+ /**
79
+ * Get the label text
80
+ */
81
+ get text() {
82
+ return this.model.text;
83
+ }
84
+ /**
85
+ * Set the label text
86
+ */
87
+ set text(value) {
88
+ this.model.text = value;
89
+ }
90
+ /**
91
+ * Convert cell reference to absolute format (e.g., "A1" -> "$A$1")
92
+ */
93
+ _toAbsoluteRef(ref) {
94
+ // If already absolute, return as-is
95
+ if (ref.includes("$")) {
96
+ return ref;
97
+ }
98
+ // Parse and convert
99
+ const addr = colCache.decodeAddress(ref);
100
+ return `$${colCache.n2l(addr.col)}$${addr.row}`;
101
+ }
102
+ /**
103
+ * Parse range input into anchor positions
104
+ */
105
+ _parseRange(range) {
106
+ let tl;
107
+ let br;
108
+ if (typeof range === "string") {
109
+ // Parse cell reference like "B2" or "B2:D3"
110
+ const decoded = colCache.decode(range);
111
+ if ("top" in decoded) {
112
+ // It's a range like "B2:D3"
113
+ tl = {
114
+ col: decoded.left - 1, // Convert to 0-based
115
+ colOff: DEFAULT_COL_OFF,
116
+ row: decoded.top - 1,
117
+ rowOff: DEFAULT_ROW_OFF
118
+ };
119
+ br = {
120
+ col: decoded.right - 1,
121
+ colOff: DEFAULT_END_COL_OFF,
122
+ row: decoded.bottom - 1,
123
+ rowOff: DEFAULT_END_ROW_OFF
124
+ };
125
+ }
126
+ else {
127
+ // Single cell reference - create default size checkbox
128
+ tl = {
129
+ col: decoded.col - 1,
130
+ colOff: DEFAULT_COL_OFF,
131
+ row: decoded.row - 1,
132
+ rowOff: DEFAULT_ROW_OFF
133
+ };
134
+ // Default size: about 2 columns wide, 1 row tall
135
+ br = {
136
+ col: decoded.col + 1,
137
+ colOff: DEFAULT_END_COL_OFF,
138
+ row: decoded.row,
139
+ rowOff: DEFAULT_END_ROW_OFF
140
+ };
141
+ }
142
+ }
143
+ else if ("startCol" in range) {
144
+ // startCol/startRow/endCol/endRow format (0-based)
145
+ tl = {
146
+ col: range.startCol,
147
+ colOff: range.startColOff ?? DEFAULT_COL_OFF,
148
+ row: range.startRow,
149
+ rowOff: range.startRowOff ?? DEFAULT_ROW_OFF
150
+ };
151
+ br = {
152
+ col: range.endCol,
153
+ colOff: range.endColOff ?? DEFAULT_END_COL_OFF,
154
+ row: range.endRow,
155
+ rowOff: range.endRowOff ?? DEFAULT_END_ROW_OFF
156
+ };
157
+ }
158
+ else {
159
+ // Object format with tl/br
160
+ if (typeof range.tl === "string") {
161
+ const decoded = colCache.decodeAddress(range.tl);
162
+ tl = {
163
+ col: decoded.col - 1,
164
+ colOff: DEFAULT_COL_OFF,
165
+ row: decoded.row - 1,
166
+ rowOff: DEFAULT_ROW_OFF
167
+ };
168
+ }
169
+ else {
170
+ tl = {
171
+ col: range.tl.col,
172
+ colOff: range.tl.colOff ?? DEFAULT_COL_OFF,
173
+ row: range.tl.row,
174
+ rowOff: range.tl.rowOff ?? DEFAULT_ROW_OFF
175
+ };
176
+ }
177
+ if (range.br) {
178
+ if (typeof range.br === "string") {
179
+ const decoded = colCache.decodeAddress(range.br);
180
+ br = {
181
+ col: decoded.col - 1,
182
+ colOff: DEFAULT_END_COL_OFF,
183
+ row: decoded.row - 1,
184
+ rowOff: DEFAULT_END_ROW_OFF
185
+ };
186
+ }
187
+ else {
188
+ br = {
189
+ col: range.br.col,
190
+ colOff: range.br.colOff ?? DEFAULT_END_COL_OFF,
191
+ row: range.br.row,
192
+ rowOff: range.br.rowOff ?? DEFAULT_END_ROW_OFF
193
+ };
194
+ }
195
+ }
196
+ else {
197
+ // Default size
198
+ br = {
199
+ col: tl.col + 2,
200
+ colOff: DEFAULT_END_COL_OFF,
201
+ row: tl.row + 1,
202
+ rowOff: DEFAULT_END_ROW_OFF
203
+ };
204
+ }
205
+ }
206
+ return { tl, br };
207
+ }
208
+ // =========================================================================
209
+ // Instance methods - delegate to static methods
210
+ // =========================================================================
211
+ /**
212
+ * Convert anchor to VML anchor string format
213
+ * Format: "fromCol, fromColOff, fromRow, fromRowOff, toCol, toColOff, toRow, toRowOff"
214
+ * VML uses pixels for offsets
215
+ */
216
+ getVmlAnchor() {
217
+ return FormCheckbox.getVmlAnchor(this.model);
218
+ }
219
+ /**
220
+ * Get VML style string for positioning
221
+ */
222
+ getVmlStyle() {
223
+ return FormCheckbox.getVmlStyle(this.model);
224
+ }
225
+ /**
226
+ * Get the numeric checked value for VML (0, 1, or 2)
227
+ */
228
+ getVmlCheckedValue() {
229
+ return FormCheckbox.getVmlCheckedValue(this.model);
230
+ }
231
+ // =========================================================================
232
+ // Static utility methods - can be used with FormCheckboxModel directly
233
+ // =========================================================================
234
+ /**
235
+ * Convert anchor to VML anchor string format from model
236
+ */
237
+ static getVmlAnchor(model) {
238
+ const { tl, br } = model;
239
+ const tlColOff = Math.round(tl.colOff / EMU_PER_PIXEL);
240
+ const tlRowOff = Math.round(tl.rowOff / EMU_PER_PIXEL);
241
+ const brColOff = Math.round(br.colOff / EMU_PER_PIXEL);
242
+ const brRowOff = Math.round(br.rowOff / EMU_PER_PIXEL);
243
+ return `${tl.col}, ${tlColOff}, ${tl.row}, ${tlRowOff}, ${br.col}, ${brColOff}, ${br.row}, ${brRowOff}`;
244
+ }
245
+ /**
246
+ * Get VML style string for positioning from model
247
+ */
248
+ static getVmlStyle(model) {
249
+ const marginLeft = Math.round(model.tl.colOff / EMU_PER_POINT);
250
+ const marginTop = Math.round(model.tl.rowOff / EMU_PER_POINT);
251
+ return `position:absolute;margin-left:${marginLeft}pt;margin-top:${marginTop}pt;width:96pt;height:18pt;z-index:1;visibility:visible`;
252
+ }
253
+ /**
254
+ * Get the numeric checked value for VML from model (0, 1, or 2)
255
+ */
256
+ static getVmlCheckedValue(model) {
257
+ switch (model.checked) {
258
+ case "Checked":
259
+ return 1;
260
+ case "Mixed":
261
+ return 2;
262
+ default:
263
+ return 0;
264
+ }
265
+ }
266
+ }
267
+ export { FormCheckbox };
@@ -20,6 +20,7 @@ import { ContentTypesXform } from "../xlsx/xform/core/content-types-xform.js";
20
20
  import { AppXform } from "../xlsx/xform/core/app-xform.js";
21
21
  import { WorkbookXform } from "../xlsx/xform/book/workbook-xform.js";
22
22
  import { SharedStringsXform } from "../xlsx/xform/strings/shared-strings-xform.js";
23
+ import { FeaturePropertyBagXform } from "../xlsx/xform/core/feature-property-bag-xform.js";
23
24
  import { theme1Xml } from "../xlsx/xml/theme1.js";
24
25
  import { Writeable, stringToUint8Array } from "../../stream/index.js";
25
26
  import { mediaPath, OOXML_PATHS, OOXML_REL_TARGETS, worksheetRelTarget } from "../utils/ooxml-paths.js";
@@ -134,6 +135,7 @@ export class WorkbookWriterBase {
134
135
  this.addCore(),
135
136
  this.addSharedStrings(),
136
137
  this.addStyles(),
138
+ this.addFeaturePropertyBag(),
137
139
  this.addWorkbookRels()
138
140
  ]);
139
141
  await this.addWorkbook();
@@ -226,7 +228,8 @@ export class WorkbookWriterBase {
226
228
  worksheets: this._worksheets.filter(Boolean),
227
229
  sharedStrings: this.sharedStrings,
228
230
  commentRefs: this.commentRefs,
229
- media: this.media
231
+ media: this.media,
232
+ hasCheckboxes: this.styles.hasCheckboxes
230
233
  };
231
234
  const xform = new ContentTypesXform();
232
235
  this._addFile(xform.toXml(model), OOXML_PATHS.contentTypes);
@@ -280,6 +283,13 @@ export class WorkbookWriterBase {
280
283
  }
281
284
  return Promise.resolve();
282
285
  }
286
+ addFeaturePropertyBag() {
287
+ if (this.styles.hasCheckboxes) {
288
+ const xform = new FeaturePropertyBagXform();
289
+ this._addFile(xform.toXml({}), OOXML_PATHS.xlFeaturePropertyBag);
290
+ }
291
+ return Promise.resolve();
292
+ }
283
293
  addWorkbookRels() {
284
294
  let count = 1;
285
295
  const relationships = [
@@ -293,6 +303,14 @@ export class WorkbookWriterBase {
293
303
  Target: OOXML_REL_TARGETS.workbookSharedStrings
294
304
  });
295
305
  }
306
+ // Add FeaturePropertyBag relationship if checkboxes are used
307
+ if (this.styles.hasCheckboxes) {
308
+ relationships.push({
309
+ Id: `rId${count++}`,
310
+ Type: RelType.FeaturePropertyBag,
311
+ Target: OOXML_REL_TARGETS.workbookFeaturePropertyBag
312
+ });
313
+ }
296
314
  this._worksheets.forEach(ws => {
297
315
  if (ws) {
298
316
  ws.rId = `rId${count++}`;
@@ -57,6 +57,26 @@ class Table {
57
57
  this.worksheet = worksheet;
58
58
  if (table) {
59
59
  this.table = table;
60
+ // When loading tables from xlsx, Excel stores table ranges and cell values in the worksheet,
61
+ // but may not embed row data into the table definition. Hydrate rows from the worksheet so
62
+ // table mutations (e.g. addRow) can correctly expand table ranges and serialize.
63
+ if (Array.isArray(table.rows) && table.rows.length === 0 && table.tableRef) {
64
+ const decoded = colCache.decode(table.tableRef);
65
+ if ("dimensions" in decoded) {
66
+ const startRow = decoded.top + (table.headerRow === false ? 0 : 1);
67
+ const endRow = decoded.bottom - (table.totalsRow === true ? 1 : 0);
68
+ if (endRow >= startRow) {
69
+ for (let r = startRow; r <= endRow; r++) {
70
+ const row = worksheet.getRow(r);
71
+ const values = [];
72
+ for (let c = decoded.left; c <= decoded.right; c++) {
73
+ values.push(row.getCell(c).value);
74
+ }
75
+ table.rows.push(values);
76
+ }
77
+ }
78
+ }
79
+ }
60
80
  // check things are ok first
61
81
  this.validate();
62
82
  this.store();
@@ -133,9 +153,10 @@ class Table {
133
153
  const { row, col } = table.tl;
134
154
  assert(row > 0, "Table must be on valid row");
135
155
  assert(col > 0, "Table must be on valid col");
136
- const { width, filterHeight, tableHeight } = this;
137
- // autoFilterRef is a range that includes optional headers only
138
- table.autoFilterRef = colCache.encode(row, col, row + filterHeight - 1, col + width - 1);
156
+ const { width, tableHeight } = this;
157
+ // autoFilterRef is a single-row range that targets the header row only.
158
+ // Excel uses this for filter buttons; including data rows can break filter rendering.
159
+ table.autoFilterRef = colCache.encode(row, col, row, col + width - 1);
139
160
  // tableRef is a range that includes optional headers and totals
140
161
  table.tableRef = colCache.encode(row, col, row + tableHeight - 1, col + width - 1);
141
162
  table.columns.forEach((column, i) => {
@@ -304,8 +325,9 @@ class Table {
304
325
  }
305
326
  }
306
327
  this.store();
328
+ this._cache = undefined;
307
329
  }
308
- addRow(values, rowNumber) {
330
+ addRow(values, rowNumber, options) {
309
331
  // Add a row of data, either insert at rowNumber or append
310
332
  this.cacheState();
311
333
  if (rowNumber === undefined) {
@@ -314,11 +336,17 @@ class Table {
314
336
  else {
315
337
  this.table.rows.splice(rowNumber, 0, values);
316
338
  }
339
+ if (options?.commit !== false) {
340
+ this.commit();
341
+ }
317
342
  }
318
- removeRows(rowIndex, count = 1) {
343
+ removeRows(rowIndex, count = 1, options) {
319
344
  // Remove a rows of data
320
345
  this.cacheState();
321
346
  this.table.rows.splice(rowIndex, count);
347
+ if (options?.commit !== false) {
348
+ this.commit();
349
+ }
322
350
  }
323
351
  getColumn(colIndex) {
324
352
  const column = this.table.columns[colIndex];
@@ -7,7 +7,8 @@ export const OOXML_PATHS = {
7
7
  xlWorkbookRels: "xl/_rels/workbook.xml.rels",
8
8
  xlSharedStrings: "xl/sharedStrings.xml",
9
9
  xlStyles: "xl/styles.xml",
10
- xlTheme1: "xl/theme/theme1.xml"
10
+ xlTheme1: "xl/theme/theme1.xml",
11
+ xlFeaturePropertyBag: "xl/featurePropertyBag/featurePropertyBag.xml"
11
12
  };
12
13
  const worksheetXmlRegex = /^xl\/worksheets\/sheet(\d+)[.]xml$/;
13
14
  const worksheetRelsXmlRegex = /^xl\/worksheets\/_rels\/sheet(\d+)[.]xml[.]rels$/;
@@ -162,7 +163,8 @@ export const OOXML_REL_TARGETS = {
162
163
  // Targets inside xl/_rels/workbook.xml.rels (base: xl/)
163
164
  workbookStyles: "styles.xml",
164
165
  workbookSharedStrings: "sharedStrings.xml",
165
- workbookTheme1: "theme/theme1.xml"
166
+ workbookTheme1: "theme/theme1.xml",
167
+ workbookFeaturePropertyBag: "featurePropertyBag/featurePropertyBag.xml"
166
168
  };
167
169
  export function pivotCacheDefinitionRelTargetFromWorkbook(n) {
168
170
  // Target inside xl/_rels/workbook.xml.rels (base: xl/)
@@ -207,3 +209,11 @@ export function mediaRelTargetFromRels(filename) {
207
209
  // Target from a rels file located under xl/*/_rels (base is one level deeper than xl/)
208
210
  return `../media/${filename}`;
209
211
  }
212
+ // Form Control (ctrlProps) path functions
213
+ export function ctrlPropPath(id) {
214
+ return `xl/ctrlProps/ctrlProp${id}.xml`;
215
+ }
216
+ export function ctrlPropRelTargetFromWorksheet(id) {
217
+ // Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
218
+ return `../ctrlProps/ctrlProp${id}.xml`;
219
+ }
@@ -6,6 +6,7 @@ import { Enums } from "./enums.js";
6
6
  import { Image } from "./image.js";
7
7
  import { Table } from "./table.js";
8
8
  import { DataValidations } from "./data-validations.js";
9
+ import { FormCheckbox } from "./form-control.js";
9
10
  import { Encryptor } from "./utils/encryptor.js";
10
11
  import { uint8ArrayToBase64 } from "../../utils/utils.js";
11
12
  import { makePivotTable } from "./pivot-table.js";
@@ -92,6 +93,8 @@ class Worksheet {
92
93
  this.tables = {};
93
94
  this.pivotTables = [];
94
95
  this.conditionalFormattings = [];
96
+ // for form controls (legacy checkboxes, etc.)
97
+ this.formControls = [];
95
98
  }
96
99
  get name() {
97
100
  return this._name;
@@ -745,6 +748,43 @@ class Worksheet {
745
748
  return image && image.imageId;
746
749
  }
747
750
  // =========================================================================
751
+ // Form Controls (Legacy Checkboxes)
752
+ /**
753
+ * Add a form control checkbox to the worksheet.
754
+ *
755
+ * Form control checkboxes are the legacy style that work in Office 2007+,
756
+ * WPS Office, LibreOffice, and other spreadsheet applications.
757
+ *
758
+ * Unlike modern in-cell checkboxes (which only work in Microsoft 365),
759
+ * form control checkboxes are floating controls positioned over cells.
760
+ *
761
+ * @param range - Cell reference (e.g., "B2") or range (e.g., "B2:D3") for positioning
762
+ * @param options - Checkbox options
763
+ * @returns The created FormCheckbox instance
764
+ *
765
+ * @example
766
+ * // Simple checkbox at B2
767
+ * ws.addFormCheckbox("B2");
768
+ *
769
+ * // Checkbox with label and linked cell
770
+ * ws.addFormCheckbox("B2:D3", {
771
+ * text: "Accept terms",
772
+ * link: "A2",
773
+ * checked: false
774
+ * });
775
+ */
776
+ addFormCheckbox(range, options) {
777
+ const checkbox = new FormCheckbox(this, range, options);
778
+ this.formControls.push(checkbox);
779
+ return checkbox;
780
+ }
781
+ /**
782
+ * Get all form control checkboxes in the worksheet
783
+ */
784
+ getFormCheckboxes() {
785
+ return this.formControls;
786
+ }
787
+ // =========================================================================
748
788
  // Worksheet Protection
749
789
  /**
750
790
  * Protect the worksheet with optional password and options
@@ -853,7 +893,8 @@ class Worksheet {
853
893
  sheetProtection: this.sheetProtection,
854
894
  tables: Object.values(this.tables).map(table => table.model),
855
895
  pivotTables: this.pivotTables,
856
- conditionalFormattings: this.conditionalFormattings
896
+ conditionalFormattings: this.conditionalFormattings,
897
+ formControls: this.formControls.map(fc => fc.model)
857
898
  };
858
899
  // =================================================
859
900
  // columns
@@ -919,6 +960,8 @@ class Worksheet {
919
960
  }, {});
920
961
  this.pivotTables = value.pivotTables;
921
962
  this.conditionalFormattings = value.conditionalFormattings;
963
+ // Form controls are currently write-only (not parsed from XLSX)
964
+ this.formControls = [];
922
965
  }
923
966
  }
924
967
  export { Worksheet };
@@ -14,6 +14,8 @@ const RelType = {
14
14
  Table: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table",
15
15
  PivotCacheDefinition: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition",
16
16
  PivotCacheRecords: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords",
17
- PivotTable: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
17
+ PivotTable: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable",
18
+ FeaturePropertyBag: "http://schemas.microsoft.com/office/2022/11/relationships/FeaturePropertyBag",
19
+ CtrlProp: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp"
18
20
  };
19
21
  export { RelType };
@@ -1,6 +1,6 @@
1
1
  import { XmlStream } from "../../../utils/xml-stream.js";
2
2
  import { BaseXform } from "../base-xform.js";
3
- import { OOXML_PATHS, commentsPathFromName, drawingPath, pivotCacheDefinitionPath, pivotCacheRecordsPath, pivotTablePath, tablePath, toContentTypesPartName, worksheetPath } from "../../../utils/ooxml-paths.js";
3
+ import { OOXML_PATHS, commentsPathFromName, ctrlPropPath, drawingPath, pivotCacheDefinitionPath, pivotCacheRecordsPath, pivotTablePath, tablePath, toContentTypesPartName, worksheetPath } from "../../../utils/ooxml-paths.js";
4
4
  // used for rendering the [Content_Types].xml file
5
5
  // not used for parsing
6
6
  class ContentTypesXform extends BaseXform {
@@ -63,6 +63,13 @@ class ContentTypesXform extends BaseXform {
63
63
  PartName: toContentTypesPartName(OOXML_PATHS.xlStyles),
64
64
  ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
65
65
  });
66
+ // Add FeaturePropertyBag if checkboxes are used
67
+ if (model.hasCheckboxes) {
68
+ xmlStream.leafNode("Override", {
69
+ PartName: toContentTypesPartName(OOXML_PATHS.xlFeaturePropertyBag),
70
+ ContentType: "application/vnd.ms-excel.featurepropertybag+xml"
71
+ });
72
+ }
66
73
  const hasSharedStrings = model.sharedStrings && model.sharedStrings.count;
67
74
  if (hasSharedStrings) {
68
75
  xmlStream.leafNode("Override", {
@@ -98,6 +105,22 @@ class ContentTypesXform extends BaseXform {
98
105
  });
99
106
  });
100
107
  }
108
+ // Add form control (ctrlProps) content types
109
+ if (model.formControlRefs) {
110
+ // Ensure vml extension is declared (may already be declared for comments)
111
+ if (!model.commentRefs) {
112
+ xmlStream.leafNode("Default", {
113
+ Extension: "vml",
114
+ ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
115
+ });
116
+ }
117
+ for (const ctrlPropId of model.formControlRefs) {
118
+ xmlStream.leafNode("Override", {
119
+ PartName: toContentTypesPartName(ctrlPropPath(ctrlPropId)),
120
+ ContentType: "application/vnd.ms-excel.controlproperties+xml"
121
+ });
122
+ }
123
+ }
101
124
  xmlStream.leafNode("Override", {
102
125
  PartName: toContentTypesPartName(OOXML_PATHS.docPropsCore),
103
126
  ContentType: "application/vnd.openxmlformats-package.core-properties+xml"
@@ -0,0 +1,36 @@
1
+ import { BaseXform } from "../base-xform.js";
2
+ // FeaturePropertyBag is used to enable checkbox functionality
3
+ // This is a static XML file that MS Excel requires for checkboxes to work
4
+ class FeaturePropertyBagXform extends BaseXform {
5
+ render(xmlStream) {
6
+ xmlStream.openXml({ version: "1.0", encoding: "UTF-8", standalone: "yes" });
7
+ xmlStream.openNode("FeaturePropertyBags", {
8
+ xmlns: "http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag"
9
+ });
10
+ // Checkbox feature
11
+ xmlStream.leafNode("bag", { type: "Checkbox" });
12
+ // XFControls bag
13
+ xmlStream.openNode("bag", { type: "XFControls" });
14
+ xmlStream.leafNode("bagId", { k: "CellControl" }, "0");
15
+ xmlStream.closeNode();
16
+ // XFComplement bag
17
+ xmlStream.openNode("bag", { type: "XFComplement" });
18
+ xmlStream.leafNode("bagId", { k: "XFControls" }, "1");
19
+ xmlStream.closeNode();
20
+ // XFComplements bag
21
+ xmlStream.openNode("bag", { type: "XFComplements", extRef: "XFComplementsMapperExtRef" });
22
+ xmlStream.openNode("a", { k: "MappedFeaturePropertyBags" });
23
+ xmlStream.leafNode("bagId", {}, "2");
24
+ xmlStream.closeNode();
25
+ xmlStream.closeNode();
26
+ xmlStream.closeNode();
27
+ }
28
+ parseOpen() {
29
+ return false;
30
+ }
31
+ parseText() { }
32
+ parseClose() {
33
+ return false;
34
+ }
35
+ }
36
+ export { FeaturePropertyBagXform };
@@ -0,0 +1,52 @@
1
+ import { XmlStream } from "../../../utils/xml-stream.js";
2
+ import { BaseXform } from "../base-xform.js";
3
+ /**
4
+ * Control Properties Xform - Generates ctrlProp*.xml for form controls
5
+ *
6
+ * Each form control (checkbox, button, etc.) has an associated ctrlProp file
7
+ * that stores its properties like objectType, checked state, and linked cell.
8
+ */
9
+ class CtrlPropXform extends BaseXform {
10
+ get tag() {
11
+ return "formControlPr";
12
+ }
13
+ render(xmlStream, model) {
14
+ const renderModel = model || this.model;
15
+ const attrs = {
16
+ xmlns: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main",
17
+ objectType: "CheckBox",
18
+ checked: renderModel.checked,
19
+ lockText: "1"
20
+ };
21
+ // Add linked cell reference
22
+ if (renderModel.link) {
23
+ attrs.fmlaLink = renderModel.link;
24
+ }
25
+ // Add noThreeD for flat appearance
26
+ if (renderModel.noThreeD) {
27
+ attrs.noThreeD = "1";
28
+ }
29
+ xmlStream.openXml({ version: "1.0", encoding: "UTF-8", standalone: "yes" });
30
+ xmlStream.leafNode(this.tag, attrs);
31
+ }
32
+ /**
33
+ * Generate XML string directly (convenience method)
34
+ * Uses render() internally to ensure consistency
35
+ */
36
+ toXml(model) {
37
+ const xmlStream = new XmlStream();
38
+ this.render(xmlStream, model);
39
+ return xmlStream.xml;
40
+ }
41
+ // Parsing not implemented - form controls are write-only for now
42
+ parseOpen() {
43
+ return true;
44
+ }
45
+ parseText() {
46
+ // Not implemented
47
+ }
48
+ parseClose() {
49
+ return false;
50
+ }
51
+ }
52
+ export { CtrlPropXform };