exceljs-lts 4.5.3 → 4.5.5
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/README.md +43 -11
- package/README_zh.md +29 -10
- package/dist/es5/doc/cell.js +37 -12
- package/dist/es5/doc/cell.js.map +1 -1
- package/dist/es5/doc/data-validations.js +94 -1
- package/dist/es5/doc/data-validations.js.map +1 -1
- package/dist/es5/doc/image.js +28 -0
- package/dist/es5/doc/image.js.map +1 -1
- package/dist/es5/doc/worksheet.js +8 -0
- package/dist/es5/doc/worksheet.js.map +1 -1
- package/dist/es5/stream/xlsx/workbook-reader.js +2 -1
- package/dist/es5/stream/xlsx/workbook-reader.js.map +1 -1
- package/dist/es5/stream/xlsx/workbook-writer.js +21 -1
- package/dist/es5/stream/xlsx/workbook-writer.js.map +1 -1
- package/dist/es5/stream/xlsx/worksheet-reader.js +1 -1
- package/dist/es5/stream/xlsx/worksheet-reader.js.map +1 -1
- package/dist/es5/stream/xlsx/worksheet-writer.js +89 -0
- package/dist/es5/stream/xlsx/worksheet-writer.js.map +1 -1
- package/dist/es5/xlsx/rel-type.js +1 -0
- package/dist/es5/xlsx/rel-type.js.map +1 -1
- package/dist/es5/xlsx/xform/sheet/auto-filter-xform.js +53 -5
- package/dist/es5/xlsx/xform/sheet/auto-filter-xform.js.map +1 -1
- package/dist/es5/xlsx/xform/sheet/data-validations-xform.js +14 -3
- package/dist/es5/xlsx/xform/sheet/data-validations-xform.js.map +1 -1
- package/dist/exceljs.bare.js +237 -23
- package/dist/exceljs.bare.js.map +8 -8
- package/dist/exceljs.bare.min.js +6 -6
- package/dist/exceljs.bare.min.js.map +1 -1
- package/dist/exceljs.js +237 -23
- package/dist/exceljs.js.map +8 -8
- package/dist/exceljs.min.js +5 -5
- package/dist/exceljs.min.js.map +1 -1
- package/index.d.ts +40 -3
- package/lib/doc/cell.js +37 -12
- package/lib/doc/data-validations.js +83 -1
- package/lib/doc/image.js +32 -0
- package/lib/doc/worksheet.js +9 -0
- package/lib/stream/xlsx/workbook-reader.js +6 -1
- package/lib/stream/xlsx/workbook-writer.js +19 -0
- package/lib/stream/xlsx/worksheet-reader.js +1 -1
- package/lib/stream/xlsx/worksheet-writer.js +73 -0
- package/lib/xlsx/rel-type.js +1 -0
- package/lib/xlsx/xform/sheet/auto-filter-xform.js +42 -3
- package/lib/xlsx/xform/sheet/data-validations-xform.js +14 -3
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Node's Buffer type when @types/node is loaded, otherwise never.
|
|
3
|
+
* Read through `typeof globalThis` so these typings do not require @types/node.
|
|
4
|
+
* Newer @types/node declare `var Buffer`; older ones (class Buffer) expose it as `global.Buffer`.
|
|
5
|
+
*/
|
|
6
|
+
type NodeBufferType = typeof globalThis extends { Buffer: { alloc(size: number): infer B } }
|
|
7
|
+
? B
|
|
8
|
+
: typeof globalThis extends { global: { Buffer: { alloc(size: number): infer B } } }
|
|
9
|
+
? B
|
|
10
|
+
: never;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Binary data returned by writeBuffer(): a Node Buffer when @types/node is loaded
|
|
14
|
+
* (which is what Node returns at runtime), otherwise ArrayBuffer.
|
|
15
|
+
*/
|
|
16
|
+
declare type Buffer = [NodeBufferType] extends [never] ? ArrayBuffer : NodeBufferType;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Binary data accepted by xlsx.load().
|
|
20
|
+
*/
|
|
21
|
+
type BufferInput = Buffer | ArrayBuffer | Uint8Array;
|
|
2
22
|
|
|
3
23
|
export declare enum RelationshipType {
|
|
4
24
|
None = 0,
|
|
@@ -830,10 +850,14 @@ export interface PageSetup {
|
|
|
830
850
|
export interface HeaderFooter {
|
|
831
851
|
/**
|
|
832
852
|
* Set the value of differentFirst as true, which indicates that headers/footers for first page are different from the other pages, `false` by default
|
|
853
|
+
*
|
|
854
|
+
* Must be `true` for `firstHeader`/`firstFooter` to take effect.
|
|
833
855
|
*/
|
|
834
856
|
differentFirst: boolean,
|
|
835
857
|
/**
|
|
836
858
|
* Set the value of differentOddEven as true, which indicates that headers/footers for odd and even pages are different, `false` by default
|
|
859
|
+
*
|
|
860
|
+
* Must be `true` for `evenHeader`/`evenFooter` to take effect.
|
|
837
861
|
*/
|
|
838
862
|
differentOddEven: boolean,
|
|
839
863
|
/**
|
|
@@ -846,18 +870,26 @@ export interface HeaderFooter {
|
|
|
846
870
|
oddFooter: string,
|
|
847
871
|
/**
|
|
848
872
|
* Set header string for even pages, could format the string and `null` by default
|
|
873
|
+
*
|
|
874
|
+
* Only used when `differentOddEven` is `true`; otherwise Excel ignores it.
|
|
849
875
|
*/
|
|
850
876
|
evenHeader: string,
|
|
851
877
|
/**
|
|
852
878
|
* Set footer string for even pages, could format the string and `null` by default
|
|
879
|
+
*
|
|
880
|
+
* Only used when `differentOddEven` is `true`; otherwise Excel ignores it.
|
|
853
881
|
*/
|
|
854
882
|
evenFooter: string,
|
|
855
883
|
/**
|
|
856
884
|
* Set header string for the first page, could format the string and `null` by default
|
|
885
|
+
*
|
|
886
|
+
* Only used when `differentFirst` is `true`; otherwise Excel ignores it.
|
|
857
887
|
*/
|
|
858
888
|
firstHeader: string,
|
|
859
889
|
/**
|
|
860
890
|
* Set footer string for the first page, could format the string and `null` by default
|
|
891
|
+
*
|
|
892
|
+
* Only used when `differentFirst` is `true`; otherwise Excel ignores it.
|
|
861
893
|
*/
|
|
862
894
|
firstFooter: string
|
|
863
895
|
}
|
|
@@ -865,6 +897,10 @@ export interface HeaderFooter {
|
|
|
865
897
|
export type AutoFilter = string | {
|
|
866
898
|
from: string | { row: number; column: number };
|
|
867
899
|
to: string | { row: number; column: number };
|
|
900
|
+
/**
|
|
901
|
+
* Columns (0 = leftmost column of the range) whose filter button is hidden
|
|
902
|
+
*/
|
|
903
|
+
exclude?: number[];
|
|
868
904
|
};
|
|
869
905
|
|
|
870
906
|
export interface WorksheetProtection {
|
|
@@ -889,7 +925,7 @@ export interface Image {
|
|
|
889
925
|
extension: 'jpeg' | 'png' | 'gif';
|
|
890
926
|
base64?: string;
|
|
891
927
|
filename?: string;
|
|
892
|
-
buffer?: Buffer;
|
|
928
|
+
buffer?: Buffer | ArrayBuffer;
|
|
893
929
|
}
|
|
894
930
|
export interface IAnchor {
|
|
895
931
|
col: number;
|
|
@@ -1494,7 +1530,7 @@ export interface Xlsx {
|
|
|
1494
1530
|
* load from an array buffer
|
|
1495
1531
|
* @param buffer
|
|
1496
1532
|
*/
|
|
1497
|
-
load(buffer:
|
|
1533
|
+
load(buffer: BufferInput, options?: Partial<XlsxReadOptions>): Promise<Workbook>;
|
|
1498
1534
|
|
|
1499
1535
|
/**
|
|
1500
1536
|
* write to a buffer
|
|
@@ -1989,6 +2025,7 @@ export namespace stream {
|
|
|
1989
2025
|
addThemes(): Promise<void>;
|
|
1990
2026
|
addOfficeRels(): Promise<void>;
|
|
1991
2027
|
addContentTypes(): Promise<void>;
|
|
2028
|
+
addDrawings(): Promise<void>;
|
|
1992
2029
|
addApp(): Promise<void>;
|
|
1993
2030
|
addCore(): Promise<void>;
|
|
1994
2031
|
addSharedStrings(): Promise<void>;
|
package/lib/doc/cell.js
CHANGED
|
@@ -4,6 +4,7 @@ const _ = require('../utils/under-dash');
|
|
|
4
4
|
const Enums = require('./enums');
|
|
5
5
|
const {slideFormula} = require('../utils/shared-formula');
|
|
6
6
|
const Note = require('./note');
|
|
7
|
+
const {copyStyle} = require('../utils/copy-style');
|
|
7
8
|
// Cell requirements
|
|
8
9
|
// Operate inside a worksheet
|
|
9
10
|
// Store and retrieve a value with a range of types: text, number, date, hyperlink, reference, formula, etc.
|
|
@@ -39,7 +40,8 @@ class Cell {
|
|
|
39
40
|
|
|
40
41
|
// help GC by removing cyclic (and other) references
|
|
41
42
|
destroy() {
|
|
42
|
-
delete this.
|
|
43
|
+
delete this._style;
|
|
44
|
+
delete this._styleShared;
|
|
43
45
|
delete this._value;
|
|
44
46
|
delete this._row;
|
|
45
47
|
delete this._column;
|
|
@@ -48,8 +50,31 @@ class Cell {
|
|
|
48
50
|
|
|
49
51
|
// =========================================================================
|
|
50
52
|
// Styles stuff
|
|
53
|
+
|
|
54
|
+
// Cells loaded from a file share one style object per xf. The object is only
|
|
55
|
+
// copied when the style is handed out (cell.style), so code that mutates it
|
|
56
|
+
// in place (cell.style.fill = ...) changes this cell alone.
|
|
57
|
+
get style() {
|
|
58
|
+
if (this._styleShared) {
|
|
59
|
+
this._style = copyStyle(this._style);
|
|
60
|
+
this._styleShared = false;
|
|
61
|
+
}
|
|
62
|
+
return this._style;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
set style(value) {
|
|
66
|
+
this._style = value;
|
|
67
|
+
this._styleShared = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// use a style object that other cells may share (e.g. a cached xf style)
|
|
71
|
+
_setSharedStyle(style) {
|
|
72
|
+
this._style = style;
|
|
73
|
+
this._styleShared = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
get numFmt() {
|
|
52
|
-
return this.
|
|
77
|
+
return this._style.numFmt;
|
|
53
78
|
}
|
|
54
79
|
|
|
55
80
|
set numFmt(value) {
|
|
@@ -57,7 +82,7 @@ class Cell {
|
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
get font() {
|
|
60
|
-
return this.
|
|
85
|
+
return this._style.font;
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
set font(value) {
|
|
@@ -65,7 +90,7 @@ class Cell {
|
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
get alignment() {
|
|
68
|
-
return this.
|
|
93
|
+
return this._style.alignment;
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
set alignment(value) {
|
|
@@ -73,7 +98,7 @@ class Cell {
|
|
|
73
98
|
}
|
|
74
99
|
|
|
75
100
|
get border() {
|
|
76
|
-
return this.
|
|
101
|
+
return this._style.border;
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
set border(value) {
|
|
@@ -81,7 +106,7 @@ class Cell {
|
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
get fill() {
|
|
84
|
-
return this.
|
|
109
|
+
return this._style.fill;
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
set fill(value) {
|
|
@@ -89,7 +114,7 @@ class Cell {
|
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
get protection() {
|
|
92
|
-
return this.
|
|
117
|
+
return this._style.protection;
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
set protection(value) {
|
|
@@ -99,7 +124,7 @@ class Cell {
|
|
|
99
124
|
_setStyle(name, value) {
|
|
100
125
|
// style objects can be shared (e.g. by every cell loaded with the same xf),
|
|
101
126
|
// so never mutate one in place; give this cell a new object instead
|
|
102
|
-
const oldStyle = this.
|
|
127
|
+
const oldStyle = this._style;
|
|
103
128
|
const style = {...oldStyle, [name]: value};
|
|
104
129
|
this.style = style;
|
|
105
130
|
|
|
@@ -112,12 +137,12 @@ class Cell {
|
|
|
112
137
|
(Array.isArray(merges)
|
|
113
138
|
? merges.find(merge => merge.top === master.row && merge.left === master.col)
|
|
114
139
|
: merges[master.address]);
|
|
115
|
-
if (range && (master === this || master.
|
|
140
|
+
if (range && (master === this || master._style === oldStyle)) {
|
|
116
141
|
for (let r = range.top; r <= range.bottom; r++) {
|
|
117
142
|
const row = this.worksheet.findRow(r);
|
|
118
143
|
for (let c = range.left; row && c <= range.right; c++) {
|
|
119
144
|
const cell = row.findCell(c);
|
|
120
|
-
if (cell && cell.
|
|
145
|
+
if (cell && cell._style === oldStyle) {
|
|
121
146
|
cell.style = style;
|
|
122
147
|
}
|
|
123
148
|
}
|
|
@@ -357,7 +382,7 @@ class Cell {
|
|
|
357
382
|
|
|
358
383
|
get model() {
|
|
359
384
|
const {model} = this._value;
|
|
360
|
-
model.style = this.
|
|
385
|
+
model.style = this._style;
|
|
361
386
|
if (this._comment) {
|
|
362
387
|
model.comment = this._comment.model;
|
|
363
388
|
}
|
|
@@ -378,7 +403,7 @@ class Cell {
|
|
|
378
403
|
}
|
|
379
404
|
|
|
380
405
|
if (value.style) {
|
|
381
|
-
this.
|
|
406
|
+
this._setSharedStyle(value.style);
|
|
382
407
|
} else {
|
|
383
408
|
this.style = {};
|
|
384
409
|
}
|
|
@@ -1,19 +1,101 @@
|
|
|
1
|
+
const colCache = require('../utils/col-cache');
|
|
2
|
+
|
|
3
|
+
function isRange(address) {
|
|
4
|
+
return typeof address === 'string' && address.includes(':');
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
class DataValidations {
|
|
2
8
|
constructor(model) {
|
|
3
9
|
this.model = model || {};
|
|
10
|
+
// Range-keyed entries (e.g. 'A2:A1048576', kept intact when a file is loaded) are tracked
|
|
11
|
+
// separately so per-cell add/find stay O(1) when there are none (exceljs#1842).
|
|
12
|
+
this._ranges = new Map();
|
|
13
|
+
Object.keys(this.model).forEach(key => {
|
|
14
|
+
if (isRange(key)) {
|
|
15
|
+
this._addRange(key);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
4
18
|
}
|
|
5
19
|
|
|
6
20
|
add(address, validation) {
|
|
21
|
+
if (isRange(address)) {
|
|
22
|
+
this._addRange(address);
|
|
23
|
+
} else {
|
|
24
|
+
this._splitRangeAt(address);
|
|
25
|
+
}
|
|
7
26
|
return (this.model[address] = validation);
|
|
8
27
|
}
|
|
9
28
|
|
|
10
29
|
find(address) {
|
|
11
|
-
|
|
30
|
+
const validation = this.model[address];
|
|
31
|
+
if (validation !== undefined || isRange(address) || !this._ranges.size) {
|
|
32
|
+
return validation;
|
|
33
|
+
}
|
|
34
|
+
// fall back to range-keyed entries that contain the address
|
|
35
|
+
const key = this._findRangeKey(address);
|
|
36
|
+
return key ? this.model[key] : undefined;
|
|
12
37
|
}
|
|
13
38
|
|
|
14
39
|
remove(address) {
|
|
40
|
+
if (isRange(address)) {
|
|
41
|
+
this._ranges.delete(address);
|
|
42
|
+
} else {
|
|
43
|
+
this._splitRangeAt(address);
|
|
44
|
+
}
|
|
15
45
|
this.model[address] = undefined;
|
|
16
46
|
}
|
|
47
|
+
|
|
48
|
+
_addRange(key) {
|
|
49
|
+
if (!this._ranges.has(key)) {
|
|
50
|
+
this._ranges.set(key, colCache.decodeEx(key));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_findRangeKey(address) {
|
|
55
|
+
if (!this._ranges.size) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const {row, col} = colCache.decodeAddress(address);
|
|
59
|
+
for (const [key, {top, left, bottom, right}] of this._ranges) {
|
|
60
|
+
if (this.model[key] && row >= top && row <= bottom && col >= left && col <= right) {
|
|
61
|
+
return key;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If a single cell lies inside a range-keyed entry, replace that entry with up to four
|
|
68
|
+
// rectangles covering the rest of the range, so the cell can get its own validation
|
|
69
|
+
// (or none) without overlapping sqrefs.
|
|
70
|
+
_splitRangeAt(address) {
|
|
71
|
+
const key = this._findRangeKey(address);
|
|
72
|
+
if (!key) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const validation = this.model[key];
|
|
76
|
+
const {row, col} = colCache.decodeAddress(address);
|
|
77
|
+
const {top, left, bottom, right} = this._ranges.get(key);
|
|
78
|
+
delete this.model[key];
|
|
79
|
+
this._ranges.delete(key);
|
|
80
|
+
|
|
81
|
+
const addRect = (t, l, b, r) => {
|
|
82
|
+
if (t > b || l > r) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const tl = colCache.encodeAddress(t, l);
|
|
86
|
+
if (t === b && l === r) {
|
|
87
|
+
this.model[tl] = validation;
|
|
88
|
+
} else {
|
|
89
|
+
const rangeKey = `${tl}:${colCache.encodeAddress(b, r)}`;
|
|
90
|
+
this.model[rangeKey] = validation;
|
|
91
|
+
this._addRange(rangeKey);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
addRect(top, left, row - 1, right); // rows above
|
|
95
|
+
addRect(row + 1, left, bottom, right); // rows below
|
|
96
|
+
addRect(row, left, row, col - 1); // left of the cell
|
|
97
|
+
addRect(row, col + 1, row, right); // right of the cell
|
|
98
|
+
}
|
|
17
99
|
}
|
|
18
100
|
|
|
19
101
|
module.exports = DataValidations;
|
package/lib/doc/image.js
CHANGED
|
@@ -56,4 +56,36 @@ class Image {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function isFiniteNumber(value) {
|
|
60
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Image.validateRange = function validateRange(range) {
|
|
64
|
+
if (typeof range === 'string') {
|
|
65
|
+
let decoded;
|
|
66
|
+
try {
|
|
67
|
+
decoded = colCache.decode(range);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
decoded = {};
|
|
70
|
+
}
|
|
71
|
+
const isRange = ['top', 'left', 'bottom', 'right'].every(key => isFiniteNumber(decoded[key]));
|
|
72
|
+
const isAddress = isFiniteNumber(decoded.row) && isFiniteNumber(decoded.col);
|
|
73
|
+
if (!isRange && !isAddress) {
|
|
74
|
+
throw new Error(`Invalid image range "${range}": expected a cell range such as "B2:D6"`);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!range || typeof range !== 'object') {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'Invalid image range: expected a cell range string or an object with tl and br or ext'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (!range.br && !range.ext) {
|
|
84
|
+
throw new Error('Invalid image range: an object range needs either br or ext');
|
|
85
|
+
}
|
|
86
|
+
if (range.ext && typeof range.ext !== 'object') {
|
|
87
|
+
throw new Error('Invalid image range: ext must be an object with width and height');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
59
91
|
module.exports = Image;
|
package/lib/doc/worksheet.js
CHANGED
|
@@ -781,7 +781,15 @@ class Worksheet {
|
|
|
781
781
|
|
|
782
782
|
// =========================================================================
|
|
783
783
|
// Images
|
|
784
|
+
_validateImageId(imageId) {
|
|
785
|
+
if (imageId === undefined || imageId === null || !this._workbook.getImage(imageId)) {
|
|
786
|
+
throw new Error(`Image with id ${imageId} not found. Add it with workbook.addImage() first`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
784
790
|
addImage(imageId, range) {
|
|
791
|
+
this._validateImageId(imageId);
|
|
792
|
+
Image.validateRange(range);
|
|
785
793
|
const model = {
|
|
786
794
|
type: 'image',
|
|
787
795
|
imageId,
|
|
@@ -795,6 +803,7 @@ class Worksheet {
|
|
|
795
803
|
}
|
|
796
804
|
|
|
797
805
|
addBackgroundImage(imageId) {
|
|
806
|
+
this._validateImageId(imageId);
|
|
798
807
|
const model = {
|
|
799
808
|
type: 'background',
|
|
800
809
|
imageId,
|
|
@@ -108,7 +108,11 @@ class WorkbookReader extends EventEmitter {
|
|
|
108
108
|
if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
|
|
109
109
|
match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
|
|
110
110
|
sheetNo = match[1];
|
|
111
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
this.sharedStrings &&
|
|
113
|
+
this.workbookRels &&
|
|
114
|
+
(this.options.styles !== 'cache' || this.stylesParsed)
|
|
115
|
+
) {
|
|
112
116
|
yield* this._parseWorksheet(iterateStream(entry), sheetNo);
|
|
113
117
|
} else {
|
|
114
118
|
// create temp file for each worksheet
|
|
@@ -316,6 +320,7 @@ class WorkbookReader extends EventEmitter {
|
|
|
316
320
|
if (this.options.styles === 'cache') {
|
|
317
321
|
this.styles = new StyleManager();
|
|
318
322
|
await this.styles.parseStream(iterateStream(entry));
|
|
323
|
+
this.stylesParsed = true;
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
326
|
|
|
@@ -14,6 +14,7 @@ const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
|
|
|
14
14
|
const AppXform = require('../../xlsx/xform/core/app-xform');
|
|
15
15
|
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
|
|
16
16
|
const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
|
|
17
|
+
const DrawingXform = require('../../xlsx/xform/drawing/drawing-xform');
|
|
17
18
|
|
|
18
19
|
const WorksheetWriter = require('./worksheet-writer');
|
|
19
20
|
|
|
@@ -45,6 +46,7 @@ class WorkbookWriter {
|
|
|
45
46
|
this.zipOptions = options.zip;
|
|
46
47
|
|
|
47
48
|
this.media = [];
|
|
49
|
+
this.drawings = [];
|
|
48
50
|
this.commentRefs = [];
|
|
49
51
|
|
|
50
52
|
this.zip = Archiver('zip', this.zipOptions);
|
|
@@ -99,6 +101,7 @@ class WorkbookWriter {
|
|
|
99
101
|
await this.promise;
|
|
100
102
|
await this.addMedia();
|
|
101
103
|
await this._commitWorksheets();
|
|
104
|
+
await this.addDrawings();
|
|
102
105
|
await Promise.all([
|
|
103
106
|
this.addContentTypes(),
|
|
104
107
|
this.addApp(),
|
|
@@ -219,6 +222,7 @@ class WorkbookWriter {
|
|
|
219
222
|
sharedStrings: this.sharedStrings,
|
|
220
223
|
commentRefs: this.commentRefs,
|
|
221
224
|
media: this.media,
|
|
225
|
+
drawings: this.drawings,
|
|
222
226
|
};
|
|
223
227
|
const xform = new ContentTypesXform();
|
|
224
228
|
const xml = xform.toXml(model);
|
|
@@ -249,6 +253,21 @@ class WorkbookWriter {
|
|
|
249
253
|
);
|
|
250
254
|
}
|
|
251
255
|
|
|
256
|
+
addDrawings() {
|
|
257
|
+
return new Promise(resolve => {
|
|
258
|
+
const drawingXform = new DrawingXform();
|
|
259
|
+
const relsXform = new RelationshipsXform();
|
|
260
|
+
this.drawings.forEach(drawing => {
|
|
261
|
+
drawingXform.prepare(drawing, {});
|
|
262
|
+
this.zip.append(drawingXform.toXml(drawing), {name: `xl/drawings/${drawing.name}.xml`});
|
|
263
|
+
this.zip.append(relsXform.toXml(drawing.rels), {
|
|
264
|
+
name: `xl/drawings/_rels/${drawing.name}.xml.rels`,
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
resolve();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
252
271
|
addApp() {
|
|
253
272
|
return new Promise(resolve => {
|
|
254
273
|
const model = {
|
|
@@ -13,6 +13,7 @@ const Column = require('../../doc/column');
|
|
|
13
13
|
const SheetRelsWriter = require('./sheet-rels-writer');
|
|
14
14
|
const SheetCommentsWriter = require('./sheet-comments-writer');
|
|
15
15
|
const DataValidations = require('../../doc/data-validations');
|
|
16
|
+
const Image = require('../../doc/image');
|
|
16
17
|
|
|
17
18
|
const xmlBuffer = new StringBuf();
|
|
18
19
|
|
|
@@ -246,6 +247,7 @@ class WorksheetWriter {
|
|
|
246
247
|
this._writePageSetup();
|
|
247
248
|
this._writeHeaderFooter();
|
|
248
249
|
this._writeRowBreaks();
|
|
250
|
+
this._writeDrawing(); // must be after rowBreaks and before legacyDrawing
|
|
249
251
|
|
|
250
252
|
// Legacy Data tag for comments
|
|
251
253
|
this._writeLegacyData();
|
|
@@ -472,7 +474,34 @@ class WorksheetWriter {
|
|
|
472
474
|
|
|
473
475
|
// =========================================================================
|
|
474
476
|
|
|
477
|
+
_validateImageId(imageId) {
|
|
478
|
+
if (imageId === undefined || imageId === null || !this._workbook.getImage(imageId)) {
|
|
479
|
+
throw new Error(`Image with id ${imageId} not found. Add it with workbook.addImage() first`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
addImage(imageId, range) {
|
|
484
|
+
if (this.committed) {
|
|
485
|
+
throw new Error('Cannot add an image to a committed worksheet');
|
|
486
|
+
}
|
|
487
|
+
this._validateImageId(imageId);
|
|
488
|
+
Image.validateRange(range);
|
|
489
|
+
// Anchors look up column widths and row heights to convert fractional positions.
|
|
490
|
+
// Only look at what already exists: getRow() would create (and later write) empty rows.
|
|
491
|
+
const lookup = {
|
|
492
|
+
properties: this.properties,
|
|
493
|
+
getColumn: c => this._columns && this._columns[c - 1],
|
|
494
|
+
getRow: r => this.findRow(r),
|
|
495
|
+
};
|
|
496
|
+
this._media.push(new Image(lookup, {type: 'image', imageId, range}));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
getImages() {
|
|
500
|
+
return this._media.filter(m => m.type === 'image');
|
|
501
|
+
}
|
|
502
|
+
|
|
475
503
|
addBackgroundImage(imageId) {
|
|
504
|
+
this._validateImageId(imageId);
|
|
476
505
|
this._background = {
|
|
477
506
|
imageId,
|
|
478
507
|
};
|
|
@@ -696,6 +725,50 @@ class WorksheetWriter {
|
|
|
696
725
|
}
|
|
697
726
|
}
|
|
698
727
|
|
|
728
|
+
_writeDrawing() {
|
|
729
|
+
const images = this.getImages();
|
|
730
|
+
if (!images.length) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const name = `drawing${this.id}`;
|
|
734
|
+
const rId = this._sheetRelsWriter.addRelationship({
|
|
735
|
+
Target: `../drawings/${name}.xml`,
|
|
736
|
+
Type: RelType.Drawing,
|
|
737
|
+
});
|
|
738
|
+
const drawing = {rId, name, anchors: [], rels: []};
|
|
739
|
+
const nextRid = () => `rId${drawing.rels.length + 1}`;
|
|
740
|
+
const imageRIds = {};
|
|
741
|
+
images.forEach(image => {
|
|
742
|
+
const {imageId, range, hyperlinks} = image.model;
|
|
743
|
+
let imageRId = imageRIds[imageId];
|
|
744
|
+
if (!imageRId) {
|
|
745
|
+
imageRId = imageRIds[imageId] = nextRid();
|
|
746
|
+
drawing.rels.push({
|
|
747
|
+
Id: imageRId,
|
|
748
|
+
Type: RelType.Image,
|
|
749
|
+
Target: `../media/${this._workbook.getImage(imageId).name}`,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const anchor = {picture: {rId: imageRId}, range};
|
|
753
|
+
if (hyperlinks && hyperlinks.hyperlink) {
|
|
754
|
+
const hyperlinkRId = nextRid();
|
|
755
|
+
anchor.picture.hyperlinks = {tooltip: hyperlinks.tooltip, rId: hyperlinkRId};
|
|
756
|
+
drawing.rels.push({
|
|
757
|
+
Id: hyperlinkRId,
|
|
758
|
+
Type: RelType.Hyperlink,
|
|
759
|
+
Target: hyperlinks.hyperlink,
|
|
760
|
+
TargetMode: 'External',
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
drawing.anchors.push(anchor);
|
|
764
|
+
});
|
|
765
|
+
this._workbook.drawings.push(drawing);
|
|
766
|
+
|
|
767
|
+
xmlBuffer.reset();
|
|
768
|
+
xmlBuffer.addText(`<drawing r:id="${rId}"/>`);
|
|
769
|
+
this.stream.write(xmlBuffer);
|
|
770
|
+
}
|
|
771
|
+
|
|
699
772
|
_writeLegacyData() {
|
|
700
773
|
if (this.hasComments) {
|
|
701
774
|
xmlBuffer.reset();
|
package/lib/xlsx/rel-type.js
CHANGED
|
@@ -12,6 +12,7 @@ module.exports = {
|
|
|
12
12
|
CoreProperties: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
|
|
13
13
|
ExtenderProperties: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
|
|
14
14
|
Comments: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
|
|
15
|
+
Drawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
|
|
15
16
|
VmlDrawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
|
|
16
17
|
Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
|
|
17
18
|
PivotCacheDefinition: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
@@ -22,16 +22,55 @@ class AutoFilterXform extends BaseXform {
|
|
|
22
22
|
const firstAddress = getAddress(model.from);
|
|
23
23
|
const secondAddress = getAddress(model.to);
|
|
24
24
|
if (firstAddress && secondAddress) {
|
|
25
|
-
|
|
25
|
+
const ref = `${firstAddress}:${secondAddress}`;
|
|
26
|
+
if (model.exclude && model.exclude.length) {
|
|
27
|
+
// hide the filter button on the excluded columns (0 = leftmost column of the range)
|
|
28
|
+
xmlStream.openNode('autoFilter', {ref});
|
|
29
|
+
model.exclude.forEach(colId => {
|
|
30
|
+
xmlStream.leafNode('filterColumn', {colId, hiddenButton: '1'});
|
|
31
|
+
});
|
|
32
|
+
xmlStream.closeNode();
|
|
33
|
+
} else {
|
|
34
|
+
xmlStream.leafNode('autoFilter', {ref});
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
parseOpen(node) {
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
switch (node.name) {
|
|
43
|
+
case 'autoFilter':
|
|
44
|
+
this.ref = node.attributes.ref;
|
|
45
|
+
this.exclude = [];
|
|
46
|
+
this.model = this.ref;
|
|
47
|
+
return true;
|
|
48
|
+
case 'filterColumn': {
|
|
49
|
+
const {colId, hiddenButton, showButton} = node.attributes;
|
|
50
|
+
const hidden =
|
|
51
|
+
hiddenButton === '1' ||
|
|
52
|
+
hiddenButton === 'true' ||
|
|
53
|
+
showButton === '0' ||
|
|
54
|
+
showButton === 'false';
|
|
55
|
+
if (hidden && colId !== undefined) {
|
|
56
|
+
this.exclude.push(parseInt(colId, 10));
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parseClose(name) {
|
|
66
|
+
if (name === 'autoFilter') {
|
|
67
|
+
if (this.exclude.length && this.ref) {
|
|
68
|
+
const [from, to] = this.ref.split(':');
|
|
69
|
+
this.model = {from, to: to || from, exclude: this.exclude};
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
34
72
|
}
|
|
73
|
+
return true;
|
|
35
74
|
}
|
|
36
75
|
}
|
|
37
76
|
|
|
@@ -4,6 +4,9 @@ const colCache = require('../../../utils/col-cache');
|
|
|
4
4
|
const BaseXform = require('../base-xform');
|
|
5
5
|
const Range = require('../../../doc/range');
|
|
6
6
|
|
|
7
|
+
// sqref ranges with more cells than this are kept as a single range-keyed model entry
|
|
8
|
+
const MAX_EXPANDED_RANGE_CELLS = 1000;
|
|
9
|
+
|
|
7
10
|
function assign(definedName, attributes, name, defaultValue) {
|
|
8
11
|
const value = attributes[name];
|
|
9
12
|
if (value !== undefined) {
|
|
@@ -215,12 +218,20 @@ class DataValidationsXform extends BaseXform {
|
|
|
215
218
|
}
|
|
216
219
|
// The four known cases: 1. E4:L9 N4:U9 2.E4 L9 3. N4:U9 4. E4
|
|
217
220
|
const list = this._address.split(/\s+/g) || [];
|
|
221
|
+
// Large ranges (e.g. a whole column 'A2:A1048576') are kept as one range-keyed entry
|
|
222
|
+
// instead of being expanded into one entry per cell, which could take minutes and
|
|
223
|
+
// gigabytes (exceljs#1842). DataValidations.find() resolves them. Small ranges are
|
|
224
|
+
// still expanded so the model keeps its per-cell shape and lookups stay O(1).
|
|
218
225
|
list.forEach(addr => {
|
|
219
226
|
if (addr.includes(':')) {
|
|
220
227
|
const range = new Range(addr);
|
|
221
|
-
range.
|
|
222
|
-
this.model[
|
|
223
|
-
}
|
|
228
|
+
if (range.count > MAX_EXPANDED_RANGE_CELLS) {
|
|
229
|
+
this.model[range.range] = this._dataValidation;
|
|
230
|
+
} else {
|
|
231
|
+
range.forEachAddress(address => {
|
|
232
|
+
this.model[address] = this._dataValidation;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
224
235
|
} else {
|
|
225
236
|
this.model[addr] = this._dataValidation;
|
|
226
237
|
}
|