@onparallel/write-excel-file-data-validation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Santi Albo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @onparallel/write-excel-file-data-validation
2
+
3
+ Data validation custom feature for [`write-excel-file`](https://www.npmjs.com/package/write-excel-file).
4
+
5
+ Adds support for Excel's "data validation" rules — dropdown lists, numeric/date/time ranges, text length limits, and custom formulas — without requiring a fork of the base package.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install write-excel-file @onparallel/write-excel-file-data-validation
11
+ ```
12
+
13
+ `write-excel-file` is a peer dependency (`^4.0.0`). The package is published publicly under the `@onparallel` scope — no authentication required to install.
14
+
15
+ ## Usage
16
+
17
+ Register the feature when calling `writeXlsxFile()`. `write-excel-file/node`'s built-in `SheetOptions` does not know about `dataValidation`, so intersect it with the `DataValidationSheetOptions` type exported by this package:
18
+
19
+ ```ts
20
+ import writeXlsxFile, { type SheetOptions } from 'write-excel-file/node'
21
+ import dataValidation, {
22
+ type DataValidationSheetOptions
23
+ } from '@onparallel/write-excel-file-data-validation'
24
+
25
+ const sheetOptions: SheetOptions<any> & DataValidationSheetOptions = {
26
+ sheet: 'Sheet1',
27
+ dataValidation: [
28
+ {
29
+ cellRange: {
30
+ from: { row: 2, column: 1 },
31
+ to: { row: 10, column: 1 }
32
+ },
33
+ validation: {
34
+ type: 'list',
35
+ values: ['open', 'closed', 'pending'],
36
+ error: 'Pick one'
37
+ }
38
+ }
39
+ ]
40
+ }
41
+
42
+ await writeXlsxFile(data, sheetOptions, { features: [dataValidation] }).toFile('out.xlsx')
43
+ ```
44
+
45
+ If you use the intersection in many places, alias it locally: `type MySheetOptions = SheetOptions<any> & DataValidationSheetOptions`.
46
+
47
+ Why not module augmentation? `write-excel-file/node` re-exports its types with `export type { ... }`, which TypeScript does not allow downstream packages to augment.
48
+
49
+ ## Supported validation types
50
+
51
+ | `type` | OOXML `type` | Required fields |
52
+ | -------------- | ------------ | --------------------------------------------------------------------------------- |
53
+ | `'list'` | `list` | `values: string[]` **or** `valuesRange: string` (e.g. `'$E$4:$G$4'`) |
54
+ | `'integer'` | `whole` | `operator`, `value` (and `value2` for `'...'` / `'!...'`) |
55
+ | `'decimal'` | `decimal` | same as `integer` |
56
+ | `'date'` | `date` | `operator`, `value` (and `value2` for between operators); values: `Date`/`number` |
57
+ | `'time'` | `time` | same as `date`; `Date` values are reduced to fractional time-of-day |
58
+ | `'textLength'` | `textLength` | same as `integer` |
59
+ | `'custom'` | `custom` | `formula: string` |
60
+ | `'any'` | (no `type`) | Only metadata (used to attach a tooltip/error message without restricting input) |
61
+
62
+ Operators (mirror the symbols used by `conditionalFormatting`):
63
+
64
+ | Symbol | OOXML |
65
+ | ------ | -------------------- |
66
+ | `<` | `lessThan` |
67
+ | `>` | `greaterThan` |
68
+ | `<=` | `lessThanOrEqual` |
69
+ | `>=` | `greaterThanOrEqual` |
70
+ | `=` | `equal` |
71
+ | `!=` | `notEqual` |
72
+ | `...` | `between` |
73
+ | `!...` | `notBetween` |
74
+
75
+ ## Common options (any type)
76
+
77
+ | Field | Default | OOXML attribute |
78
+ | ------------------ | -------- | -------------------------------------------------------------------------------- |
79
+ | `error` | — | `error` |
80
+ | `errorTitle` | — | `errorTitle` |
81
+ | `errorStyle` | `'stop'` | `errorStyle` |
82
+ | `input` | — | `prompt` |
83
+ | `inputTitle` | — | `promptTitle` |
84
+ | `allowBlank` | `true` | `allowBlank` |
85
+ | `showErrorMessage` | `true` | `showErrorMessage` |
86
+ | `showInputMessage` | `true` | `showInputMessage` |
87
+ | `showDropdown` | `true` | `showDropDown` (inverted: OOXML `"1"` means hide) — only meaningful for `'list'` |
88
+
89
+ Maximum lengths enforced (Excel limits): titles ≤ 32 characters, messages ≤ 255 characters, inline list formulas ≤ 255 characters.
90
+
91
+ ## Limitations
92
+
93
+ - **Sheet-level only.** This feature reads `dataValidation` from `sheetOptions`. Per-cell validation (attaching a `validation` property to a cell object) is **not** supported because `write-excel-file`'s feature API does not expose sheet data to the transform hook. If you need per-cell validation, encode it as a sheet-level rule with a single-cell range:
94
+
95
+ ```ts
96
+ { cellRange: { from: { row: 5, column: 3 }, to: { row: 5, column: 3 } }, validation: { ... } }
97
+ ```
98
+
99
+ - **Inline list commas and quotes.** Excel uses `,` as the list separator inside `<formula1>"…"</formula1>` and `"` as the surrounding delimiter — neither has a working escape. Values containing `,` or `"` are rejected; use `valuesRange` to reference a range of cells instead.
100
+
101
+ ## Examples
102
+
103
+ The [`examples/`](./examples) folder contains runnable TypeScript scripts — one per validation type — that emit `.xlsx` files next to themselves. Open the generated files in Excel/Numbers/LibreOffice to confirm each rule works.
104
+
105
+ ```sh
106
+ npm install
107
+ npx tsx examples/list-inline.ts # run a single example
108
+ npm run examples # run them all
109
+ ```
110
+
111
+ See [`examples/README.md`](./examples/README.md) for the full list.
112
+
113
+ ## Development
114
+
115
+ ```sh
116
+ npm install
117
+ npm test # vitest
118
+ npm run typecheck # tsc --noEmit
119
+ npm run build # emit dist/ via tsc
120
+ ```
121
+
122
+ ## License
123
+
124
+ [MIT](./LICENSE)
@@ -0,0 +1,2 @@
1
+ export default function convertDateToExcelSerial(date: Date): number;
2
+ //# sourceMappingURL=convertDateToExcelSerial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convertDateToExcelSerial.d.ts","sourceRoot":"","sources":["../src/convertDateToExcelSerial.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAEnE"}
@@ -0,0 +1,9 @@
1
+ // Excel serial date = count of days since 1900-01-01 (with the 1900-leap-year quirk).
2
+ // 70 * 365 + 19 = days between 1900-01-01 (Excel epoch) and 1970-01-01 (Unix epoch),
3
+ // accounting for 19 leap days in that range.
4
+ const DAYS_BEFORE_UNIX_EPOCH = 70 * 365 + 19;
5
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
6
+ export default function convertDateToExcelSerial(date) {
7
+ return date.getTime() / MS_PER_DAY + DAYS_BEFORE_UNIX_EPOCH;
8
+ }
9
+ //# sourceMappingURL=convertDateToExcelSerial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convertDateToExcelSerial.js","sourceRoot":"","sources":["../src/convertDateToExcelSerial.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,qFAAqF;AACrF,6CAA6C;AAC7C,MAAM,sBAAsB,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,CAAA;AAC5C,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAEtC,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAAC,IAAU;IAC1D,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,UAAU,GAAG,sBAAsB,CAAA;AAC5D,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Feature } from 'write-excel-file/node';
2
+ declare const dataValidation: Feature<any>;
3
+ export default dataValidation;
4
+ //# sourceMappingURL=dataValidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataValidation.d.ts","sourceRoot":"","sources":["../src/dataValidation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAmBpD,QAAA,MAAM,cAAc,EAAE,OAAO,CAAC,GAAG,CA8BhC,CAAA;AAED,eAAe,cAAc,CAAA"}
@@ -0,0 +1,305 @@
1
+ import { insertElementMarkupAccordingToOrderOfSiblings, getOrderOfSiblings, sanitizeAttributeValue, sanitizeTextContent } from 'write-excel-file/utility';
2
+ import getCellCoordinate from './getCellCoordinate.js';
3
+ import convertDateToExcelSerial from './convertDateToExcelSerial.js';
4
+ const MAX_TITLE_LENGTH = 32;
5
+ const MAX_MESSAGE_LENGTH = 255;
6
+ // `Feature<any>` rather than `Feature<unknown>` so the feature is assignable to
7
+ // `Feature<FileContent>` for any `FileContent` (Buffer/Blob/etc) chosen by the caller.
8
+ // This feature only transforms worksheet XML and never reads or writes file content,
9
+ // so the `FileContent` parameter is irrelevant here.
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const dataValidation = {
12
+ files: {
13
+ transform: {
14
+ 'xl/worksheets/sheet{id}.xml': {
15
+ // `<dataValidations/>` must be placed between `<conditionalFormatting/>` and `<hyperlinks/>`
16
+ // inside `<worksheet/>`. We use `transform()` (not `insert()`) so the inserter can place
17
+ // the markup at the canonical sibling position even when other features have already
18
+ // appended siblings (such as `<drawing/>`).
19
+ transform: (xml, sheetOptions) => {
20
+ const { dataValidation: rules } = sheetOptions;
21
+ if (rules && rules.length > 0) {
22
+ const dataValidationsXml = getDataValidationsXml(rules);
23
+ const order = getOrderOfSiblings('xl/worksheets/sheet{id}.xml', 'worksheet');
24
+ if (!order) {
25
+ throw new Error('write-excel-file did not return an order of siblings for `worksheet`');
26
+ }
27
+ return insertElementMarkupAccordingToOrderOfSiblings(xml, dataValidationsXml, order, 'worksheet');
28
+ }
29
+ return xml;
30
+ }
31
+ }
32
+ }
33
+ }
34
+ };
35
+ export default dataValidation;
36
+ function getDataValidationsXml(rules) {
37
+ let xml = `<dataValidations count="${rules.length}">`;
38
+ for (const rule of rules) {
39
+ xml += getDataValidationXml(rule);
40
+ }
41
+ xml += '</dataValidations>';
42
+ return xml;
43
+ }
44
+ function getDataValidationXml(rule) {
45
+ const { cellRange, validation } = rule;
46
+ if (!cellRange) {
47
+ throw new Error('A data validation rule must specify a `cellRange`');
48
+ }
49
+ if (!validation) {
50
+ throw new Error('A data validation rule must specify a `validation`');
51
+ }
52
+ const { from, to } = cellRange;
53
+ if (!from || !to) {
54
+ throw new Error('A data validation `cellRange` must specify both `from` and `to`');
55
+ }
56
+ if (from.row < 1 || from.column < 1 || to.row < 1 || to.column < 1) {
57
+ throw new Error(`Data validation \`cellRange\` row and column indexes must be >= 1 (1-based), got: ${JSON.stringify(cellRange)}`);
58
+ }
59
+ if (to.row < from.row || to.column < from.column) {
60
+ throw new Error(`Data validation \`cellRange\` \`to\` must be greater than or equal to \`from\`, got: ${JSON.stringify(cellRange)}`);
61
+ }
62
+ const sqref = getCellCoordinate(from.row - 1, from.column - 1) +
63
+ ':' +
64
+ getCellCoordinate(to.row - 1, to.column - 1);
65
+ const attributes = [];
66
+ const xlsxType = getXlsxType(validation.type);
67
+ if (xlsxType) {
68
+ attributes.push(`type="${xlsxType}"`);
69
+ }
70
+ if (hasOperator(validation.type)) {
71
+ attributes.push(`operator="${getXlsxOperatorName(validation.operator)}"`);
72
+ }
73
+ const { error, errorTitle, errorStyle, input, inputTitle, allowBlank, showErrorMessage, showInputMessage } = validation;
74
+ // OOXML defaults `allowBlank` to false; we default it to true (matching xlsxwriter).
75
+ attributes.push(`allowBlank="${allowBlank === false ? '0' : '1'}"`);
76
+ // `showDropDown` in OOXML is inverted: setting it to `1` HIDES the dropdown.
77
+ if (validation.type === 'list' && validation.showDropdown === false) {
78
+ attributes.push('showDropDown="1"');
79
+ }
80
+ // OOXML defaults `showErrorMessage`/`showInputMessage` to false; without them Excel does
81
+ // not reject invalid input. Default both to true.
82
+ attributes.push(`showErrorMessage="${showErrorMessage === false ? '0' : '1'}"`);
83
+ attributes.push(`showInputMessage="${showInputMessage === false ? '0' : '1'}"`);
84
+ if (errorStyle !== undefined) {
85
+ validateErrorStyle(errorStyle);
86
+ // OOXML default for `errorStyle` is `stop`. Skip the attribute in that case.
87
+ if (errorStyle !== 'stop') {
88
+ attributes.push(`errorStyle="${errorStyle}"`);
89
+ }
90
+ }
91
+ if (errorTitle !== undefined) {
92
+ validateTitleLength(errorTitle, 'errorTitle');
93
+ attributes.push(`errorTitle="${sanitizeAttributeValue(errorTitle)}"`);
94
+ }
95
+ if (error !== undefined) {
96
+ validateMessageLength(error, 'error');
97
+ attributes.push(`error="${sanitizeAttributeValue(error)}"`);
98
+ }
99
+ if (inputTitle !== undefined) {
100
+ validateTitleLength(inputTitle, 'inputTitle');
101
+ attributes.push(`promptTitle="${sanitizeAttributeValue(inputTitle)}"`);
102
+ }
103
+ if (input !== undefined) {
104
+ validateMessageLength(input, 'input');
105
+ attributes.push(`prompt="${sanitizeAttributeValue(input)}"`);
106
+ }
107
+ attributes.push(`sqref="${sqref}"`);
108
+ const [formula1, formula2] = getFormulas(validation);
109
+ let xml = `<dataValidation ${attributes.join(' ')}>`;
110
+ if (formula1 !== undefined) {
111
+ xml += `<formula1>${sanitizeTextContent(formula1)}</formula1>`;
112
+ }
113
+ if (formula2 !== undefined) {
114
+ xml += `<formula2>${sanitizeTextContent(formula2)}</formula2>`;
115
+ }
116
+ xml += '</dataValidation>';
117
+ return xml;
118
+ }
119
+ function getXlsxType(type) {
120
+ switch (type) {
121
+ case 'list':
122
+ return 'list';
123
+ case 'integer':
124
+ return 'whole';
125
+ case 'decimal':
126
+ return 'decimal';
127
+ case 'date':
128
+ return 'date';
129
+ case 'time':
130
+ return 'time';
131
+ case 'textLength':
132
+ return 'textLength';
133
+ case 'custom':
134
+ return 'custom';
135
+ case 'any':
136
+ return undefined;
137
+ default:
138
+ throw new Error(`Unknown data validation type: ${type}`);
139
+ }
140
+ }
141
+ function hasOperator(type) {
142
+ switch (type) {
143
+ case 'integer':
144
+ case 'decimal':
145
+ case 'date':
146
+ case 'time':
147
+ case 'textLength':
148
+ return true;
149
+ default:
150
+ return false;
151
+ }
152
+ }
153
+ function getXlsxOperatorName(operator) {
154
+ switch (operator) {
155
+ case '<':
156
+ return 'lessThan';
157
+ case '>':
158
+ return 'greaterThan';
159
+ case '<=':
160
+ return 'lessThanOrEqual';
161
+ case '>=':
162
+ return 'greaterThanOrEqual';
163
+ case '=':
164
+ return 'equal';
165
+ case '!=':
166
+ return 'notEqual';
167
+ case '...':
168
+ return 'between';
169
+ case '!...':
170
+ return 'notBetween';
171
+ default:
172
+ throw new Error(`Unknown data validation operator: ${operator}`);
173
+ }
174
+ }
175
+ function isBetweenOperator(operator) {
176
+ return operator === '...' || operator === '!...';
177
+ }
178
+ function getFormulas(validation) {
179
+ switch (validation.type) {
180
+ case 'list': {
181
+ if ('values' in validation) {
182
+ return [getListFormulaFromValues(validation.values)];
183
+ }
184
+ if ('valuesRange' in validation) {
185
+ return [validation.valuesRange];
186
+ }
187
+ throw new Error('A `list` data validation must specify either `values` or `valuesRange`');
188
+ }
189
+ case 'custom': {
190
+ if (validation.formula === undefined) {
191
+ throw new Error('A `custom` data validation must specify a `formula`');
192
+ }
193
+ return [validation.formula];
194
+ }
195
+ case 'any':
196
+ return [];
197
+ case 'integer':
198
+ case 'decimal':
199
+ case 'textLength': {
200
+ if (validation.value === undefined) {
201
+ throw new Error(`A \`${validation.type}\` data validation must specify a \`value\``);
202
+ }
203
+ if (isBetweenOperator(validation.operator)) {
204
+ if (validation.value2 === undefined) {
205
+ throw new Error(`A \`${validation.type}\` data validation with operator \`${validation.operator}\` must specify a \`value2\``);
206
+ }
207
+ return [
208
+ formatNumericValue(validation.value, validation.type),
209
+ formatNumericValue(validation.value2, validation.type)
210
+ ];
211
+ }
212
+ return [formatNumericValue(validation.value, validation.type)];
213
+ }
214
+ case 'date':
215
+ case 'time': {
216
+ if (validation.value === undefined) {
217
+ throw new Error(`A \`${validation.type}\` data validation must specify a \`value\``);
218
+ }
219
+ if (isBetweenOperator(validation.operator)) {
220
+ const value2 = validation.value2;
221
+ if (value2 === undefined) {
222
+ throw new Error(`A \`${validation.type}\` data validation with operator \`${validation.operator}\` must specify a \`value2\``);
223
+ }
224
+ return [
225
+ formatDateOrTimeValue(validation.value, validation.type),
226
+ formatDateOrTimeValue(value2, validation.type)
227
+ ];
228
+ }
229
+ return [formatDateOrTimeValue(validation.value, validation.type)];
230
+ }
231
+ }
232
+ }
233
+ function getListFormulaFromValues(values) {
234
+ if (!Array.isArray(values)) {
235
+ throw new Error(`A \`list\` data validation \`values\` must be an array, got: ${String(values)}`);
236
+ }
237
+ if (values.length === 0) {
238
+ throw new Error('A `list` data validation `values` must contain at least one value');
239
+ }
240
+ for (const value of values) {
241
+ if (typeof value !== 'string') {
242
+ throw new Error(`A \`list\` data validation \`values\` must be strings, got: ${String(value)}`);
243
+ }
244
+ if (value.indexOf(',') !== -1) {
245
+ throw new Error(`A \`list\` data validation \`values\` cannot contain commas (used by Excel as the separator inside the inline list formula). Use \`valuesRange\` to reference a range of cells instead. Got: "${value}"`);
246
+ }
247
+ if (value.indexOf('"') !== -1) {
248
+ throw new Error(`A \`list\` data validation \`values\` cannot contain double-quote characters. Use \`valuesRange\` to reference a range of cells instead. Got: "${value}"`);
249
+ }
250
+ }
251
+ const serialized = values.join(',');
252
+ if (serialized.length + 2 > MAX_MESSAGE_LENGTH) {
253
+ throw new Error(`A \`list\` data validation \`values\` serialized to "${serialized}" exceeds the maximum length of ${MAX_MESSAGE_LENGTH} characters. Use \`valuesRange\` instead to reference a range of cells.`);
254
+ }
255
+ return `"${serialized}"`;
256
+ }
257
+ function formatNumericValue(value, type) {
258
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
259
+ throw new Error(`A \`${type}\` data validation \`value\` must be a finite number, got: ${String(value)}`);
260
+ }
261
+ return String(value);
262
+ }
263
+ function formatDateOrTimeValue(value, type) {
264
+ let serial;
265
+ if (value instanceof Date) {
266
+ serial = convertDateToExcelSerial(value);
267
+ }
268
+ else if (typeof value === 'number') {
269
+ serial = value;
270
+ }
271
+ else {
272
+ throw new Error(`A \`${type}\` data validation \`value\` must be a Date or a number, got: ${String(value)}`);
273
+ }
274
+ if (!Number.isFinite(serial)) {
275
+ throw new Error(`A \`${type}\` data validation \`value\` is not a finite number (possibly an invalid Date), got: ${String(value)}`);
276
+ }
277
+ // For `time`, Excel expects a fractional value in [0, 1) representing the time of day.
278
+ // A `Date` carries both a date and a time, so reduce the serial to its fractional part.
279
+ if (type === 'time') {
280
+ serial = serial - Math.floor(serial);
281
+ }
282
+ return String(serial);
283
+ }
284
+ function validateErrorStyle(errorStyle) {
285
+ if (errorStyle !== 'stop' && errorStyle !== 'warning' && errorStyle !== 'information') {
286
+ throw new Error(`Unknown data validation \`errorStyle\`: ${errorStyle}. Expected \`stop\`, \`warning\` or \`information\``);
287
+ }
288
+ }
289
+ function validateTitleLength(title, fieldName) {
290
+ if (typeof title !== 'string') {
291
+ throw new Error(`Data validation \`${fieldName}\` must be a string`);
292
+ }
293
+ if (title.length > MAX_TITLE_LENGTH) {
294
+ throw new Error(`Data validation \`${fieldName}\` is longer than ${MAX_TITLE_LENGTH} characters: "${title}"`);
295
+ }
296
+ }
297
+ function validateMessageLength(message, fieldName) {
298
+ if (typeof message !== 'string') {
299
+ throw new Error(`Data validation \`${fieldName}\` must be a string`);
300
+ }
301
+ if (message.length > MAX_MESSAGE_LENGTH) {
302
+ throw new Error(`Data validation \`${fieldName}\` is longer than ${MAX_MESSAGE_LENGTH} characters: "${message}"`);
303
+ }
304
+ }
305
+ //# sourceMappingURL=dataValidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataValidation.js","sourceRoot":"","sources":["../src/dataValidation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export default function getCellCoordinate(rowIndex: number, columnIndex: number): string;
2
+ //# sourceMappingURL=getCellCoordinate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getCellCoordinate.d.ts","sourceRoot":"","sources":["../src/getCellCoordinate.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvF"}
@@ -0,0 +1,13 @@
1
+ const LETTERS_COUNT = 26;
2
+ function getColumnLetter(columnIndex) {
3
+ const prefix = Math.floor(columnIndex / LETTERS_COUNT);
4
+ const letter = String.fromCharCode(97 + (columnIndex % LETTERS_COUNT)).toUpperCase();
5
+ if (prefix === 0) {
6
+ return letter;
7
+ }
8
+ return getColumnLetter(prefix - 1) + letter;
9
+ }
10
+ export default function getCellCoordinate(rowIndex, columnIndex) {
11
+ return `${getColumnLetter(columnIndex)}${rowIndex + 1}`;
12
+ }
13
+ //# sourceMappingURL=getCellCoordinate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getCellCoordinate.js","sourceRoot":"","sources":["../src/getCellCoordinate.ts"],"names":[],"mappings":"AAAA,MAAM,aAAa,GAAG,EAAE,CAAA;AAExB,SAAS,eAAe,CAAC,WAAmB;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,aAAa,CAAC,CAAA;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;IACpF,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QAClB,OAAO,MAAM,CAAA;IACd,CAAC;IACD,OAAO,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAA;AAC5C,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,QAAgB,EAAE,WAAmB;IAC9E,OAAO,GAAG,eAAe,CAAC,WAAW,CAAC,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAA;AACxD,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { default } from './dataValidation.js';
2
+ export type { DataValidation, DataValidationRule, DataValidationOperator, DataValidationErrorStyle, DataValidationSheetOptions } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC7C,YAAY,EACX,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from './dataValidation.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,72 @@
1
+ export type DataValidationOperatorOnOneValue = '<' | '<=' | '>' | '>=' | '=' | '!=';
2
+ export type DataValidationOperatorOnTwoValues = '...' | '!...';
3
+ export type DataValidationOperator = DataValidationOperatorOnOneValue | DataValidationOperatorOnTwoValues;
4
+ export type DataValidationErrorStyle = 'stop' | 'warning' | 'information';
5
+ interface DataValidationCommonProperties {
6
+ error?: string;
7
+ errorTitle?: string;
8
+ errorStyle?: DataValidationErrorStyle;
9
+ input?: string;
10
+ inputTitle?: string;
11
+ allowBlank?: boolean;
12
+ showErrorMessage?: boolean;
13
+ showInputMessage?: boolean;
14
+ }
15
+ interface DataValidationListWithValues extends DataValidationCommonProperties {
16
+ type: 'list';
17
+ values: string[];
18
+ showDropdown?: boolean;
19
+ }
20
+ interface DataValidationListWithRange extends DataValidationCommonProperties {
21
+ type: 'list';
22
+ valuesRange: string;
23
+ showDropdown?: boolean;
24
+ }
25
+ interface DataValidationNumericOnOneValue extends DataValidationCommonProperties {
26
+ type: 'integer' | 'decimal' | 'textLength';
27
+ operator: DataValidationOperatorOnOneValue;
28
+ value: number;
29
+ }
30
+ interface DataValidationNumericOnTwoValues extends DataValidationCommonProperties {
31
+ type: 'integer' | 'decimal' | 'textLength';
32
+ operator: DataValidationOperatorOnTwoValues;
33
+ value: number;
34
+ value2: number;
35
+ }
36
+ interface DataValidationDateOnOneValue extends DataValidationCommonProperties {
37
+ type: 'date' | 'time';
38
+ operator: DataValidationOperatorOnOneValue;
39
+ value: Date | number;
40
+ }
41
+ interface DataValidationDateOnTwoValues extends DataValidationCommonProperties {
42
+ type: 'date' | 'time';
43
+ operator: DataValidationOperatorOnTwoValues;
44
+ value: Date | number;
45
+ value2: Date | number;
46
+ }
47
+ interface DataValidationCustom extends DataValidationCommonProperties {
48
+ type: 'custom';
49
+ formula: string;
50
+ }
51
+ interface DataValidationAny extends DataValidationCommonProperties {
52
+ type: 'any';
53
+ }
54
+ export type DataValidation = DataValidationListWithValues | DataValidationListWithRange | DataValidationNumericOnOneValue | DataValidationNumericOnTwoValues | DataValidationDateOnOneValue | DataValidationDateOnTwoValues | DataValidationCustom | DataValidationAny;
55
+ export interface DataValidationRule {
56
+ cellRange: {
57
+ from: {
58
+ row: number;
59
+ column: number;
60
+ };
61
+ to: {
62
+ row: number;
63
+ column: number;
64
+ };
65
+ };
66
+ validation: DataValidation;
67
+ }
68
+ export interface DataValidationSheetOptions {
69
+ dataValidation?: DataValidationRule[];
70
+ }
71
+ export {};
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAAA;AAEnF,MAAM,MAAM,iCAAiC,GAAG,KAAK,GAAG,MAAM,CAAA;AAE9D,MAAM,MAAM,sBAAsB,GAC/B,gCAAgC,GAChC,iCAAiC,CAAA;AAEpC,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,CAAA;AAEzE,UAAU,8BAA8B;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,wBAAwB,CAAA;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,UAAU,4BAA6B,SAAQ,8BAA8B;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,YAAY,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,2BAA4B,SAAQ,8BAA8B;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,+BAAgC,SAAQ,8BAA8B;IAC/E,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAA;IAC1C,QAAQ,EAAE,gCAAgC,CAAA;IAC1C,KAAK,EAAE,MAAM,CAAA;CACb;AAED,UAAU,gCAAiC,SAAQ,8BAA8B;IAChF,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAA;IAC1C,QAAQ,EAAE,iCAAiC,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACd;AAED,UAAU,4BAA6B,SAAQ,8BAA8B;IAC5E,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IACrB,QAAQ,EAAE,gCAAgC,CAAA;IAC1C,KAAK,EAAE,IAAI,GAAG,MAAM,CAAA;CACpB;AAED,UAAU,6BAA8B,SAAQ,8BAA8B;IAC7E,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IACrB,QAAQ,EAAE,iCAAiC,CAAA;IAC3C,KAAK,EAAE,IAAI,GAAG,MAAM,CAAA;IACpB,MAAM,EAAE,IAAI,GAAG,MAAM,CAAA;CACrB;AAED,UAAU,oBAAqB,SAAQ,8BAA8B;IACpE,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CACf;AAED,UAAU,iBAAkB,SAAQ,8BAA8B;IACjE,IAAI,EAAE,KAAK,CAAA;CACX;AAED,MAAM,MAAM,cAAc,GACvB,4BAA4B,GAC5B,2BAA2B,GAC3B,+BAA+B,GAC/B,gCAAgC,GAChC,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,iBAAiB,CAAA;AAEpB,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE;QACV,IAAI,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;QACrC,EAAE,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KACnC,CAAA;IACD,UAAU,EAAE,cAAc,CAAA;CAC1B;AAED,MAAM,WAAW,0BAA0B;IAC1C,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAA;CACrC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@onparallel/write-excel-file-data-validation",
3
+ "version": "0.1.0",
4
+ "description": "Data validation custom feature for write-excel-file",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src/**/*.ts",
18
+ "!src/**/*.test.ts",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/onparallel/write-excel-file-data-validation.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/onparallel/write-excel-file-data-validation/issues"
28
+ },
29
+ "homepage": "https://github.com/onparallel/write-excel-file-data-validation#readme",
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "typecheck": "tsc --noEmit",
39
+ "format": "prettier --write .",
40
+ "format:check": "prettier --check .",
41
+ "examples": "for f in examples/*.ts; do tsx \"$f\"; done",
42
+ "prepublishOnly": "npm run format:check && npm run typecheck && npm test && npm run build"
43
+ },
44
+ "keywords": [
45
+ "xlsx",
46
+ "excel",
47
+ "data-validation",
48
+ "dropdown",
49
+ "write-excel-file"
50
+ ],
51
+ "author": "Santi Albo <santi@onparallel.com>",
52
+ "license": "MIT",
53
+ "peerDependencies": {
54
+ "write-excel-file": "^4.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^20.0.0",
58
+ "prettier": "^3.3.0",
59
+ "tsx": "^4.7.0",
60
+ "typescript": "^5.4.0",
61
+ "vitest": "^1.6.0",
62
+ "write-excel-file": "^4.0.7"
63
+ }
64
+ }
@@ -0,0 +1,9 @@
1
+ // Excel serial date = count of days since 1900-01-01 (with the 1900-leap-year quirk).
2
+ // 70 * 365 + 19 = days between 1900-01-01 (Excel epoch) and 1970-01-01 (Unix epoch),
3
+ // accounting for 19 leap days in that range.
4
+ const DAYS_BEFORE_UNIX_EPOCH = 70 * 365 + 19
5
+ const MS_PER_DAY = 24 * 60 * 60 * 1000
6
+
7
+ export default function convertDateToExcelSerial(date: Date): number {
8
+ return date.getTime() / MS_PER_DAY + DAYS_BEFORE_UNIX_EPOCH
9
+ }
@@ -0,0 +1,409 @@
1
+ import {
2
+ insertElementMarkupAccordingToOrderOfSiblings,
3
+ getOrderOfSiblings,
4
+ sanitizeAttributeValue,
5
+ sanitizeTextContent
6
+ } from 'write-excel-file/utility'
7
+ import type { Feature } from 'write-excel-file/node'
8
+
9
+ import getCellCoordinate from './getCellCoordinate.js'
10
+ import convertDateToExcelSerial from './convertDateToExcelSerial.js'
11
+ import type {
12
+ DataValidation,
13
+ DataValidationOperator,
14
+ DataValidationRule,
15
+ DataValidationSheetOptions
16
+ } from './types.js'
17
+
18
+ const MAX_TITLE_LENGTH = 32
19
+ const MAX_MESSAGE_LENGTH = 255
20
+
21
+ // `Feature<any>` rather than `Feature<unknown>` so the feature is assignable to
22
+ // `Feature<FileContent>` for any `FileContent` (Buffer/Blob/etc) chosen by the caller.
23
+ // This feature only transforms worksheet XML and never reads or writes file content,
24
+ // so the `FileContent` parameter is irrelevant here.
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const dataValidation: Feature<any> = {
27
+ files: {
28
+ transform: {
29
+ 'xl/worksheets/sheet{id}.xml': {
30
+ // `<dataValidations/>` must be placed between `<conditionalFormatting/>` and `<hyperlinks/>`
31
+ // inside `<worksheet/>`. We use `transform()` (not `insert()`) so the inserter can place
32
+ // the markup at the canonical sibling position even when other features have already
33
+ // appended siblings (such as `<drawing/>`).
34
+ transform: (xml, sheetOptions) => {
35
+ const { dataValidation: rules } = sheetOptions as DataValidationSheetOptions
36
+ if (rules && rules.length > 0) {
37
+ const dataValidationsXml = getDataValidationsXml(rules)
38
+ const order = getOrderOfSiblings('xl/worksheets/sheet{id}.xml', 'worksheet')
39
+ if (!order) {
40
+ throw new Error(
41
+ 'write-excel-file did not return an order of siblings for `worksheet`'
42
+ )
43
+ }
44
+ return insertElementMarkupAccordingToOrderOfSiblings(
45
+ xml,
46
+ dataValidationsXml,
47
+ order,
48
+ 'worksheet'
49
+ )
50
+ }
51
+ return xml
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ export default dataValidation
59
+
60
+ function getDataValidationsXml(rules: DataValidationRule[]): string {
61
+ let xml = `<dataValidations count="${rules.length}">`
62
+ for (const rule of rules) {
63
+ xml += getDataValidationXml(rule)
64
+ }
65
+ xml += '</dataValidations>'
66
+ return xml
67
+ }
68
+
69
+ function getDataValidationXml(rule: DataValidationRule): string {
70
+ const { cellRange, validation } = rule
71
+
72
+ if (!cellRange) {
73
+ throw new Error('A data validation rule must specify a `cellRange`')
74
+ }
75
+
76
+ if (!validation) {
77
+ throw new Error('A data validation rule must specify a `validation`')
78
+ }
79
+
80
+ const { from, to } = cellRange
81
+
82
+ if (!from || !to) {
83
+ throw new Error('A data validation `cellRange` must specify both `from` and `to`')
84
+ }
85
+
86
+ if (from.row < 1 || from.column < 1 || to.row < 1 || to.column < 1) {
87
+ throw new Error(
88
+ `Data validation \`cellRange\` row and column indexes must be >= 1 (1-based), got: ${JSON.stringify(cellRange)}`
89
+ )
90
+ }
91
+
92
+ if (to.row < from.row || to.column < from.column) {
93
+ throw new Error(
94
+ `Data validation \`cellRange\` \`to\` must be greater than or equal to \`from\`, got: ${JSON.stringify(cellRange)}`
95
+ )
96
+ }
97
+
98
+ const sqref =
99
+ getCellCoordinate(from.row - 1, from.column - 1) +
100
+ ':' +
101
+ getCellCoordinate(to.row - 1, to.column - 1)
102
+
103
+ const attributes: string[] = []
104
+
105
+ const xlsxType = getXlsxType(validation.type)
106
+ if (xlsxType) {
107
+ attributes.push(`type="${xlsxType}"`)
108
+ }
109
+
110
+ if (hasOperator(validation.type)) {
111
+ attributes.push(
112
+ `operator="${getXlsxOperatorName((validation as { operator: DataValidationOperator }).operator)}"`
113
+ )
114
+ }
115
+
116
+ const {
117
+ error,
118
+ errorTitle,
119
+ errorStyle,
120
+ input,
121
+ inputTitle,
122
+ allowBlank,
123
+ showErrorMessage,
124
+ showInputMessage
125
+ } = validation
126
+
127
+ // OOXML defaults `allowBlank` to false; we default it to true (matching xlsxwriter).
128
+ attributes.push(`allowBlank="${allowBlank === false ? '0' : '1'}"`)
129
+
130
+ // `showDropDown` in OOXML is inverted: setting it to `1` HIDES the dropdown.
131
+ if (validation.type === 'list' && validation.showDropdown === false) {
132
+ attributes.push('showDropDown="1"')
133
+ }
134
+
135
+ // OOXML defaults `showErrorMessage`/`showInputMessage` to false; without them Excel does
136
+ // not reject invalid input. Default both to true.
137
+ attributes.push(`showErrorMessage="${showErrorMessage === false ? '0' : '1'}"`)
138
+ attributes.push(`showInputMessage="${showInputMessage === false ? '0' : '1'}"`)
139
+
140
+ if (errorStyle !== undefined) {
141
+ validateErrorStyle(errorStyle)
142
+ // OOXML default for `errorStyle` is `stop`. Skip the attribute in that case.
143
+ if (errorStyle !== 'stop') {
144
+ attributes.push(`errorStyle="${errorStyle}"`)
145
+ }
146
+ }
147
+
148
+ if (errorTitle !== undefined) {
149
+ validateTitleLength(errorTitle, 'errorTitle')
150
+ attributes.push(`errorTitle="${sanitizeAttributeValue(errorTitle)}"`)
151
+ }
152
+
153
+ if (error !== undefined) {
154
+ validateMessageLength(error, 'error')
155
+ attributes.push(`error="${sanitizeAttributeValue(error)}"`)
156
+ }
157
+
158
+ if (inputTitle !== undefined) {
159
+ validateTitleLength(inputTitle, 'inputTitle')
160
+ attributes.push(`promptTitle="${sanitizeAttributeValue(inputTitle)}"`)
161
+ }
162
+
163
+ if (input !== undefined) {
164
+ validateMessageLength(input, 'input')
165
+ attributes.push(`prompt="${sanitizeAttributeValue(input)}"`)
166
+ }
167
+
168
+ attributes.push(`sqref="${sqref}"`)
169
+
170
+ const [formula1, formula2] = getFormulas(validation)
171
+
172
+ let xml = `<dataValidation ${attributes.join(' ')}>`
173
+ if (formula1 !== undefined) {
174
+ xml += `<formula1>${sanitizeTextContent(formula1)}</formula1>`
175
+ }
176
+ if (formula2 !== undefined) {
177
+ xml += `<formula2>${sanitizeTextContent(formula2)}</formula2>`
178
+ }
179
+ xml += '</dataValidation>'
180
+
181
+ return xml
182
+ }
183
+
184
+ function getXlsxType(type: DataValidation['type']): string | undefined {
185
+ switch (type) {
186
+ case 'list':
187
+ return 'list'
188
+ case 'integer':
189
+ return 'whole'
190
+ case 'decimal':
191
+ return 'decimal'
192
+ case 'date':
193
+ return 'date'
194
+ case 'time':
195
+ return 'time'
196
+ case 'textLength':
197
+ return 'textLength'
198
+ case 'custom':
199
+ return 'custom'
200
+ case 'any':
201
+ return undefined
202
+ default:
203
+ throw new Error(`Unknown data validation type: ${type as string}`)
204
+ }
205
+ }
206
+
207
+ function hasOperator(type: DataValidation['type']): boolean {
208
+ switch (type) {
209
+ case 'integer':
210
+ case 'decimal':
211
+ case 'date':
212
+ case 'time':
213
+ case 'textLength':
214
+ return true
215
+ default:
216
+ return false
217
+ }
218
+ }
219
+
220
+ function getXlsxOperatorName(operator: DataValidationOperator): string {
221
+ switch (operator) {
222
+ case '<':
223
+ return 'lessThan'
224
+ case '>':
225
+ return 'greaterThan'
226
+ case '<=':
227
+ return 'lessThanOrEqual'
228
+ case '>=':
229
+ return 'greaterThanOrEqual'
230
+ case '=':
231
+ return 'equal'
232
+ case '!=':
233
+ return 'notEqual'
234
+ case '...':
235
+ return 'between'
236
+ case '!...':
237
+ return 'notBetween'
238
+ default:
239
+ throw new Error(`Unknown data validation operator: ${operator as string}`)
240
+ }
241
+ }
242
+
243
+ function isBetweenOperator(operator: DataValidationOperator): boolean {
244
+ return operator === '...' || operator === '!...'
245
+ }
246
+
247
+ type Formulas = [string | undefined, string | undefined] | [string] | []
248
+
249
+ function getFormulas(validation: DataValidation): Formulas {
250
+ switch (validation.type) {
251
+ case 'list': {
252
+ if ('values' in validation) {
253
+ return [getListFormulaFromValues(validation.values)]
254
+ }
255
+ if ('valuesRange' in validation) {
256
+ return [validation.valuesRange]
257
+ }
258
+ throw new Error('A `list` data validation must specify either `values` or `valuesRange`')
259
+ }
260
+
261
+ case 'custom': {
262
+ if (validation.formula === undefined) {
263
+ throw new Error('A `custom` data validation must specify a `formula`')
264
+ }
265
+ return [validation.formula]
266
+ }
267
+
268
+ case 'any':
269
+ return []
270
+
271
+ case 'integer':
272
+ case 'decimal':
273
+ case 'textLength': {
274
+ if (validation.value === undefined) {
275
+ throw new Error(`A \`${validation.type}\` data validation must specify a \`value\``)
276
+ }
277
+ if (isBetweenOperator(validation.operator)) {
278
+ if ((validation as { value2?: number }).value2 === undefined) {
279
+ throw new Error(
280
+ `A \`${validation.type}\` data validation with operator \`${validation.operator}\` must specify a \`value2\``
281
+ )
282
+ }
283
+ return [
284
+ formatNumericValue(validation.value, validation.type),
285
+ formatNumericValue((validation as { value2: number }).value2, validation.type)
286
+ ]
287
+ }
288
+ return [formatNumericValue(validation.value, validation.type)]
289
+ }
290
+
291
+ case 'date':
292
+ case 'time': {
293
+ if (validation.value === undefined) {
294
+ throw new Error(`A \`${validation.type}\` data validation must specify a \`value\``)
295
+ }
296
+ if (isBetweenOperator(validation.operator)) {
297
+ const value2 = (validation as { value2?: Date | number }).value2
298
+ if (value2 === undefined) {
299
+ throw new Error(
300
+ `A \`${validation.type}\` data validation with operator \`${validation.operator}\` must specify a \`value2\``
301
+ )
302
+ }
303
+ return [
304
+ formatDateOrTimeValue(validation.value, validation.type),
305
+ formatDateOrTimeValue(value2, validation.type)
306
+ ]
307
+ }
308
+ return [formatDateOrTimeValue(validation.value, validation.type)]
309
+ }
310
+ }
311
+ }
312
+
313
+ function getListFormulaFromValues(values: string[]): string {
314
+ if (!Array.isArray(values)) {
315
+ throw new Error(
316
+ `A \`list\` data validation \`values\` must be an array, got: ${String(values)}`
317
+ )
318
+ }
319
+ if (values.length === 0) {
320
+ throw new Error('A `list` data validation `values` must contain at least one value')
321
+ }
322
+ for (const value of values) {
323
+ if (typeof value !== 'string') {
324
+ throw new Error(
325
+ `A \`list\` data validation \`values\` must be strings, got: ${String(value)}`
326
+ )
327
+ }
328
+ if (value.indexOf(',') !== -1) {
329
+ throw new Error(
330
+ `A \`list\` data validation \`values\` cannot contain commas (used by Excel as the separator inside the inline list formula). Use \`valuesRange\` to reference a range of cells instead. Got: "${value}"`
331
+ )
332
+ }
333
+ if (value.indexOf('"') !== -1) {
334
+ throw new Error(
335
+ `A \`list\` data validation \`values\` cannot contain double-quote characters. Use \`valuesRange\` to reference a range of cells instead. Got: "${value}"`
336
+ )
337
+ }
338
+ }
339
+ const serialized = values.join(',')
340
+ if (serialized.length + 2 > MAX_MESSAGE_LENGTH) {
341
+ throw new Error(
342
+ `A \`list\` data validation \`values\` serialized to "${serialized}" exceeds the maximum length of ${MAX_MESSAGE_LENGTH} characters. Use \`valuesRange\` instead to reference a range of cells.`
343
+ )
344
+ }
345
+ return `"${serialized}"`
346
+ }
347
+
348
+ function formatNumericValue(value: number, type: string): string {
349
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
350
+ throw new Error(
351
+ `A \`${type}\` data validation \`value\` must be a finite number, got: ${String(value)}`
352
+ )
353
+ }
354
+ return String(value)
355
+ }
356
+
357
+ function formatDateOrTimeValue(value: Date | number, type: 'date' | 'time'): string {
358
+ let serial: number
359
+ if (value instanceof Date) {
360
+ serial = convertDateToExcelSerial(value)
361
+ } else if (typeof value === 'number') {
362
+ serial = value
363
+ } else {
364
+ throw new Error(
365
+ `A \`${type}\` data validation \`value\` must be a Date or a number, got: ${String(value)}`
366
+ )
367
+ }
368
+ if (!Number.isFinite(serial)) {
369
+ throw new Error(
370
+ `A \`${type}\` data validation \`value\` is not a finite number (possibly an invalid Date), got: ${String(value)}`
371
+ )
372
+ }
373
+ // For `time`, Excel expects a fractional value in [0, 1) representing the time of day.
374
+ // A `Date` carries both a date and a time, so reduce the serial to its fractional part.
375
+ if (type === 'time') {
376
+ serial = serial - Math.floor(serial)
377
+ }
378
+ return String(serial)
379
+ }
380
+
381
+ function validateErrorStyle(errorStyle: string): void {
382
+ if (errorStyle !== 'stop' && errorStyle !== 'warning' && errorStyle !== 'information') {
383
+ throw new Error(
384
+ `Unknown data validation \`errorStyle\`: ${errorStyle}. Expected \`stop\`, \`warning\` or \`information\``
385
+ )
386
+ }
387
+ }
388
+
389
+ function validateTitleLength(title: string, fieldName: string): void {
390
+ if (typeof title !== 'string') {
391
+ throw new Error(`Data validation \`${fieldName}\` must be a string`)
392
+ }
393
+ if (title.length > MAX_TITLE_LENGTH) {
394
+ throw new Error(
395
+ `Data validation \`${fieldName}\` is longer than ${MAX_TITLE_LENGTH} characters: "${title}"`
396
+ )
397
+ }
398
+ }
399
+
400
+ function validateMessageLength(message: string, fieldName: string): void {
401
+ if (typeof message !== 'string') {
402
+ throw new Error(`Data validation \`${fieldName}\` must be a string`)
403
+ }
404
+ if (message.length > MAX_MESSAGE_LENGTH) {
405
+ throw new Error(
406
+ `Data validation \`${fieldName}\` is longer than ${MAX_MESSAGE_LENGTH} characters: "${message}"`
407
+ )
408
+ }
409
+ }
@@ -0,0 +1,14 @@
1
+ const LETTERS_COUNT = 26
2
+
3
+ function getColumnLetter(columnIndex: number): string {
4
+ const prefix = Math.floor(columnIndex / LETTERS_COUNT)
5
+ const letter = String.fromCharCode(97 + (columnIndex % LETTERS_COUNT)).toUpperCase()
6
+ if (prefix === 0) {
7
+ return letter
8
+ }
9
+ return getColumnLetter(prefix - 1) + letter
10
+ }
11
+
12
+ export default function getCellCoordinate(rowIndex: number, columnIndex: number): string {
13
+ return `${getColumnLetter(columnIndex)}${rowIndex + 1}`
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export { default } from './dataValidation.js'
2
+ export type {
3
+ DataValidation,
4
+ DataValidationRule,
5
+ DataValidationOperator,
6
+ DataValidationErrorStyle,
7
+ DataValidationSheetOptions
8
+ } from './types.js'
package/src/types.ts ADDED
@@ -0,0 +1,89 @@
1
+ export type DataValidationOperatorOnOneValue = '<' | '<=' | '>' | '>=' | '=' | '!='
2
+
3
+ export type DataValidationOperatorOnTwoValues = '...' | '!...'
4
+
5
+ export type DataValidationOperator =
6
+ | DataValidationOperatorOnOneValue
7
+ | DataValidationOperatorOnTwoValues
8
+
9
+ export type DataValidationErrorStyle = 'stop' | 'warning' | 'information'
10
+
11
+ interface DataValidationCommonProperties {
12
+ error?: string
13
+ errorTitle?: string
14
+ errorStyle?: DataValidationErrorStyle
15
+ input?: string
16
+ inputTitle?: string
17
+ allowBlank?: boolean
18
+ showErrorMessage?: boolean
19
+ showInputMessage?: boolean
20
+ }
21
+
22
+ interface DataValidationListWithValues extends DataValidationCommonProperties {
23
+ type: 'list'
24
+ values: string[]
25
+ showDropdown?: boolean
26
+ }
27
+
28
+ interface DataValidationListWithRange extends DataValidationCommonProperties {
29
+ type: 'list'
30
+ valuesRange: string
31
+ showDropdown?: boolean
32
+ }
33
+
34
+ interface DataValidationNumericOnOneValue extends DataValidationCommonProperties {
35
+ type: 'integer' | 'decimal' | 'textLength'
36
+ operator: DataValidationOperatorOnOneValue
37
+ value: number
38
+ }
39
+
40
+ interface DataValidationNumericOnTwoValues extends DataValidationCommonProperties {
41
+ type: 'integer' | 'decimal' | 'textLength'
42
+ operator: DataValidationOperatorOnTwoValues
43
+ value: number
44
+ value2: number
45
+ }
46
+
47
+ interface DataValidationDateOnOneValue extends DataValidationCommonProperties {
48
+ type: 'date' | 'time'
49
+ operator: DataValidationOperatorOnOneValue
50
+ value: Date | number
51
+ }
52
+
53
+ interface DataValidationDateOnTwoValues extends DataValidationCommonProperties {
54
+ type: 'date' | 'time'
55
+ operator: DataValidationOperatorOnTwoValues
56
+ value: Date | number
57
+ value2: Date | number
58
+ }
59
+
60
+ interface DataValidationCustom extends DataValidationCommonProperties {
61
+ type: 'custom'
62
+ formula: string
63
+ }
64
+
65
+ interface DataValidationAny extends DataValidationCommonProperties {
66
+ type: 'any'
67
+ }
68
+
69
+ export type DataValidation =
70
+ | DataValidationListWithValues
71
+ | DataValidationListWithRange
72
+ | DataValidationNumericOnOneValue
73
+ | DataValidationNumericOnTwoValues
74
+ | DataValidationDateOnOneValue
75
+ | DataValidationDateOnTwoValues
76
+ | DataValidationCustom
77
+ | DataValidationAny
78
+
79
+ export interface DataValidationRule {
80
+ cellRange: {
81
+ from: { row: number; column: number }
82
+ to: { row: number; column: number }
83
+ }
84
+ validation: DataValidation
85
+ }
86
+
87
+ export interface DataValidationSheetOptions {
88
+ dataValidation?: DataValidationRule[]
89
+ }