@trebco/treb 23.6.2 → 25.0.0-rc1

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 (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +293 -299
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +320 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,3099 @@
1
+ /*
2
+ * This file is part of TREB.
3
+ *
4
+ * TREB is free software: you can redistribute it and/or modify it under the
5
+ * terms of the GNU General Public License as published by the Free Software
6
+ * Foundation, either version 3 of the License, or (at your option) any
7
+ * later version.
8
+ *
9
+ * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ * details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License along
15
+ * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ * Copyright 2022-2023 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ // --- treb imports -----------------------------------------------------------
23
+
24
+ import {
25
+ Cell, ValueType, Cells, Style,
26
+ Area, ICellAddress, CellSerializationOptions, IsFlatDataArray,
27
+ IsNestedRowArray, CellValue, ImportedSheetData, Complex,
28
+ DimensionedQuantity, IsCellAddress, IArea, Table, TableTheme,
29
+ } from 'treb-base-types';
30
+ import { NumberFormatCache } from 'treb-format';
31
+ import { Measurement, ValidateURI } from 'treb-utils';
32
+
33
+ import type { TextPart } from 'treb-base-types';
34
+
35
+ // --- local imports ----------------------------------------------------------
36
+
37
+ import type { FreezePane, SerializedSheet, ScrollOffset } from './sheet_types';
38
+ import type { SerializeOptions } from './serialize_options';
39
+ import { CreateSelection, GridSelection } from './grid_selection';
40
+ import { Annotation } from './annotation';
41
+
42
+ // --- constants --------------------------------------------------------------
43
+
44
+ const DEFAULT_COLUMN_WIDTH = 100;
45
+ // const DEFAULT_ROW_HEIGHT = 26; // not used because it's based on font (theoretically)
46
+ const DEFAULT_ROW_HEADER_WIDTH = 60;
47
+
48
+ // does this have optional ref/style because an older version inlined styles,
49
+ // instead of using references? we can probably drop support for that because
50
+ // if that was the case, it was a long time ago
51
+
52
+ interface CellStyleRef {
53
+ row: number;
54
+ column: number;
55
+ ref?: number;
56
+ style?: Style.Properties;
57
+ rows?: number;
58
+ }
59
+
60
+ export class Sheet {
61
+
62
+ // --- static members -------------------------------------------------------
63
+
64
+ public static base_id = 100;
65
+
66
+ public static readonly default_sheet_name = 'Sheet1';
67
+
68
+ // FIXME: use the external measurement object (from utils)
69
+ // private static measurement_canvas?: HTMLCanvasElement;
70
+
71
+ /**
72
+ * adding verbose flag so we can figure out who is publishing
73
+ * (and stop -- part of the ExecCommand switchover)
74
+ */
75
+ // public static readonly sheet_events = new EventSource<SheetEvent>(true, 'sheet-events');
76
+
77
+
78
+ // --- instance members -----------------------------------------------------
79
+
80
+ /**
81
+ * in the old model, we had a concept of "default" style properties. we then
82
+ * used that object for theming: we would set default properties when the theme
83
+ * changed.
84
+ *
85
+ * the problem is that if there are multiple instances on a single page, with
86
+ * different themes, they would clash.
87
+ *
88
+ * so the new concept is to have a default property set per instance, managed
89
+ * by the grid instance. any sheets that are loaded in/created by grid will
90
+ * get a reference to that property set, and grid can update it as desired.
91
+ *
92
+ * because it's a reference, it should be constant.
93
+ * FIXME: move to model...
94
+ */
95
+ public readonly default_style_properties: Style.Properties;
96
+
97
+ /* moved from grid */
98
+ public annotations: Annotation[] = [];
99
+
100
+ // moved from layout
101
+ public freeze: FreezePane = {
102
+ rows: 0,
103
+ columns: 0,
104
+ };
105
+
106
+ /** testing */
107
+ // public scale = 1.0;
108
+
109
+ public visible = true;
110
+
111
+ /** standard width (FIXME: static?) */
112
+ public default_column_width = 100;
113
+
114
+ /** standard height (FIXME: static?) */
115
+ public default_row_height = 25;
116
+
117
+ /** cells data */
118
+ public readonly cells: Cells = new Cells();
119
+
120
+ /**
121
+ * selection. moved to sheet to preserve selections in multiple sheets.
122
+ * this instance should just be used to populate the actual selection,
123
+ * not used as a reference.
124
+ */
125
+ public selection: GridSelection = CreateSelection();
126
+
127
+ /**
128
+ * cache scroll offset for flipping between sheets. should this be
129
+ * persisted? (...)
130
+ */
131
+ public scroll_offset: ScrollOffset = { x: 0, y: 0 };
132
+
133
+ /**
134
+ * named ranges: name -> area
135
+ * FIXME: this needs to move to an outer container, otherwise we
136
+ * may get conflicts w/ multiple sheets. unless we want to allow that...
137
+ */
138
+ // public named_ranges = new NamedRangeCollection();
139
+
140
+ public name = Sheet.default_sheet_name;
141
+
142
+ public background_image?: string;
143
+
144
+ protected _image: HTMLImageElement|undefined = undefined;
145
+
146
+ public get image(): HTMLImageElement|undefined {
147
+ return this._image;
148
+ }
149
+
150
+ /** internal ID */
151
+ // tslint:disable-next-line: variable-name
152
+ private id_: number;
153
+
154
+ // tslint:disable-next-line:variable-name
155
+ private row_height_: number[] = [];
156
+
157
+ // tslint:disable-next-line:variable-name
158
+ private column_width_: number[] = [];
159
+
160
+ /**
161
+ * optionally, custom row headers (instead of 1...2...3...)
162
+ * FIXME: should maybe be a function instead?
163
+ * FIXME: why is this any type? just sloppiness?
164
+ */
165
+ private row_headers: string[] = [];
166
+
167
+ /**
168
+ * optionally, custom column headers (instead of A...B...C...)
169
+ * FIXME: should maybe be a function instead?
170
+ * FIXME: why is this any type? just sloppiness?
171
+ */
172
+ private column_headers: string[] = [];
173
+
174
+ /** size of header */
175
+ private row_header_width = 100;
176
+
177
+ /** size of header */
178
+ private column_header_height = 25;
179
+
180
+ // we cache composite styles so we don't wind up with objects
181
+ // for every cell, when all we need is a single reference.
182
+
183
+ private style_map: Style.Properties[] = [];
184
+
185
+ // we use json for comparison. it should be faster than the alternative
186
+ // (even if that doesn't make sense).
187
+
188
+ private style_json_map: string[] = [];
189
+
190
+ // style now uses overlays, but we want to precalculate the
191
+ // overlaid values. we need to hold on to the originals, in
192
+ // the event something changes, so we can redo the calculation.
193
+
194
+ // there's a default at the bottom that gets applied to everything.
195
+ // (in Style). above that, we have the sheet style
196
+
197
+ private sheet_style: Style.Properties = {};
198
+
199
+ // then individual (applied) row and column styles (indexed by row/column)
200
+
201
+ private row_styles: Record<number, Style.Properties> = {};
202
+
203
+ private column_styles: Record<number, Style.Properties> = {};
204
+
205
+ /*
206
+ we used to have "alternate row" styles. it's clumsy, but it is a nice
207
+ effect. we will add that back via a "pattern". not sure how the UI would
208
+ work for this, but programatically it works.
209
+
210
+ just rows atm, not columns.
211
+ */
212
+
213
+ private row_pattern: Style.Properties[] = [];
214
+
215
+ // and finally any cell-specific styles. [FIXME: this is sparse]
216
+ // [why FIXME? sparse is OK in js]
217
+
218
+ private cell_style: Style.Properties[][] = [];
219
+
220
+ // --- accessors ------------------------------------------------------------
221
+
222
+ // public get column_header_count() { return this.column_header_count_; }
223
+
224
+ public get header_offset(): { x: number, y: number } {
225
+ return { x: this.row_header_width, y: this.column_header_height };
226
+ }
227
+
228
+ /** accessor: now just a wrapper for the call on cells */
229
+ public get rows(): number { return this.cells.rows; }
230
+
231
+ /** accessor: now just a wrapper for the call on cells */
232
+ public get columns(): number { return this.cells.columns; }
233
+
234
+ public get id(): number { return this.id_; }
235
+
236
+ public set id(id: number) {
237
+ this.id_ = id;
238
+ if (this.id >= Sheet.base_id) {
239
+ Sheet.base_id = this.id + 1;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * constructor is now protected. use a factory method (Blank or FromJSON).
245
+ */
246
+ protected constructor(theme_style_properties: Style.Properties) {
247
+
248
+ this.default_style_properties = theme_style_properties;
249
+
250
+ // FIXME: the below should be called in a separate 'init' method
251
+ // that can be called after we change styles (since it will measure)
252
+
253
+ this.default_column_width = DEFAULT_COLUMN_WIDTH;
254
+ this.row_header_width = DEFAULT_ROW_HEADER_WIDTH;
255
+ this.UpdateDefaultRowHeight();
256
+
257
+ this.id_ = Sheet.base_id++;
258
+
259
+ }
260
+
261
+ // --- class methods --------------------------------------------------------
262
+
263
+ public static Reset(): void {
264
+ this.base_id = 100;
265
+ }
266
+
267
+ /**
268
+ * factory method creates a new sheet
269
+ */
270
+ public static Blank(style_defaults: Style.Properties, name?: string, rows = 30, columns = 20): Sheet {
271
+
272
+ const sheet = new Sheet(style_defaults);
273
+
274
+ if (name) {
275
+ sheet.name = name;
276
+ }
277
+
278
+ rows = Math.max(rows, 1);
279
+ columns = Math.max(columns, 1);
280
+ sheet.cells.EnsureCell({ row: rows - 1, column: columns - 1 });
281
+ return sheet;
282
+ }
283
+
284
+ /**
285
+ * update old-style alignment constants to the new symbolic values.
286
+ * updates in place.
287
+ */
288
+ public static UpdateStyle(properties: Style.Properties) {
289
+
290
+ if (typeof properties.horizontal_align === 'number') {
291
+ const members = [
292
+ Style.HorizontalAlign.None,
293
+ Style.HorizontalAlign.Left,
294
+ Style.HorizontalAlign.Center,
295
+ Style.HorizontalAlign.Right,
296
+ ]
297
+ properties.horizontal_align = members[properties.horizontal_align] || undefined;
298
+ }
299
+
300
+ if (typeof properties.vertical_align === 'number') {
301
+ const members = [
302
+ Style.VerticalAlign.None,
303
+ Style.VerticalAlign.Top,
304
+ Style.VerticalAlign.Bottom,
305
+ Style.VerticalAlign.Middle,
306
+ ]
307
+ properties.vertical_align = members[properties.vertical_align] || undefined;
308
+ }
309
+
310
+ }
311
+
312
+ /**
313
+ * deserialize json representation. returns new instance or updates
314
+ * passed instance.
315
+ *
316
+ * FIXME: why not make this an instance method, always call on new instance?
317
+ *
318
+ * @param hints UpdateHints supports partial deserialization/replacement
319
+ * if we know there are only minor changes (as part of undo/redo, probably)
320
+ */
321
+ public static FromJSON(json: string | Partial<SerializedSheet>, style_defaults: Style.Properties, sheet?: Sheet): Sheet {
322
+
323
+ const source: SerializedSheet = (typeof json === 'string') ?
324
+ JSON.parse(json) : json as SerializedSheet;
325
+
326
+ const unflatten_numeric_array = (target: number[], data: Record<string, number>) => { // , default_value: number) => {
327
+ Object.keys(data).forEach((key) => {
328
+ const index = Number(key) || 0;
329
+ target[index] = data[key];
330
+ });
331
+ };
332
+
333
+ if (!sheet) {
334
+ sheet = new Sheet(style_defaults);
335
+ }
336
+
337
+ if (source.default_column_width) {
338
+ sheet.default_column_width = source.default_column_width;
339
+ }
340
+ if (source.default_row_height) {
341
+ sheet.default_row_height = source.default_row_height;
342
+ }
343
+
344
+ // persist ID, name
345
+
346
+ if (source.id) {
347
+ sheet.id = source.id;
348
+ }
349
+ if (source.name) {
350
+ sheet.name = source.name;
351
+ }
352
+
353
+ if (source.background_image) {
354
+ sheet.background_image = source.background_image;
355
+ }
356
+
357
+ // FIXME: this should only be done on load (and possibly paste).
358
+ // we don't need to do it on every parse, which also happens on
359
+ // undo and some other things.
360
+
361
+ const patch_style = (style: Style.Properties) => {
362
+
363
+ // this part is for back compat with older color schemes, it
364
+ // could theoretically come out if we don't care (or maybe have a tool)
365
+
366
+ // UPDATE for updated font properties
367
+
368
+ const ref = (style as Style.Properties & {
369
+ text_color?: string;
370
+ background?: string;
371
+ border_top_color?: string;
372
+ border_left_color?: string;
373
+ border_bottom_color?: string;
374
+ border_right_color?: string;
375
+
376
+ font_bold?: boolean;
377
+ font_italic?: boolean;
378
+ font_underline?: boolean;
379
+ font_strike?: boolean;
380
+
381
+ font_size_value?: number;
382
+ font_size_unit?: 'pt' | 'px' | 'em' | '%';
383
+
384
+ });
385
+
386
+ this.UpdateStyle(ref);
387
+
388
+ if (ref.font_size_value || ref.font_size_unit) {
389
+
390
+ ref.font_size = {
391
+ unit: ref.font_size_unit || 'pt',
392
+ value: ref.font_size_value || 10,
393
+ };
394
+
395
+ ref.font_size_unit = undefined;
396
+ ref.font_size_value = undefined;
397
+ }
398
+
399
+ if (ref.font_bold) {
400
+ ref.bold = true;
401
+ ref.font_bold = undefined;
402
+ }
403
+
404
+ if (ref.font_italic) {
405
+ ref.italic = true;
406
+ ref.font_italic = undefined;
407
+ }
408
+
409
+ if (ref.font_underline) {
410
+ ref.underline = true;
411
+ ref.font_underline = undefined;
412
+ }
413
+
414
+ if (ref.font_strike) {
415
+ ref.strike = true;
416
+ ref.font_strike = undefined;
417
+ }
418
+
419
+ if (ref.text_color) {
420
+ if (ref.text_color !== 'none') {
421
+ ref.text = { text: ref.text_color };
422
+ }
423
+ ref.text_color = undefined; // will get cleared, eventually
424
+ }
425
+
426
+ if (ref.background) {
427
+ if (ref.background !== 'none') {
428
+ ref.fill = { text: ref.background };
429
+ }
430
+ ref.background = undefined; // ibid
431
+ }
432
+
433
+ if (ref.border_top_color) {
434
+ if (ref.border_top_color !== 'none') {
435
+ ref.border_top_fill = { text: ref.border_top_color };
436
+ }
437
+ ref.border_top_color = undefined;
438
+ }
439
+
440
+ if (ref.border_left_color) {
441
+ if (ref.border_left_color !== 'none') {
442
+ ref.border_left_fill = { text: ref.border_left_color };
443
+ }
444
+ ref.border_left_color = undefined;
445
+ }
446
+
447
+ if (ref.border_bottom_color) {
448
+ if (ref.border_bottom_color !== 'none') {
449
+ ref.border_bottom_fill = { text: ref.border_bottom_color };
450
+ }
451
+ ref.border_bottom_color = undefined;
452
+ }
453
+
454
+ if (ref.border_right_color) {
455
+ if (ref.border_right_color !== 'none') {
456
+ ref.border_right_fill = { text: ref.border_right_color };
457
+ }
458
+ ref.border_right_color = undefined;
459
+ }
460
+
461
+ };
462
+
463
+ // use the new name, if available; fall back to the old name, and because
464
+ // that's now optional, add a default.
465
+
466
+ const cell_style_refs = source.styles || source.cell_style_refs || [];
467
+
468
+ /*
469
+ const cell_style_refs = source.cell_style_refs;
470
+ */
471
+ for (const entry of cell_style_refs) {
472
+ patch_style(entry);
473
+ }
474
+
475
+ // styles (part 1) -- moved up in case we use inlined style refs
476
+
477
+ // so this is converting "ref" (number) to "style" (properties)...
478
+ // in the same object. why do we do this here, and early?
479
+
480
+ sheet.cell_style = [];
481
+
482
+ if (cell_style_refs) {
483
+ (source.cell_styles || []).forEach((cell_style: CellStyleRef) => {
484
+ if (typeof cell_style.ref === 'number') {
485
+ cell_style.style =
486
+ JSON.parse(JSON.stringify(cell_style_refs[cell_style.ref])); // clone
487
+ }
488
+ });
489
+ }
490
+
491
+ // data: cells (moved after style)
492
+
493
+ sheet.cells.FromJSON(source.data);
494
+ if (source.rows) sheet.cells.EnsureRow(source.rows - 1);
495
+ if (source.columns) sheet.cells.EnsureColumn(source.columns - 1);
496
+
497
+ // new style stuff
498
+
499
+ // different handling for nested, flat, but we only have to
500
+ // check once because data is either nested or it isn't.
501
+
502
+ if (source.data) {
503
+ if (IsFlatDataArray(source.data)) {
504
+ for (const entry of source.data) {
505
+ if (entry.style_ref) {
506
+ if (!sheet.cell_style[entry.column]) sheet.cell_style[entry.column] = [];
507
+ sheet.cell_style[entry.column][entry.row] = // entry.style;
508
+ JSON.parse(JSON.stringify(cell_style_refs[entry.style_ref])); // clone
509
+ }
510
+ }
511
+ }
512
+ else {
513
+ if (IsNestedRowArray(source.data)) {
514
+ for (const block of source.data) {
515
+ const row = block.row;
516
+ for (const entry of block.cells) {
517
+ const column = entry.column;
518
+ if (entry.style_ref) {
519
+ if (!sheet.cell_style[column]) sheet.cell_style[column] = [];
520
+ sheet.cell_style[column][row] = // entry.style;
521
+ JSON.parse(JSON.stringify(cell_style_refs[entry.style_ref])); // clone
522
+ }
523
+ }
524
+ }
525
+ }
526
+ else {
527
+ for (const block of source.data) {
528
+ const column = block.column;
529
+ for (const entry of block.cells) {
530
+ const row = entry.row;
531
+ if (entry.style_ref) {
532
+ if (!sheet.cell_style[column]) sheet.cell_style[column] = [];
533
+ sheet.cell_style[column][row] = // entry.style;
534
+ JSON.parse(JSON.stringify(cell_style_refs[entry.style_ref])); // clone
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ }
541
+
542
+
543
+ // freeze
544
+
545
+ sheet.freeze.rows = 0;
546
+ sheet.freeze.columns = 0;
547
+
548
+ if (source.freeze) {
549
+ sheet.freeze.rows = source.freeze.rows || 0;
550
+ sheet.freeze.columns = source.freeze.columns || 0;
551
+ }
552
+
553
+ // scroll, optionally
554
+
555
+ sheet.scroll_offset = source.scroll ? { ...source.scroll } : { x: 0, y: 0 };
556
+
557
+ // wrap up styles
558
+
559
+ for (const cell_style of ((source.cell_styles || []) as CellStyleRef[])) {
560
+ if (cell_style.style) {
561
+ if (!sheet.cell_style[cell_style.column]) sheet.cell_style[cell_style.column] = [];
562
+ sheet.cell_style[cell_style.column][cell_style.row] = cell_style.style;
563
+
564
+ // update for blocks
565
+ // these are styles, not references... not sure why we translated
566
+ // (above) but if so, we probably need to clone
567
+
568
+ if (cell_style.rows) {
569
+ for (let r = 1; r < cell_style.rows; r++) {
570
+ sheet.cell_style[cell_style.column][cell_style.row + r] =
571
+ JSON.parse(JSON.stringify(cell_style.style));
572
+ }
573
+ }
574
+ }
575
+ }
576
+
577
+ sheet.sheet_style = source.sheet_style || {};
578
+ // sheet.row_styles = source.row_style;
579
+ // sheet.column_styles = source.column_style;
580
+
581
+ // these are NOT arrays atm. that might be a problem (might not). I think
582
+ // this was accidental. when running, we don't care, because empty array
583
+ // indexes don't consume memory (AFAIK). when serializing, we do care, but
584
+ // how we serialize shouldn't impact how we operate at runtime.
585
+
586
+ // it breaks when we do patching (below), although we could just fix
587
+ // patching. also TODO: merge patching with the map routine.
588
+
589
+ sheet.column_styles = {};
590
+ sheet.row_styles = {};
591
+
592
+ const MapStyles = (source_list: Record<number, number | Style.Properties>, target_list: Record<number, Style.Properties>) => {
593
+
594
+ for (const key of Object.keys(source_list)) {
595
+ const index = Number(key);
596
+ const value = source_list[index];
597
+ if (typeof value === 'number') {
598
+ const properties = cell_style_refs[value];
599
+ if (properties) {
600
+ target_list[index] = JSON.parse(JSON.stringify(properties)); // clone jic
601
+ patch_style(target_list[index]);
602
+ }
603
+ }
604
+ else if (value) {
605
+ target_list[index] = value;
606
+ patch_style(target_list[index]);
607
+ }
608
+ }
609
+ };
610
+
611
+ MapStyles(source.row_style, sheet.row_styles);
612
+ MapStyles(source.column_style, sheet.column_styles);
613
+
614
+ /*
615
+ for (const key of Object.keys(source.column_style)) {
616
+ const index = Number(key);
617
+ const value = source.column_style[index];
618
+ if (typeof value === 'number') {
619
+ const properties = cell_style_refs[value];
620
+ if (properties) {
621
+ sheet.column_styles[index] = JSON.parse(JSON.stringify(properties)); // clone jic
622
+ }
623
+ }
624
+ else {
625
+ sheet.column_styles[index] = value;
626
+ }
627
+ }
628
+ */
629
+
630
+ sheet.row_pattern = source.row_pattern || [];
631
+
632
+ // patch other styles
633
+
634
+ patch_style(sheet.sheet_style || {});
635
+ for (const entry of sheet.row_pattern) {
636
+ patch_style(entry);
637
+ }
638
+
639
+ /*
640
+ for (const key of Object.keys(sheet.column_styles)) {
641
+ patch_style(sheet.column_styles[key as any]);
642
+ }
643
+
644
+ for (const key of Object.keys(sheet.row_styles)) {
645
+ patch_style(sheet.row_styles[key as any]);
646
+ }
647
+ */
648
+
649
+ // ok
650
+
651
+
652
+ // if (hints && !hints.data) sheet.FlushCellStyles();
653
+
654
+ // sheet.default_row_height = obj.default_row_height;
655
+ // sheet.default_column_width = obj.default_column_width;
656
+
657
+ sheet.row_height_ = [];
658
+ unflatten_numeric_array(sheet.row_height_, source.row_height || {},
659
+ ); // sheet.default_row_height);
660
+ // obj.default_row_height);
661
+
662
+ if (sheet.row_height_.length) {
663
+ sheet.cells.EnsureRow(sheet.row_height_.length - 1);
664
+ }
665
+
666
+ sheet.column_width_ = [];
667
+ unflatten_numeric_array(sheet.column_width_, source.column_width || {},
668
+ ); // sheet.default_column_width);
669
+ // obj.default_column_width);
670
+
671
+ if (sheet.column_width_.length) {
672
+ sheet.cells.EnsureColumn(sheet.column_width_.length - 1);
673
+ }
674
+
675
+ // NOTE: we're padding out rows/columns here to be under annotations,
676
+ // otherwise the pruning may have removed them. it would probably be
677
+ // preferable to not prune them... that shouldn't add much extra data
678
+ // because it would just be the number.
679
+
680
+ // FIXME
681
+
682
+ sheet.annotations = (source.annotations || []).map((entry) => new Annotation(entry));
683
+
684
+ if (source.selection) {
685
+
686
+ // copy to ensure there's no link to random object
687
+ sheet.selection = JSON.parse(JSON.stringify(source.selection));
688
+
689
+ }
690
+
691
+ sheet.visible = true; // default
692
+ if (typeof source.visible !== 'undefined') {
693
+ sheet.visible = !!source.visible;
694
+ }
695
+
696
+
697
+ return sheet;
698
+
699
+ }
700
+
701
+
702
+ public Activate() {
703
+
704
+ // load background image, if set
705
+
706
+ if (this.background_image) {
707
+ const resource = ValidateURI(this.background_image);
708
+ if (resource) {
709
+ this._image = document.createElement('img');
710
+ this._image.src = resource;
711
+ }
712
+
713
+ // this._image = image_store.Get(this.background_image);
714
+ }
715
+ }
716
+
717
+ /* *
718
+ * factory method creates a sheet from a 2D array.
719
+ *
720
+ * /
721
+ public static FromArray(data: any[] = [], transpose = false): Sheet {
722
+ const sheet = new Sheet();
723
+ sheet.cells.FromArray(data, transpose);
724
+
725
+ return sheet;
726
+ }
727
+ */
728
+
729
+
730
+ // --- public methods -------------------------------------------------------
731
+
732
+ public MergeCells(area: Area): void {
733
+
734
+ // FIXME: it's an error if this area includes some
735
+ // (but not all) of another merge area.
736
+
737
+ // ...
738
+
739
+ // assuming we're good to go...
740
+
741
+ area = area.Clone();
742
+ this.cells.Apply(area, (cell, c, r) => {
743
+ cell.merge_area = area;
744
+ cell.render_clean = [];
745
+
746
+ // clear data in !head
747
+ if (c !== area.start.column || r !== area.start.row) cell.Reset();
748
+ }, true);
749
+
750
+ }
751
+
752
+ public UnmergeCells(area: Area): void {
753
+
754
+ // this _must_ be the full merge area. to get it, just get
755
+ // the merge property from a particular cell or cells.
756
+
757
+ // let's check:
758
+
759
+ let match = true;
760
+ this.cells.Apply(area, (cell) => {
761
+ match = match && !!cell.merge_area && area.Equals(cell.merge_area);
762
+ }, false);
763
+
764
+ if (!match) {
765
+ console.warn('area mismatch');
766
+ return;
767
+ }
768
+
769
+ this.cells.Apply(area, (cell) => {
770
+ cell.merge_area = undefined;
771
+ cell.render_clean = [];
772
+ }, false);
773
+
774
+ }
775
+
776
+ /**
777
+ * FIXME: measure the font.
778
+ *
779
+ * Can we use the same metrics as renderer? That uses a canvas. Obviously
780
+ * canvas won't work if there's no DOM but it's OK if this method fails in
781
+ * that case; the only question is will it break if it's running headless?
782
+ */
783
+ public StyleFontSize(style: Style.Properties, default_properties: Style.Properties = {}): number {
784
+
785
+ let font_height = (style.font_size?.value || 0);
786
+
787
+ let scale = 0;
788
+
789
+ switch (style.font_size?.unit) {
790
+ case 'px':
791
+ font_height *= (75 / 100);
792
+ break;
793
+
794
+ case 'em':
795
+ scale = style.font_size.value || 1;
796
+ break;
797
+
798
+ case '%':
799
+ scale = (style.font_size.value || 100) / 100;
800
+ break;
801
+ }
802
+
803
+ if (scale) {
804
+ font_height = scale * (default_properties.font_size?.value || 10);
805
+ if (default_properties.font_size?.unit === 'px') {
806
+ font_height *= (75 / 100);
807
+ }
808
+ }
809
+
810
+ return font_height || 10;
811
+
812
+ }
813
+
814
+ /**
815
+ * FIXME: this is called in the ctor, which made sense when sheets
816
+ * were more ephemeral. now that we update a single instance, rather
817
+ * than create new instances, we lose this behavior. we should call
818
+ * this when we change sheet style.
819
+ *
820
+ * removing parameter, event
821
+ */
822
+ public UpdateDefaultRowHeight(): void {
823
+
824
+ const composite = Style.Composite([this.default_style_properties, this.sheet_style]);
825
+
826
+ if (typeof window !== 'undefined') {
827
+
828
+ const measurement = Measurement.MeasureText(Style.Font(composite), 'M');
829
+ const height = Math.round(measurement.height * 1.4);
830
+
831
+ if (this.default_row_height < height) {
832
+ this.default_row_height = height;
833
+ }
834
+
835
+ }
836
+ /*
837
+ else {
838
+ // console.info('worker?');
839
+ }
840
+ */
841
+
842
+ }
843
+
844
+ /**
845
+ * deprecated (or give me a reason to keep it)
846
+ * KEEP IT: just maintain flexibility, it has very low cost
847
+ */
848
+ public SetRowHeaders(headers: CellValue[]): void {
849
+ this.row_headers = headers.map(value => value === undefined ? '' : value.toString());
850
+ if (this.row_headers) {
851
+ this.cells.EnsureRow(this.row_headers.length - 1);
852
+ }
853
+ }
854
+
855
+ /**
856
+ * deprecated (or give me a reason to keep it)
857
+ * KEEP IT: just maintain flexibility, it has very low cost
858
+ */
859
+ public SetColumnHeaders(headers: CellValue[]): void {
860
+ this.column_headers = headers.map(value => value === undefined ? '' : value.toString());
861
+ if (headers) {
862
+ this.cells.EnsureColumn(headers.length - 1);
863
+ }
864
+ }
865
+
866
+ /**
867
+ * deprecated
868
+ * KEEP IT: just maintain flexibility, it has very low cost
869
+ */
870
+ public RowHeader(row: number): string | number {
871
+ if (this.row_headers) {
872
+ if (this.row_headers.length > row) return this.row_headers[row];
873
+ return '';
874
+ }
875
+ return row + 1;
876
+ }
877
+
878
+ /**
879
+ * deprecated
880
+ * KEEP IT: just maintain flexibility, it has very low cost
881
+ * (we did drop the multiple rows, though)
882
+ */
883
+ public ColumnHeader(column: number): string {
884
+ let s = '';
885
+ if (this.column_headers) {
886
+ if (this.column_headers.length > column) return this.column_headers[column];
887
+ return '';
888
+ }
889
+ for (; ;) {
890
+ const c = column % 26;
891
+ s = String.fromCharCode(65 + c) + s;
892
+ column = Math.floor(column / 26);
893
+ if (column) column--;
894
+ else break;
895
+ }
896
+ return s;
897
+ }
898
+
899
+ public GetRowHeight(row: number): number {
900
+ const height = this.row_height_[row];
901
+ if (typeof height === 'undefined') return this.default_row_height;
902
+ return height;
903
+ }
904
+
905
+ public SetRowHeight(row: number, height: number): number {
906
+ this.row_height_[row] = height;
907
+ this.cells.EnsureRow(row);
908
+ return height;
909
+ }
910
+
911
+ public GetColumnWidth(column: number): number {
912
+ const width = this.column_width_[column];
913
+ if (typeof width === 'undefined') return this.default_column_width;
914
+ return width;
915
+ }
916
+
917
+ public SetColumnWidth(column: number, width: number): number {
918
+ this.column_width_[column] = width;
919
+ this.cells.EnsureColumn(column);
920
+ return width;
921
+ }
922
+
923
+ /**
924
+ * returns set of properties in B that differ from A. returns
925
+ * property values from B.
926
+ *
927
+ * this is the function I could never get to work inline for
928
+ * Style.Properties -- not sure why it works better with a generic
929
+ * function (although the partial here is new, so maybe it's that?)
930
+ *
931
+ * seems to be related to
932
+ * https://github.com/microsoft/TypeScript/pull/30769
933
+ *
934
+ */
935
+ public Delta<T extends object>(A: T, B: T): Partial<T> {
936
+
937
+ const result: Partial<T> = {};
938
+
939
+ // keys that are in either object. this will result in some
940
+ // duplication, probably not too bad. could precompute array? (...)
941
+
942
+ // you could do that using a composite object, but would be wasteful.
943
+ // would look good in typescript but generate extra javascript. might
944
+ // still be faster, though? (...)
945
+
946
+ const keys = [...Object.keys(A), ...Object.keys(B)] as Array<keyof T>;
947
+
948
+ // FIXME: should check if B[key] is undefined, in which case you don't
949
+ // want it? (...) that seems appropriate, but since the method we are
950
+ // replacing did not do that, I'm hesitant to do it now
951
+
952
+ for (const key of keys) {
953
+ const a = A[key];
954
+ const b = B[key];
955
+
956
+ // we are not checking for arrays, that's not a consideration atm
957
+
958
+ if (typeof a === 'object' && typeof b === 'object') {
959
+
960
+ // is this faster than checking properties?
961
+ // especially if we know the list?
962
+
963
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
964
+ result[key] = b;
965
+ }
966
+
967
+ }
968
+ else if (a !== b) {
969
+ result[key] = b;
970
+ }
971
+
972
+ //if (A[key] !== B[key]) {
973
+ // result[key] = B[key];
974
+ //}
975
+
976
+ }
977
+
978
+ return result;
979
+
980
+ }
981
+
982
+ /**
983
+ * updates cell styles. flushes cached style.
984
+ *
985
+ * @param delta merge with existing properties (we will win conflicts)
986
+ * @param inline this is part of another operation, don't do any undo/state updates
987
+ */
988
+ public UpdateCellStyle(address: ICellAddress, properties: Style.Properties, delta = true): void {
989
+
990
+ // so what this is doing is constructing two merge stacks: one including
991
+ // the cell style, and one without. any deltas among the two are the cell
992
+ // style. the aim here is to remove properties that would be duplicative
993
+ // because they stack, so if the base sheet has color=red, there is no
994
+ // reason to apply that to the cell as well.
995
+
996
+ const { row, column } = address;
997
+
998
+ if (!this.cell_style[column]) this.cell_style[column] = [];
999
+
1000
+ // testing
1001
+ // const underlying = this.CompositeStyleForCell(address, false);
1002
+ const underlying = this.CompositeStyleForCell(address, false, false);
1003
+
1004
+ const merged = Style.Composite([
1005
+ this.default_style_properties,
1006
+ underlying,
1007
+ Style.Merge(this.cell_style[column][row] || {}, properties, delta),
1008
+ ]);
1009
+
1010
+ const composite = this.Delta(underlying, merged);
1011
+
1012
+ /*
1013
+ // this is type "any" because of the assignment, below, which fails
1014
+ // otherwise. however this could be done with spread assignments? (...)
1015
+ // A: no, it's not merging them, it is looking for deltas.
1016
+ // ...but, what if you filtered? (...) [A] how?
1017
+
1018
+ // I think the only way to do it with types would be to use delete, which
1019
+ // somehow seems wasteful and slow (although I have not validated that)
1020
+
1021
+ const composite: any = {};
1022
+
1023
+ // find properties that are different, those will be the cell style.
1024
+
1025
+ for (const key of Object.keys(merged) as Style.PropertyKeys[]) {
1026
+ if (merged[key] !== underlying[key]) {
1027
+ composite[key] = merged[key];
1028
+ }
1029
+ }
1030
+ for (const key of Object.keys(underlying) as Style.PropertyKeys[]) {
1031
+ if (merged[key] !== underlying[key]) {
1032
+ composite[key] = merged[key];
1033
+ }
1034
+ }
1035
+ */
1036
+
1037
+ this.cell_style[column][row] = composite; // merged;
1038
+
1039
+ // targeted flush
1040
+ this.CellData(address).FlushStyle();
1041
+
1042
+ }
1043
+
1044
+ /**
1045
+ * invalidate sets the "render dirty" flag on cells, whether there
1046
+ * is any change or not. we are currently using it to force rendering
1047
+ * when border/background changes, and we need to handle bleed into
1048
+ * neighboring cells.
1049
+ */
1050
+ public Invalidate(area: Area): void {
1051
+ this.cells.Apply(this.RealArea(area), cell => cell.render_clean = []);
1052
+ }
1053
+
1054
+ /**
1055
+ *
1056
+ * @param area
1057
+ * @param style
1058
+ * @param delta
1059
+ * @param render LEGACY PARAMETER NOT USED
1060
+ */
1061
+ public UpdateAreaStyle(area?: Area, style: Style.Properties = {}, delta = true): void {
1062
+
1063
+ if (!area) return;
1064
+
1065
+ if (area.entire_sheet) {
1066
+ this.UpdateSheetStyle(style, delta);
1067
+ }
1068
+ else if (area.entire_column) {
1069
+ for (let column = area.start.column; column <= area.end.column; column++) {
1070
+ this.UpdateColumnStyle(column, style, delta);
1071
+ }
1072
+ }
1073
+ else if (area.entire_row) {
1074
+ for (let row = area.start.row; row <= area.end.row; row++) {
1075
+ this.UpdateRowStyle(row, style, delta);
1076
+ }
1077
+ }
1078
+ else area.Array().forEach((address) => this.UpdateCellStyle(address, style, delta));
1079
+
1080
+ }
1081
+
1082
+ /**
1083
+ * checks if the given cell has been assigned a specific style, either for
1084
+ * the cell itself, or for row and column.
1085
+ */
1086
+ public HasCellStyle(address: ICellAddress): boolean {
1087
+ return !!((this.cell_style[address.column] && this.cell_style[address.column][address.row])
1088
+ || this.row_styles[address.row]
1089
+ || this.column_styles[address.column]
1090
+ || this.row_pattern.length);
1091
+ }
1092
+
1093
+ /**
1094
+ * returns the next non-hidden column. so if you are column C (2) and columns
1095
+ * D, E, and F are hidden, then it will return 6 (G).
1096
+ */
1097
+ public NextVisibleColumn(column: number): number {
1098
+ for (++column; this.column_width_[column] === 0; column++) { /* */ }
1099
+ return column;
1100
+ }
1101
+
1102
+ /**
1103
+ * @see NextVisibleColumn
1104
+ * because this one goes left, it may return -1 meaning you are at the left edge
1105
+ */
1106
+ public PreviousVisibleColumn(column: number): number {
1107
+ for (--column; column >= 0 && this.column_width_[column] === 0; column--) { /* */ }
1108
+ return column;
1109
+ }
1110
+
1111
+ /**
1112
+ * @see NextVisibleColumn
1113
+ */
1114
+ public NextVisibleRow(row: number): number {
1115
+ for (++row; this.row_height_[row] === 0; row++) { /* */ }
1116
+ return row;
1117
+ }
1118
+
1119
+ /**
1120
+ * @see PreviousVisibleColumn
1121
+ */
1122
+ public PreviousVisibleRow(row: number): number {
1123
+ for (--row; row >= 0 && this.row_height_[row] === 0; row--) { /* */ }
1124
+ return row;
1125
+ }
1126
+
1127
+ /**
1128
+ * if this cell is part of a table, get row information -- is this
1129
+ * an alternate row, is it the header, is it the last (visible) row
1130
+ *
1131
+ * @param table
1132
+ * @param row
1133
+ * @returns
1134
+ */
1135
+ public TableRow(table: Table, row: number): {
1136
+ alternate?: boolean;
1137
+ header?: boolean;
1138
+ last?: boolean;
1139
+ totals?: boolean;
1140
+ } {
1141
+
1142
+ const result = {
1143
+ alternate: false,
1144
+ header: (row === table.area.start.row),
1145
+ last: false,
1146
+ totals: (table.totals_row && row === table.area.end.row),
1147
+ }
1148
+
1149
+ // can short circuit here
1150
+
1151
+ if (result.header || result.totals) {
1152
+ return result;
1153
+ }
1154
+
1155
+ // how we handle last row depends on totals. if we have a totals
1156
+ // row, and it's visible, we don't need to do the "last row" thing.
1157
+
1158
+ const totals_visible = (table.totals_row && (this.GetRowHeight(table.area.end.row) > 0));
1159
+
1160
+ if (!totals_visible) {
1161
+ let last = table.area.end.row;
1162
+ for ( ; last >= table.area.start.row; last-- ) {
1163
+ if (this.GetRowHeight(last)) {
1164
+ result.last = (last === row);
1165
+ break;
1166
+ }
1167
+ }
1168
+ }
1169
+
1170
+ let start = table.area.start.row + 1 ; // (table.headers ? 1 : 0);
1171
+ for ( ; start <= table.area.end.row; start++ ) {
1172
+ if (!this.GetRowHeight(start)) {
1173
+ continue;
1174
+ }
1175
+
1176
+ result.alternate = !result.alternate;
1177
+ if (start === row) {
1178
+ break;
1179
+ }
1180
+ }
1181
+
1182
+ return result;
1183
+ }
1184
+
1185
+ /**
1186
+ * returns style properties for cells surrounding this cell,
1187
+ * mapped like a number pad:
1188
+ *
1189
+ * +---+---+---+
1190
+ * | 7 | 8 | 9 |
1191
+ * +---+---+---+
1192
+ * | 4 | X | 6 |
1193
+ * +---+---+---+
1194
+ * | 1 | 2 | 3 |
1195
+ * +---+---+---+
1196
+ *
1197
+ * presuming you already have X (5). this is called by renderer, we
1198
+ * move it here so we can inline the next/previous loops.
1199
+ *
1200
+ */
1201
+ public SurroundingStyle(address: ICellAddress, table?: TableTheme): Style.Properties[] {
1202
+ const map: Style.Properties[] = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}];
1203
+
1204
+ // FIXME: what about merges? (...)
1205
+
1206
+ let column_right = address.column + 1;
1207
+ let column_left = address.column - 1;
1208
+ let row_below = address.row + 1;
1209
+ let row_above = address.row - 1;
1210
+
1211
+ for (; this.column_width_[column_right] === 0; column_right++) { /* */ }
1212
+ for (; this.row_height_[row_below] === 0; row_below++) { /* */ }
1213
+
1214
+ for (; column_left >= 0 && this.column_width_[column_left] === 0; column_left--) { /* */ }
1215
+ for (; row_above >= 0 && this.row_height_[row_above] === 0; row_above--) { /* */ }
1216
+
1217
+ if (column_left >= 0 && row_above >= 0) {
1218
+ map[7] = this.CellStyleData({ row: row_above, column: column_left }, table) || {};
1219
+ }
1220
+
1221
+ if (column_left >= 0) {
1222
+ map[4] = this.CellStyleData({ row: address.row, column: column_left }, table) || {};
1223
+ map[1] = this.CellStyleData({ row: row_below, column: column_left }, table) || {};
1224
+ }
1225
+
1226
+ if (row_above >= 0) {
1227
+ map[8] = this.CellStyleData({ row: row_above, column: address.column }, table) || {};
1228
+ map[9] = this.CellStyleData({ row: row_above, column: column_right }, table) || {};
1229
+ }
1230
+
1231
+ map[6] = this.CellStyleData({ row: address.row, column: column_right }, table) || {};
1232
+ map[2] = this.CellStyleData({ row: row_below, column: address.column }, table) || {};
1233
+ map[3] = this.CellStyleData({ row: row_below, column: column_right }, table) || {};
1234
+
1235
+ return map;
1236
+ }
1237
+
1238
+ /**
1239
+ * get style only. as noted in the comment to `CellData` there used to be
1240
+ * no case where this was useful without calculated value as well; but we
1241
+ * now have a case: fixing borders by checking neighboring cells. (testing).
1242
+ *
1243
+ * switching from null to undefined as "missing" type
1244
+ *
1245
+ * UPDATE: this is a convenient place to do table formatting. table
1246
+ * formatting is complicated because it's variable; it depends on row
1247
+ * visibility so we can't cache it. this is a good spot because we're
1248
+ * already calling this function when doing border rendering; we can call
1249
+ * it separately, if necessary, when rendering cells.
1250
+ *
1251
+ * table formats are applied on top of cell formats, after compositing,
1252
+ * and we don't preserve the style.
1253
+ *
1254
+ */
1255
+ public CellStyleData(address: ICellAddress, default_table_theme?: TableTheme): Style.Properties | undefined {
1256
+
1257
+ // don't create if it doesn't exist
1258
+ const cell = this.cells.GetCell(address);
1259
+ if (!cell) {
1260
+ return undefined;
1261
+ }
1262
+
1263
+ // composite style if necessary
1264
+ if (!cell.style) {
1265
+ const index = this.GetStyleIndex(this.CompositeStyleForCell(address));
1266
+ cell.style = this.style_map[index];
1267
+ }
1268
+
1269
+ if (cell.table) {
1270
+
1271
+ const table_theme = cell.table.theme || default_table_theme;
1272
+
1273
+ if (table_theme) {
1274
+
1275
+ let style = JSON.parse(JSON.stringify(cell.style));
1276
+ const data = this.TableRow(cell.table, address.row);
1277
+
1278
+ if (data.header) {
1279
+ if (table_theme.header) {
1280
+ style = Style.Composite([style, table_theme.header]);
1281
+ }
1282
+ }
1283
+ else if (data.totals) {
1284
+
1285
+ // like headers, totals is outside of the alternating rows thing
1286
+ if (table_theme.total) {
1287
+ style = Style.Composite([style, table_theme.total]);
1288
+ }
1289
+ }
1290
+ else {
1291
+ if (data.alternate) {
1292
+ if (table_theme.odd) {
1293
+ style = Style.Composite([style, table_theme.odd]);
1294
+ }
1295
+ }
1296
+ else {
1297
+ if (table_theme.even) {
1298
+ style = Style.Composite([style, table_theme.even]);
1299
+ }
1300
+ }
1301
+ }
1302
+
1303
+ /*
1304
+ if (data.last) {
1305
+ if (table_styles.footer) {
1306
+ style = Style.Composite([style, table_styles.footer]);
1307
+ }
1308
+ }
1309
+ */
1310
+
1311
+ return style;
1312
+ }
1313
+ }
1314
+
1315
+ return cell.style;
1316
+
1317
+ }
1318
+
1319
+ /**
1320
+ * accessor to get cell style without row pattern -- for cut/copy
1321
+ * @param address
1322
+ */
1323
+ public GetCopyStyle(address: ICellAddress): Style.Properties {
1324
+ return this.CompositeStyleForCell(address, true, false);
1325
+ }
1326
+
1327
+ /**
1328
+ * wrapper for getting all relevant render data.
1329
+ * TODO: merge in "FormattedValue". restructure data so we don't have
1330
+ * two caches (formatted and calculated).
1331
+ *
1332
+ * NOTE: we removed "GetCellStyle" in favor of this function. the rationale
1333
+ * is that there are no reasonable cases where someone looks up the style
1334
+ * without that being a next step to (or in reasonable proximity to)
1335
+ * rendering. so it's reasonable to call this function even if it's in
1336
+ * advance of rendering.
1337
+ *
1338
+ * NOTE: that applies to the "GetCellFormula" and "GetCellValue" functions
1339
+ * as well -- so remove those too.
1340
+ *
1341
+ * NOTE: actually GetCellFormula resolves array formulae, so maybe not --
1342
+ * or the caller needs to check.
1343
+ *
1344
+ */
1345
+ public CellData(address: ICellAddress): Cell {
1346
+
1347
+ const cell = this.cells.EnsureCell(address);
1348
+
1349
+ // if cell has rendered type (i.e. not undefined), then it has
1350
+ // complete render data and we can return it as-is.
1351
+
1352
+ if (cell.rendered_type) return cell;
1353
+
1354
+ // otherwise we need to render it. if we have a calculated value, use that.
1355
+
1356
+ let type: ValueType;
1357
+ let value: CellValue;
1358
+
1359
+ if (cell.calculated_type) {
1360
+ value = cell.calculated;
1361
+ type = cell.calculated_type;
1362
+ }
1363
+ else {
1364
+ value = cell.value;
1365
+ type = cell.type;
1366
+ }
1367
+
1368
+ // do we have style for this cell? if not, we need to composite it.
1369
+
1370
+ if (!cell.style) {
1371
+ const index = this.GetStyleIndex(this.CompositeStyleForCell(address));
1372
+ cell.style = this.style_map[index];
1373
+ }
1374
+
1375
+ // why is this done here? shouldn't it be done by/in the renderer?
1376
+
1377
+ if (!type || value === null || typeof value === 'undefined') {
1378
+ cell.formatted = '';
1379
+ cell.rendered_type = ValueType.string;
1380
+ }
1381
+ else if (type === ValueType.number) {
1382
+
1383
+ // IE11. not sure of the effect of this.
1384
+
1385
+ if (isNaN(value as number)) {
1386
+ cell.formatted = // Style.Format(cell.style, value); // formats NaN
1387
+ (typeof cell.style.nan === 'undefined') ? 'NaN' : cell.style.nan;
1388
+ }
1389
+ else {
1390
+ cell.formatted = // Style.Format(cell.style, value);
1391
+ this.FormatNumber(value, cell.style.number_format);
1392
+ }
1393
+ cell.rendered_type = ValueType.number;
1394
+ }
1395
+ else if (type === ValueType.error) {
1396
+ cell.formatted = '#' + (value || 'ERR?');
1397
+ cell.rendered_type = ValueType.error;
1398
+ }
1399
+ else if (type === ValueType.boolean) {
1400
+ cell.formatted = value.toString().toUpperCase(); // implicit locale?
1401
+ cell.rendered_type = ValueType.boolean;
1402
+ }
1403
+ else if (type === ValueType.formula && cell.calculated === undefined) {
1404
+ cell.formatted = '';
1405
+ cell.rendered_type = ValueType.string;
1406
+ }
1407
+ else if (type === ValueType.complex) {
1408
+
1409
+ // formatting complex value (note for searching)
1410
+ // here testing "mathematical italic small i", "𝑖", U+1D456
1411
+ //
1412
+ // I'm not sure this is a good idea, the character might not be available
1413
+ // in a particular font (not sure if those are auto-filled or what)
1414
+ //
1415
+ // what we _should_ do is have a formatting flag (in text part) to
1416
+ // indicate italic, and then render a regular lower-case i in italic.
1417
+ // that also means that if you copy it as text, it's still just a regular
1418
+ // i and not a high-value unicode character. which is helpful.
1419
+
1420
+ // OK we tried that and it looked like crap. I would like to go back
1421
+ // to using "𝑖" but I'm not sure... maybe a flag>
1422
+
1423
+ // NOTE: all that moved to NumberFormat
1424
+
1425
+ const complex = value as Complex;
1426
+ if (isNaN(complex.real) || isNaN(complex.imaginary)) {
1427
+
1428
+ // render nan for nan values
1429
+ cell.formatted = // Style.Format(cell.style, value); // formats NaN
1430
+ (typeof cell.style.nan === 'undefined') ? 'NaN' : cell.style.nan;
1431
+ }
1432
+ else {
1433
+ const format = NumberFormatCache.Get(cell.style.number_format || '', true);
1434
+ cell.formatted = format.FormatComplex(complex);
1435
+ }
1436
+
1437
+ cell.rendered_type = ValueType.complex;
1438
+ }
1439
+ else if (type === ValueType.dimensioned_quantity) {
1440
+
1441
+ // is this really what we want? NaN mm? or can we just do NaN?
1442
+
1443
+ // the reason for the question is that we want to move formatting
1444
+ // of DQ into format, in order that we can do logic on the formatting
1445
+ // side. but that won't work if we're short-circuiting here
1446
+
1447
+ // actually I guess it's immaterial, NaN mm is effectively === to NaN ft
1448
+
1449
+ if (isNaN((value as DimensionedQuantity).value)) {
1450
+ cell.formatted = // Style.Format(cell.style, value); // formats NaN
1451
+ (typeof cell.style.nan === 'undefined') ? 'NaN' : cell.style.nan;
1452
+
1453
+ cell.formatted += (` ` + (value as DimensionedQuantity).unit);
1454
+ }
1455
+ else {
1456
+ const format = NumberFormatCache.Get(cell.style.number_format || '', true);
1457
+ cell.formatted = // Style.Format(cell.style, value);
1458
+ // this.FormatNumber((value as DimensionedQuantity).value, cell.style.number_format);
1459
+ // this.FormatNumber(value, cell.style.number_format);
1460
+ format.FormatDimensionedQuantity(value as DimensionedQuantity);
1461
+ }
1462
+
1463
+ cell.rendered_type = ValueType.dimensioned_quantity; // who cares about rendered_type? (...)
1464
+
1465
+ }
1466
+ else {
1467
+
1468
+ // why is this being treated as a number? (...)
1469
+ // A: it's not, number format has a text section. defaults
1470
+ // to @ (just show the text), but could be different
1471
+
1472
+ cell.formatted = this.FormatNumber(value, cell.style.number_format);
1473
+ cell.rendered_type = ValueType.string;
1474
+ }
1475
+
1476
+ // now we can return it
1477
+ return cell;
1478
+
1479
+ }
1480
+
1481
+ /**
1482
+ * format number using passed format; gets the actual format object
1483
+ * and calls method. returns a string or array of text parts
1484
+ * (@see treb-format).
1485
+ */
1486
+ public FormatNumber(value: CellValue, format = ''): string | TextPart[] {
1487
+ const formatted = NumberFormatCache.Get(format).FormatParts(value);
1488
+ if (!formatted.length) return '';
1489
+ if (formatted.length === 1 && !formatted[0].flag) { return formatted[0].text || ''; }
1490
+ return formatted;
1491
+ }
1492
+
1493
+ // no references... removing
1494
+ //public ColumnHeaderHeight(): number {
1495
+ // return this.column_header_height || this.default_row_height_x;
1496
+ //}
1497
+
1498
+ /**
1499
+ * the only place this is called is in a method that shows/hides headers;
1500
+ * it sets the size either to 1 (hidden) or undefined, which uses the
1501
+ * defaults here. that suggests we should have a show/hide method instead.
1502
+ *
1503
+ * @param row_header_width
1504
+ * @param column_header_height
1505
+ */
1506
+ public SetHeaderSize(
1507
+ row_header_width = DEFAULT_ROW_HEADER_WIDTH,
1508
+ column_header_height = this.default_row_height): void {
1509
+
1510
+ this.row_header_width = row_header_width;
1511
+ this.column_header_height = column_header_height;
1512
+ }
1513
+
1514
+ /**
1515
+ * resize row to match character hight, taking into
1516
+ * account multi-line values.
1517
+ *
1518
+ * UPDATE: since the only caller calls with inline = true, removing
1519
+ * parameter, test, and extra behavior.
1520
+ */
1521
+ public AutoSizeRow(row: number, default_properties: Style.Properties = {}, allow_shrink = true): void {
1522
+
1523
+ let height = this.default_row_height;
1524
+ const padding = 9; // 9?
1525
+
1526
+ for (let column = 0; column < this.cells.columns; column++) {
1527
+
1528
+ const cell = this.CellData({ row, column });
1529
+ const style = cell.style;
1530
+ let text = cell.formatted || '';
1531
+
1532
+ if (typeof text !== 'string') {
1533
+ text = text.map((part) => part.text).join('');
1534
+ }
1535
+
1536
+ if (style && text && text.length) {
1537
+ const lines = text.split(/\n/);
1538
+ const font_height = Math.round(this.StyleFontSize(style, default_properties) * 1.5); // it's a start, we still need to measure properly
1539
+ height = Math.max(height, ((font_height || 10) + padding) * lines.length);
1540
+ }
1541
+ }
1542
+
1543
+ if (!allow_shrink) {
1544
+ const test = this.GetRowHeight(row);
1545
+ if (test >= height) { return; }
1546
+ }
1547
+
1548
+ this.SetRowHeight(row, height);
1549
+
1550
+ }
1551
+
1552
+ /* *
1553
+ * auto-sizes the column, but if the allow_shrink parameter is not set
1554
+ * it will only enlarge, never shrink the column.
1555
+ *
1556
+ * UPDATE: since the only caller calls with inline = true, removing
1557
+ * parameter, test, and extra behavior.
1558
+ *
1559
+ * UPDATE: moving to grid, for reasons of canvas...
1560
+ * /
1561
+ public AutoSizeColumn(column: number, allow_shrink = true): void {
1562
+
1563
+ if (!Sheet.measurement_canvas) {
1564
+ Sheet.measurement_canvas = document.createElement('canvas');
1565
+ }
1566
+ Sheet.measurement_canvas.style.font = Style.Font(this.default_style_properties);
1567
+ console.info("SMC", Sheet.measurement_canvas.style.font);
1568
+ (self as any).SMC = Sheet.measurement_canvas;
1569
+
1570
+ document
1571
+
1572
+ const context = Sheet.measurement_canvas.getContext('2d');
1573
+ if (!context) return;
1574
+
1575
+ let width = 12;
1576
+ const padding = 4 * 2; // FIXME: parameterize
1577
+
1578
+ if (!allow_shrink) width = this.GetColumnWidth(column);
1579
+
1580
+ for (let row = 0; row < this.cells.rows; row++) {
1581
+ const cell = this.CellData({ row, column });
1582
+ let text = cell.formatted || '';
1583
+ if (typeof text !== 'string') {
1584
+ text = text.map((part) => part.text).join('');
1585
+ }
1586
+
1587
+ if (text && text.length) {
1588
+ context.font = Style.Font(cell.style || {});
1589
+
1590
+ console.info({text, style: Style.Font(cell.style||{}), cf: context.font});
1591
+
1592
+ width = Math.max(width, Math.ceil(context.measureText(text).width) + padding);
1593
+ }
1594
+ }
1595
+
1596
+ this.SetColumnWidth(column, width);
1597
+
1598
+ }
1599
+ */
1600
+
1601
+ /** returns the style properties for a given style index */
1602
+ public GetStyle(index: number): Style.Properties {
1603
+ return this.style_map[index];
1604
+ }
1605
+
1606
+ /* *
1607
+ * if the cell is in an array, returns the array as an Area.
1608
+ * if not, returns falsy (null or undefined).
1609
+ *
1610
+ * FIXME: is this used? seems like the caller could do this
1611
+ * calculation.
1612
+ *
1613
+ * Answer was no, so removed
1614
+ * /
1615
+ public ContainingArray(address: ICellAddress): Area | undefined {
1616
+ const cell = this.cells.GetCell(address);
1617
+ if (cell) return cell.area;
1618
+ return undefined;
1619
+ }
1620
+ */
1621
+
1622
+ /**
1623
+ *
1624
+ * @param before_row insert before
1625
+ * @param count number to insert
1626
+ */
1627
+ public InsertRows(before_row = 0, count = 1): boolean {
1628
+
1629
+ // this needs to be shared between sheet/cells and the
1630
+ // outside spreadsheet logic. we should not be fixing references,
1631
+ // for example, because we don't have the graph.
1632
+
1633
+ // we should definitely fix merge heads. also array heads.
1634
+
1635
+ // also: you cannot insert rows that would break arrays.
1636
+ // if the new row(s) are inside of a merged cell, that cell
1637
+ // consumes the new row(s).
1638
+
1639
+ // validate we won't break arrays. a new row would break an
1640
+ // array if before_row is in an array and (before_row-1) is
1641
+ // in the same array.
1642
+
1643
+ if (before_row) {
1644
+ for (let column = 0; column < this.cells.columns; column++) {
1645
+ const cell1 = this.cells.GetCell({ row: before_row - 1, column }, false);
1646
+ if (cell1 && cell1.area) {
1647
+ const cell2 = this.cells.GetCell({ row: before_row, column }, false);
1648
+ if (cell2 && cell2.area && cell2.area.Equals(cell1.area)) {
1649
+ return false; // failed
1650
+ }
1651
+ }
1652
+ }
1653
+ }
1654
+
1655
+ // this.named_ranges.PatchNamedRanges(0, 0, before_row, count);
1656
+
1657
+ // ok we can insert...
1658
+
1659
+ if (count < 0) {
1660
+ this.cells.DeleteRows(before_row, -count);
1661
+ }
1662
+ else {
1663
+ this.cells.InsertRows(before_row, count);
1664
+ }
1665
+
1666
+ // now we have to fix arrays and merge heads. these lists will keep
1667
+ // track of the _new_ starting address.
1668
+
1669
+ const merge_heads: Record<string, Area> = {};
1670
+ const array_heads: Record<string, Area> = {};
1671
+ // const table_heads: Record<string, Table> = {};
1672
+
1673
+ // now grab arrays and merge heads that are below the new rows
1674
+ // this should include merges that span the new range
1675
+
1676
+ for (let row = before_row; row < this.cells.rows; row++) {
1677
+ for (let column = 0; column < this.cells.columns; column++) {
1678
+ const cell = this.cells.GetCell({ row, column }, false);
1679
+ if (cell) {
1680
+
1681
+ /*
1682
+ if (cell.table) {
1683
+ const label = new Area(cell.table.area.start, cell.table.area.end).spreadsheet_label;
1684
+ if (!table_heads[label]) {
1685
+ table_heads[label] = cell.table;
1686
+ }
1687
+ }
1688
+ */
1689
+
1690
+ if (cell.area && !array_heads[cell.area.spreadsheet_label]) {
1691
+ array_heads[cell.area.spreadsheet_label] = cell.area;
1692
+ }
1693
+ if (cell.merge_area && !merge_heads[cell.merge_area.spreadsheet_label]) {
1694
+ merge_heads[cell.merge_area.spreadsheet_label] = cell.merge_area;
1695
+ }
1696
+ }
1697
+ }
1698
+ }
1699
+
1700
+ // console.info("IR arrays", array_heads);
1701
+ // console.info("IR merges", merge_heads);
1702
+
1703
+ for (const key of Object.keys(array_heads)) {
1704
+ const head = array_heads[key];
1705
+ const patched = new Area(
1706
+ { row: head.start.row + count, column: head.start.column },
1707
+ { row: head.end.row + count, column: head.end.column });
1708
+ patched.Iterate((address) => {
1709
+ const cell = this.cells.GetCell(address, true);
1710
+ cell.area = patched;
1711
+ });
1712
+ }
1713
+
1714
+ /*
1715
+ for (const key of Object.keys(table_heads)) {
1716
+ const table = table_heads[key];
1717
+
1718
+ const patched_start = { ...table.area.start };
1719
+ if (table.area.start.row >= before_row) patched_start.row += count;
1720
+ const patched = new Area(
1721
+ patched_start,
1722
+ { row: table.area.end.row + count, column: table.area.end.column });
1723
+
1724
+ table.area = { start: patched.start, end: patched.end };
1725
+
1726
+ // we don't need to reset table for cells that already have it,
1727
+ // but we do need to add it to new rows. could simplify. FIXME
1728
+
1729
+ patched.Iterate((address) => {
1730
+ const cell = this.cells.GetCell(address, true);
1731
+ cell.table = table;
1732
+ });
1733
+ }
1734
+ */
1735
+
1736
+ for (const key of Object.keys(merge_heads)) {
1737
+ const head = merge_heads[key];
1738
+ const patched_start = { row: head.start.row, column: head.start.column };
1739
+ if (head.start.row >= before_row) patched_start.row += count;
1740
+ const patched = new Area(
1741
+ patched_start,
1742
+ { row: head.end.row + count, column: head.end.column });
1743
+ patched.Iterate((address) => {
1744
+ const cell = this.cells.GetCell(address, true);
1745
+ cell.merge_area = patched;
1746
+ });
1747
+ }
1748
+
1749
+ // row styles
1750
+
1751
+ const row_keys = Object.keys(this.row_styles);
1752
+ const new_row_style: Record<number, Style.Properties> = {};
1753
+
1754
+ row_keys.forEach((key) => {
1755
+ const index = Number(key);
1756
+ if (index < before_row) new_row_style[index] = this.row_styles[index];
1757
+ else if (count < 0 && index < before_row - count) { /* ? */ }
1758
+ else new_row_style[index + count] = this.row_styles[index];
1759
+ });
1760
+
1761
+ this.row_styles = new_row_style;
1762
+
1763
+ // cell styles
1764
+
1765
+ let args: Array<number | undefined> = [];
1766
+
1767
+ if (count < 0) {
1768
+ args = [before_row, -count];
1769
+ }
1770
+ else {
1771
+ args = [before_row, 0];
1772
+ for (let i = 0; i < count; i++) args.push(undefined);
1773
+ }
1774
+
1775
+ // console.info('m5.1');
1776
+
1777
+ this.cell_style.forEach((column) => {
1778
+
1779
+ if (column && column.length >= before_row) {
1780
+ // eslint-disable-next-line prefer-spread
1781
+ column.splice.apply(column, args as [number, number, Style.Properties]);
1782
+ }
1783
+ });
1784
+
1785
+ // console.info('m6');
1786
+
1787
+ // row heights
1788
+
1789
+ // eslint-disable-next-line prefer-spread
1790
+ this.row_height_.splice.apply(this.row_height_, args as [number, number, number]);
1791
+
1792
+ // invalidate style cache
1793
+ this.FlushCellStyles();
1794
+
1795
+ // console.info('m7');
1796
+
1797
+ return true;
1798
+
1799
+ }
1800
+
1801
+
1802
+ /**
1803
+ * see InsertRow for details
1804
+ */
1805
+ public InsertColumns(before_column = 0, count = 1): boolean {
1806
+
1807
+ // check for array breaks
1808
+
1809
+ if (before_column) {
1810
+ for (let row = 0; row < this.cells.rows; row++) {
1811
+ const cell1 = this.cells.GetCell({ row, column: before_column - 1 }, false);
1812
+ if (cell1 && cell1.area) {
1813
+ const cell2 = this.cells.GetCell({ row, column: before_column }, false);
1814
+ if (cell2 && cell2.area && cell2.area.Equals(cell1.area)) return false; // failed
1815
+ }
1816
+ }
1817
+ }
1818
+
1819
+ // this.named_ranges.PatchNamedRanges(before_column, count, 0, 0);
1820
+
1821
+ // ok we can insert...
1822
+
1823
+ if (count < 0) {
1824
+ this.cells.DeleteColumns(before_column, -count);
1825
+ }
1826
+ else {
1827
+ this.cells.InsertColumns(before_column, count);
1828
+ }
1829
+
1830
+ // now we have to fix arrays and merge heads. these lists will keep
1831
+ // track of the _new_ starting address.
1832
+
1833
+ // NOTE: tables are handled by the grid routine. for a time we were
1834
+ // doing that here but it's easier to unify on the grid size, since
1835
+ // we may need to update column headers or remove the model reference.
1836
+
1837
+ const merge_heads: Record<string, Area> = {};
1838
+ const array_heads: Record<string, Area> = {};
1839
+
1840
+ // now grab arrays and merge heads that are below the new rows
1841
+ // this should include merges that span the new range
1842
+
1843
+ for (let column = before_column; column < this.cells.columns; column++) {
1844
+ for (let row = 0; row < this.cells.rows; row++) {
1845
+ const cell = this.cells.GetCell({ row, column }, false);
1846
+ if (cell) {
1847
+ if (cell.area && !array_heads[cell.area.spreadsheet_label]) {
1848
+ array_heads[cell.area.spreadsheet_label] = cell.area;
1849
+ }
1850
+ if (cell.merge_area && !merge_heads[cell.merge_area.spreadsheet_label]) {
1851
+ merge_heads[cell.merge_area.spreadsheet_label] = cell.merge_area;
1852
+ }
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ for (const key of Object.keys(array_heads)) {
1858
+ const head = array_heads[key];
1859
+ const patched = new Area(
1860
+ { row: head.start.row, column: head.start.column + count },
1861
+ { row: head.end.row, column: head.end.column + count });
1862
+ patched.Iterate((address) => {
1863
+ const cell = this.cells.GetCell(address, true);
1864
+ cell.area = patched;
1865
+ });
1866
+ }
1867
+
1868
+ for (const key of Object.keys(merge_heads)) {
1869
+ const head = merge_heads[key];
1870
+ const patched_start = { row: head.start.row, column: head.start.column };
1871
+ if (head.start.column >= before_column) patched_start.column += count;
1872
+ const patched = new Area(
1873
+ patched_start,
1874
+ { row: head.end.row, column: head.end.column + count });
1875
+ patched.Iterate((address) => {
1876
+ const cell = this.cells.GetCell(address, true);
1877
+ cell.merge_area = patched;
1878
+ });
1879
+ }
1880
+
1881
+ // column styles
1882
+
1883
+ const column_keys = Object.keys(this.column_styles);
1884
+ const new_column_style: Record<number, Style.Properties> = {};
1885
+
1886
+ column_keys.forEach((key) => {
1887
+ const index = Number(key);
1888
+ if (index < before_column) new_column_style[index] = this.column_styles[index];
1889
+ else if (count < 0 && index < before_column - count) { /* ? */ }
1890
+ else new_column_style[index + count] = this.column_styles[index];
1891
+ });
1892
+
1893
+ this.column_styles = new_column_style;
1894
+
1895
+ // cell styles
1896
+
1897
+ let args: Array<number | undefined> = [];
1898
+
1899
+ if (count < 0) {
1900
+ args = [before_column, -count];
1901
+ }
1902
+ else {
1903
+ args = [before_column, 0];
1904
+ for (let i = 0; i < count; i++) args.push(undefined);
1905
+ }
1906
+
1907
+ // eslint-disable-next-line prefer-spread
1908
+ this.cell_style.splice.apply(this.cell_style, args as [number, number, Style.Properties[]]);
1909
+
1910
+ // row heights
1911
+
1912
+ // eslint-disable-next-line prefer-spread
1913
+ this.column_width_.splice.apply(this.column_width_, args as [number, number, number]);
1914
+
1915
+ // invalidate style cache
1916
+
1917
+ this.FlushCellStyles();
1918
+
1919
+ return true;
1920
+
1921
+ }
1922
+
1923
+ /** clear cells in area */
1924
+ public ClearArea(area: Area): void {
1925
+
1926
+ // this is not allowed if any of the cells are in
1927
+ // an array, and the array does not match the passed
1928
+ // array.
1929
+
1930
+ // ...
1931
+
1932
+ // assuming it's ok, :
1933
+
1934
+ area = this.RealArea(area);
1935
+ this.cells.Apply(area, (cell) => cell.Reset());
1936
+
1937
+ }
1938
+
1939
+ // ATM we have 4 methods to set value/values. we need a distinction for
1940
+ // arrays, but that could be a parameter. the single-value/multi-value
1941
+ // area functions could probably be consolidated, also the single-cell-
1942
+ // single-value function... you need logic either on the outside or the
1943
+ // inside, put that logic where it makes the most sense.
1944
+
1945
+ // also some of this could be moved to the Cells class... if for no
1946
+ // other reason than to remove the iteration overhead
1947
+
1948
+ public SetAreaValues2(area: Area, values: CellValue | CellValue[][]): void {
1949
+
1950
+ // we don't want to limit this to the existing area, we only
1951
+ // want to remove infinities (if set). it's possible to expand
1952
+ // the grid here (maybe -- check option?)
1953
+
1954
+ // actually, realarea already does exactly that -- which is not
1955
+ // what I thought. we may need a new, different method to clip.
1956
+
1957
+ area = this.RealArea(area);
1958
+ this.cells.SetArea(area, values);
1959
+ }
1960
+
1961
+ /**
1962
+ * set the area as an array formula, based in the top-left cell
1963
+ */
1964
+ public SetArrayValue(area: Area, value: CellValue): void {
1965
+ area = this.RealArea(area);
1966
+ this.cells.Apply(area, (element) => element.SetArray(area), true);
1967
+ const cell = this.cells.GetCell(area.start, true);
1968
+ cell.SetArrayHead(area, value);
1969
+ }
1970
+
1971
+ /**
1972
+ * set a single value in a single cell
1973
+ */
1974
+ public SetCellValue(address: ICellAddress, value: CellValue): void {
1975
+ const cell = this.cells.GetCell(address, true);
1976
+ cell.Set(value);
1977
+ }
1978
+
1979
+ /**
1980
+ * FIXME: does not need to be in sheet
1981
+ *
1982
+ * @param headers_only - only return tables if the cell is in the
1983
+ * header (first) row. useful if you only want to worry about headers.
1984
+ */
1985
+ public TablesFromArea(area: IArea|ICellAddress, headers_only = false): Table[] {
1986
+
1987
+ if (IsCellAddress(area)) {
1988
+ const cell = this.cells.GetCell(area, false);
1989
+ if (cell?.table) {
1990
+ if (!headers_only || (area.row === cell.table.area.start.row)) {
1991
+ return [cell.table];
1992
+ }
1993
+ }
1994
+ return [];
1995
+ }
1996
+
1997
+ const set: Set<Table> = new Set();
1998
+
1999
+ for (let row = area.start.row; row <= area.end.row; row++) {
2000
+ for (let column = area.start.column; column <= area.end.column; column++) {
2001
+ const cell = this.cells.GetCell({row, column}, false);
2002
+ if (cell?.table && !set.has(cell.table)) {
2003
+ if (!headers_only || (row === cell.table.area.start.row)) {
2004
+ set.add(cell.table);
2005
+ }
2006
+ }
2007
+ }
2008
+ }
2009
+
2010
+ return Array.from(set.values());
2011
+
2012
+ }
2013
+
2014
+ /**
2015
+ * returns the area bounding actual content
2016
+ * (i.e. flattening "entire row/column/sheet")
2017
+ *
2018
+ * FIXME: this does not clamp to actual cells... why not?
2019
+ * FIXME: so now we are (optionally) clamping end; should clamp start, too
2020
+ *
2021
+ * @param clamp -- new parameter will optionally clamp to actual sheet size
2022
+ */
2023
+ public RealArea(area: Area, clamp = false): Area {
2024
+
2025
+ const start = area.start; // this is a copy
2026
+ const end = area.end; // ditto
2027
+
2028
+ if (area.entire_row) {
2029
+ start.column = 0;
2030
+ start.absolute_column = false;
2031
+ end.column = this.cells.columns - 1;
2032
+ end.absolute_column = false;
2033
+ }
2034
+
2035
+ if (area.entire_column) {
2036
+ start.row = 0;
2037
+ start.absolute_row = false;
2038
+ end.row = this.cells.rows - 1;
2039
+ end.absolute_row = false;
2040
+ }
2041
+
2042
+ if (clamp) {
2043
+ if (end.row >= this.rows) {
2044
+ end.row = this.rows - 1;
2045
+ end.absolute_row = false;
2046
+ }
2047
+ if (end.column >= this.columns) {
2048
+ end.column = this.columns - 1;
2049
+ end.absolute_column = false;
2050
+ }
2051
+ }
2052
+
2053
+ return new Area(start, end);
2054
+
2055
+ }
2056
+
2057
+ /**
2058
+ * this is a new GetCellStyle function, used for external access
2059
+ * to style (for API access). there was an old GetCellStyle function
2060
+ * for rendering, but that's been removed (control+F for info).
2061
+ */
2062
+ public GetCellStyle(area: ICellAddress|IArea, apply_theme = false): Style.Properties|Style.Properties[][] {
2063
+
2064
+ if (IsCellAddress(area)) {
2065
+ return this.CompositeStyleForCell(area, true, false, apply_theme);
2066
+ }
2067
+
2068
+ if (area.start.row === area.end.row && area.start.column === area.end.column) {
2069
+ return this.CompositeStyleForCell(area.start, true, false, apply_theme);
2070
+ }
2071
+
2072
+ const result: Style.Properties[][] = [];
2073
+
2074
+ for (let r = area.start.row; r <= area.end.row; r++) {
2075
+ const row: Style.Properties[] = [];
2076
+ for (let c = area.start.column; c <= area.end.column; c++) {
2077
+ // const cell = this.CellData({row: r, column: c});
2078
+ // row.push(cell.style || {});
2079
+ row.push(this.CompositeStyleForCell({row: r, column: c}, true, false, apply_theme));
2080
+ }
2081
+ result.push(row);
2082
+ }
2083
+
2084
+ return result;
2085
+
2086
+ }
2087
+
2088
+ ///
2089
+ public FormattedCellValue(address: ICellAddress): CellValue {
2090
+
2091
+ const cell = this.CellData(address);
2092
+ if (!cell) {
2093
+ return undefined;
2094
+ }
2095
+
2096
+ if (typeof cell.formatted === 'string') return cell.formatted;
2097
+ if (cell.formatted) {
2098
+ return cell.formatted.map(part => {
2099
+ switch (part.flag) {
2100
+ case 1:
2101
+ return ' ';
2102
+ case 2:
2103
+ return ' '; // ??
2104
+ default:
2105
+ return part.text;
2106
+ }
2107
+ }).join('');
2108
+ }
2109
+ return cell.value;
2110
+ }
2111
+
2112
+ public GetFormattedRange(from: ICellAddress, to: ICellAddress = from): CellValue | CellValue[][] {
2113
+
2114
+ if (from.row === to.row && from.column === to.column) {
2115
+ return this.FormattedCellValue(from);
2116
+ }
2117
+
2118
+ const result: CellValue[][] = [];
2119
+
2120
+ // grab rows
2121
+ for (let row = from.row; row <= to.row; row++) {
2122
+ const target: CellValue[] = [];
2123
+ for (let column = from.column; column <= to.column; column++) {
2124
+ target.push(this.FormattedCellValue({ row, column }));
2125
+ }
2126
+ result.push(target);
2127
+ }
2128
+
2129
+ return result;
2130
+
2131
+ }
2132
+
2133
+ /**
2134
+ * get all styles used in the sheet. this is used to populate color
2135
+ * and number format lists in the toolbar. we used to just serialize
2136
+ * the document and use that, but that's absurdly wasteful. for this
2137
+ * application we don't even need composites.
2138
+ *
2139
+ * although, this is a bit dangerous because you could (in theory)
2140
+ * modify the results in place. so maybe we should either duplicate or
2141
+ * just return the requested data...
2142
+ */
2143
+ public NumberFormatsAndColors(
2144
+ color_map: Record<string, number>,
2145
+ number_format_map: Record<string, number>,
2146
+ ): void {
2147
+
2148
+ const parse = (style: Style.Properties) => {
2149
+
2150
+ if (style.number_format) {
2151
+ number_format_map[style.number_format] = 1;
2152
+ }
2153
+
2154
+ if (style.text?.text && style.text.text !== 'none') {
2155
+ // const color = Measurement.MeasureColorARGB(style.text_color);
2156
+ color_map[style.text.text] = 1;
2157
+ }
2158
+
2159
+ if (style.fill?.text) {
2160
+ color_map[style.fill.text] = 1;
2161
+ }
2162
+
2163
+ //if (style.background && style.background !== 'none') {
2164
+ // color_map[style.background] = 1;
2165
+ //}
2166
+
2167
+ if (style.border_top_fill?.text) {
2168
+ color_map[style.border_top_fill.text] = 1;
2169
+ }
2170
+ if (style.border_left_fill?.text) {
2171
+ color_map[style.border_left_fill.text] = 1;
2172
+ }
2173
+ if (style.border_right_fill?.text) {
2174
+ color_map[style.border_right_fill.text] = 1;
2175
+ }
2176
+ if (style.border_bottom_fill?.text) {
2177
+ color_map[style.border_bottom_fill.text] = 1;
2178
+ }
2179
+
2180
+ };
2181
+
2182
+ parse(this.sheet_style);
2183
+
2184
+ for (const key in this.row_styles) {
2185
+ parse(this.row_styles[key]);
2186
+ }
2187
+
2188
+ for (const key in this.column_styles) {
2189
+ parse(this.column_styles[key]);
2190
+ }
2191
+
2192
+ for (const style of this.row_pattern) {
2193
+ parse(style);
2194
+ }
2195
+
2196
+ for (const row of this.cell_style) {
2197
+ if (row) {
2198
+ for (const style of row) {
2199
+ if (style) {
2200
+ parse(style);
2201
+ }
2202
+ }
2203
+ }
2204
+ }
2205
+
2206
+ }
2207
+
2208
+ public CompressCellStyles(data: number[][]) {
2209
+
2210
+ // we can almost certainly compress the cell style map (above) if there
2211
+ // are consistent areas. not sure what the optimal algorithms for this
2212
+ // are, but there are probably some out there. let's start naively and
2213
+ // see what we can get.
2214
+
2215
+ // I think the real issue is imports from XLSX; we're getting a lot
2216
+ // of individual cell styles where there should probably be R/C styles.
2217
+
2218
+ // actually we might be working against ourselves here if we are
2219
+ // removing populated cells from this array: because in that case we'll
2220
+ // get fewer contiguous blocks. perhaps we should have a "lookaround"
2221
+ // in the original array? (...)
2222
+
2223
+ // OTOH this can never be _worse_ than the old method, and I don't think
2224
+ // it costs much more. so we'll stick with this for the time being, see
2225
+ // if we can further optimize later.
2226
+
2227
+ // (note: tried passing the original array, and checking for overlap,
2228
+ // but ultimately savings was minimal. not worth it)
2229
+
2230
+ const list: Array<{ row: number; column: number; ref: number, rows?: number }> = [];
2231
+
2232
+ for (let c = 0; c < data.length; c++) {
2233
+ const column = data[c];
2234
+
2235
+ if (column) {
2236
+ for (let r = 0; r < column.length; r++) {
2237
+ const style = column[r];
2238
+ if (style) {
2239
+
2240
+ let k = r + 1;
2241
+
2242
+ for (; k < column.length; k++) {
2243
+ if (column[k] !== style) { break; }
2244
+ }
2245
+
2246
+ if ( k > r + 1 ){
2247
+ list.push({ row: r, column: c, ref: style, rows: k - r });
2248
+ }
2249
+ else {
2250
+ list.push({ row: r, column: c, ref: style });
2251
+ }
2252
+
2253
+ r = k - 1;
2254
+
2255
+ }
2256
+ }
2257
+ }
2258
+ }
2259
+
2260
+ return list;
2261
+
2262
+ }
2263
+
2264
+ /**
2265
+ * generates serializable object. given the new data semantics this
2266
+ * has to change a bit. here is what we are storing:
2267
+ *
2268
+ * all style data (sheet, row/column, alternate and cell)
2269
+ * raw value for cell
2270
+ * array head for arrays
2271
+ * row height and column width arrays
2272
+ *
2273
+ * because we have sparse arrays, we convert them to flat objects first.
2274
+ */
2275
+ public toJSON(options: SerializeOptions = {}): SerializedSheet {
2276
+
2277
+ // flatten height/width arrays
2278
+
2279
+ const flatten_numeric_array = (arr: number[], default_value: number) => {
2280
+ const obj: Record<number, number> = {};
2281
+
2282
+ for (let i = 0; i < arr.length; i++) {
2283
+ if ((typeof arr[i] !== 'undefined') && arr[i] !== default_value) obj[i] = arr[i];
2284
+ }
2285
+ if (Object.keys(obj).length) return obj;
2286
+ return undefined;
2287
+ };
2288
+
2289
+ // flatten cell styles, which is a sparse array
2290
+ // UPDATE: ref table
2291
+
2292
+ // NOTE: we originally did this (I think) because it's possible for a
2293
+ // cell to have a style but have no other data, and therefore not be
2294
+ // represented. but we should be able to store the data in the cell object
2295
+ // if we have it...
2296
+
2297
+ let cell_style_refs = [{}]; // include an empty entry at zero
2298
+
2299
+ const cell_style_map: Record<string, number> = {};
2300
+
2301
+ const cell_reference_map: number[][] = [];
2302
+
2303
+ // (1) create a map of cells -> references, and build the reference
2304
+ // table at the same time. preserve indexes? (...)
2305
+
2306
+ // it would be nice if we could use some sort of numeric test, rather
2307
+ // than leaving empty indexes as undefined -- that requires a type test
2308
+ // (to avoid zeros).
2309
+
2310
+ const empty_json = JSON.stringify({});
2311
+
2312
+ // actually we could just offset the index by 1... (see above)
2313
+
2314
+ for (let c = 0; c < this.cell_style.length; c++) {
2315
+ const column = this.cell_style[c];
2316
+ if (column) {
2317
+ cell_reference_map[c] = [];
2318
+ for (let r = 0; r < column.length; r++) {
2319
+ if (column[r]) {
2320
+ const style_as_json = JSON.stringify(column[r]);
2321
+ if (style_as_json !== empty_json) {
2322
+ let reference_index = cell_style_map[style_as_json];
2323
+ if (typeof reference_index !== 'number') {
2324
+ cell_style_map[style_as_json] = reference_index = cell_style_refs.length;
2325
+ cell_style_refs.push(column[r]);
2326
+ }
2327
+ cell_reference_map[c][r] = reference_index;
2328
+ }
2329
+ }
2330
+ }
2331
+ }
2332
+ }
2333
+
2334
+ // it might be more efficient to store cell styles separately from
2335
+ // cell data, as we might be able to compress it. it looks more like
2336
+ // an indexed image, and we likely don't have that many styles.
2337
+
2338
+ /**
2339
+ * this assumes that "empty" style is at index 0
2340
+ */
2341
+ const StyleToRef = (style: Style.Properties) => {
2342
+
2343
+ const style_as_json = JSON.stringify(style);
2344
+ if (style_as_json === empty_json) {
2345
+ return 0;
2346
+ }
2347
+
2348
+ let reference_index = cell_style_map[style_as_json];
2349
+ if (typeof reference_index !== 'number') {
2350
+ cell_style_map[style_as_json] = reference_index = cell_style_refs.length;
2351
+ cell_style_refs.push(style);
2352
+ }
2353
+
2354
+ return reference_index;
2355
+
2356
+ };
2357
+
2358
+ // ensure we're not linked
2359
+ cell_style_refs = JSON.parse(JSON.stringify(cell_style_refs));
2360
+
2361
+ // same here (note broken naming)
2362
+ const sheet_style = JSON.parse(JSON.stringify(this.sheet_style));
2363
+ // const row_style = JSON.parse(JSON.stringify(this.row_styles));
2364
+ // const column_style = JSON.parse(JSON.stringify(this.column_styles));
2365
+ const row_pattern = JSON.parse(JSON.stringify(this.row_pattern));
2366
+
2367
+ // row and column styles are Record<number, props> and not arrays.
2368
+ // I think they should probably be arrays. it's not critical but
2369
+ // using records (objects) converts keys to strings, which is sloppy.
2370
+
2371
+
2372
+ // const column_style: Array<number|Style.Properties> = [];
2373
+ // const row_style: Array<number|Style.Properties> = [];
2374
+
2375
+ const column_style: Record<number, Style.Properties | number> = {};
2376
+ const row_style: Record<number, Style.Properties | number> = {};
2377
+
2378
+ for (const key of Object.keys(this.column_styles)) {
2379
+ const index = Number(key);
2380
+ const style = this.column_styles[index];
2381
+ if (style) {
2382
+ const reference = StyleToRef(style);
2383
+ if (reference) {
2384
+ column_style[index] = reference;
2385
+ }
2386
+ }
2387
+ }
2388
+
2389
+ for (const key of Object.keys(this.row_styles)) {
2390
+ const index = Number(key);
2391
+ const style = this.row_styles[index];
2392
+ if (style) {
2393
+ const reference = StyleToRef(style);
2394
+ if (reference) {
2395
+ row_style[index] = reference;
2396
+ }
2397
+ }
2398
+ }
2399
+
2400
+ const translate_border_color = (color: string | undefined, default_color: string | undefined): string | undefined => {
2401
+ if (typeof color !== 'undefined' && color !== 'none') {
2402
+ if (color === default_color) {
2403
+ return undefined;
2404
+ }
2405
+ else {
2406
+ return Measurement.MeasureColorARGB(color);
2407
+ }
2408
+ }
2409
+ return undefined;
2410
+ }
2411
+
2412
+ const translate_border_fill = (color: Style.Color = {}, default_color: Style.Color = {}) => {
2413
+ const result: Style.Color = {
2414
+ ...default_color,
2415
+ ...color,
2416
+ };
2417
+ if (result.text) {
2418
+ result.text = Measurement.MeasureColorARGB(result.text);
2419
+ return result;
2420
+ }
2421
+ else if (typeof result.theme === 'number') {
2422
+ return result;
2423
+ }
2424
+ return undefined;
2425
+ };
2426
+
2427
+ // translate, if necessary
2428
+ if (options.export_colors) {
2429
+ const style_list: Style.Properties[] = [];
2430
+ for (const group of [
2431
+ //row_style, column_style, // these are moved -> csr (which should be renamed)
2432
+ cell_style_refs, [sheet_style], row_pattern]) {
2433
+ if (Array.isArray(group)) {
2434
+ for (const entry of group) style_list.push(entry);
2435
+ }
2436
+ else {
2437
+ for (const key of Object.keys(group)) style_list.push(group[key]);
2438
+ }
2439
+ }
2440
+
2441
+ for (const style of style_list as Style.Properties[]) {
2442
+
2443
+ // don't set "undefined" overrides. also, was this broken
2444
+ // wrt all the defaults from top? probably
2445
+
2446
+ let fill = translate_border_fill(style.border_top_fill, Style.DefaultProperties.border_top_fill);
2447
+ if (fill !== undefined) { style.border_top_fill = fill; }
2448
+
2449
+ fill = translate_border_fill(style.border_left_fill, Style.DefaultProperties.border_left_fill);
2450
+ if (fill !== undefined) { style.border_left_fill = fill; }
2451
+
2452
+ fill = translate_border_fill(style.border_right_fill, Style.DefaultProperties.border_right_fill);
2453
+ if (fill !== undefined) { style.border_right_fill = fill; }
2454
+
2455
+ fill = translate_border_fill(style.border_bottom_fill, Style.DefaultProperties.border_bottom_fill);
2456
+ if (fill !== undefined) { style.border_bottom_fill = fill; }
2457
+
2458
+ if (style.fill?.text) {
2459
+ style.fill.text = Measurement.MeasureColorARGB(style.fill.text);
2460
+ }
2461
+
2462
+ //if (typeof style.background !== 'undefined' && style.background !== 'none') {
2463
+ // style.background = Measurement.MeasureColorARGB(style.background);
2464
+ //}
2465
+
2466
+ if (style.text) {
2467
+ if (style.text.text && style.text.text !== 'none') {
2468
+ style.text.text = Measurement.MeasureColorARGB(style.text.text);
2469
+ }
2470
+ }
2471
+
2472
+ }
2473
+ }
2474
+
2475
+ // FIXME: flatten row/column styles too
2476
+
2477
+ // flatten data -- also remove unecessary fields (FIXME: you might
2478
+ // keep rendered data, so it doesn't have to do work on initial render?)
2479
+
2480
+ const serialization_options: CellSerializationOptions = {
2481
+ calculated_value: !!options.rendered_values,
2482
+ preserve_type: !!options.preserve_type,
2483
+ expand_arrays: !!options.expand_arrays,
2484
+ decorated_cells: !!options.decorated_cells,
2485
+ nested: true,
2486
+ cell_style_refs: cell_reference_map,
2487
+ tables: !!options.tables,
2488
+ };
2489
+
2490
+ // the rows/columns we export can be shrunk to the actual used area,
2491
+ // subject to serialization option.
2492
+
2493
+ const serialized_data = this.cells.toJSON(serialization_options);
2494
+ const data = serialized_data.data;
2495
+
2496
+ let { rows, columns } = serialized_data;
2497
+
2498
+ if (!options.shrink) {
2499
+ rows = this.rows;
2500
+ columns = this.columns;
2501
+ }
2502
+ else {
2503
+
2504
+ // pad by 1 (2?)
2505
+
2506
+ rows += 2;
2507
+ columns += 1;
2508
+
2509
+ }
2510
+
2511
+ // push out for annotations
2512
+
2513
+ for (const annotation of this.annotations) {
2514
+ if (!annotation.extent) {
2515
+ this.CalculateAnnotationExtent(annotation);
2516
+ }
2517
+ if (annotation.extent) {
2518
+ rows = Math.max(rows, annotation.extent.row + 1);
2519
+ columns = Math.max(columns, annotation.extent.column + 1);
2520
+ }
2521
+ }
2522
+
2523
+ // (3) (style) for anything that hasn't been consumed, create a
2524
+ // cell style map. FIXME: optional [?]
2525
+
2526
+ /*
2527
+ const cell_styles: Array<{ row: number; column: number; ref: number }> = [];
2528
+
2529
+ for (let c = 0; c < cell_reference_map.length; c++) {
2530
+ const column = cell_reference_map[c];
2531
+ if (column) {
2532
+ for (let r = 0; r < column.length; r++) {
2533
+ if (column[r]) {
2534
+ cell_styles.push({ row: r, column: c, ref: column[r] });
2535
+ }
2536
+ }
2537
+ }
2538
+ }
2539
+
2540
+ const CS2 = this.CompressCellStyles(cell_reference_map);
2541
+ console.info({cs1: JSON.stringify(cell_styles), cs2: JSON.stringify(CS2)});
2542
+ */
2543
+
2544
+ // using blocks. this is our naive method. we could do (at minimum)
2545
+ // testing row-dominant vs column-dominant and see which is better;
2546
+ // but that kind of thing adds time, so it should be optional.
2547
+
2548
+ const cell_styles = this.CompressCellStyles(cell_reference_map);
2549
+
2550
+ const result: SerializedSheet = {
2551
+
2552
+ // not used atm, but in the event we need to gate
2553
+ // or swap importers on versions in the future
2554
+
2555
+ // FIXME: drop, in favor of container versioning. there's no point
2556
+ // in this submodule versioning (is there? ...)
2557
+
2558
+ // version: (ModuleInfo as any).version,
2559
+
2560
+ id: this.id,
2561
+ name: this.name,
2562
+
2563
+ data,
2564
+ sheet_style,
2565
+ rows,
2566
+ columns,
2567
+ cell_styles,
2568
+ styles: cell_style_refs,
2569
+ row_style,
2570
+ column_style,
2571
+
2572
+ row_pattern: row_pattern.length ? row_pattern : undefined,
2573
+
2574
+ // why are these serialized? (...) export!
2575
+
2576
+ default_row_height: this.default_row_height,
2577
+ default_column_width: this.default_column_width,
2578
+
2579
+ row_height: flatten_numeric_array(this.row_height_, this.default_row_height),
2580
+ column_width: flatten_numeric_array(this.column_width_, this.default_column_width),
2581
+
2582
+ selection: JSON.parse(JSON.stringify(this.selection)),
2583
+ annotations: JSON.parse(JSON.stringify(this.annotations)),
2584
+
2585
+ };
2586
+
2587
+ // omit default (true)
2588
+ if (!this.visible) {
2589
+ result.visible = this.visible;
2590
+ }
2591
+
2592
+ if (this.scroll_offset.x || this.scroll_offset.y) {
2593
+ result.scroll = this.scroll_offset;
2594
+ }
2595
+
2596
+ if (this.background_image) {
2597
+ result.background_image = this.background_image;
2598
+ }
2599
+
2600
+ // moved to outer container (data model)
2601
+
2602
+ /*
2603
+ // omit if empty
2604
+
2605
+ if (this.named_ranges.Count()) {
2606
+ result.named_ranges = JSON.parse(JSON.stringify(this.named_ranges.Map()));
2607
+ }
2608
+ */
2609
+
2610
+ // only put in freeze if used
2611
+
2612
+ if (this.freeze.rows || this.freeze.columns) {
2613
+ result.freeze = this.freeze;
2614
+ }
2615
+
2616
+ return result;
2617
+ }
2618
+
2619
+ /*
2620
+ * export values and calcualted values; as for csv export (which is what it's for) * /
2621
+ public ExportValueData(transpose = false, dates_as_strings = false, export_functions = false): CellValue[][] {
2622
+
2623
+ const arr: CellValue[][] = [];
2624
+ const data = this.cells.data;
2625
+
2626
+ if (transpose) {
2627
+ const rowcount = data[0].length; // assuming it's a rectangle
2628
+ for (let r = 0; r < rowcount; r++) {
2629
+ const row: CellValue[] = [];
2630
+ for (const column of data) {
2631
+ const ref = column[r];
2632
+ let value: CellValue;
2633
+ if (!export_functions && typeof ref.calculated !== 'undefined') value = ref.calculated;
2634
+ else if (typeof ref.value === 'undefined') value = '';
2635
+ else value = ref.value;
2636
+
2637
+ if (dates_as_strings && ref.style && typeof value === 'number') {
2638
+ const format = NumberFormatCache.Get(ref.style.number_format || '');
2639
+ if (format.date_format) value = format.Format(value);
2640
+ }
2641
+
2642
+ // if (dates_as_strings && ref.style && ref.style.date && typeof value === 'number') {
2643
+ // value = Style.Format(ref.style, value);
2644
+ // }
2645
+ row.push(value);
2646
+ }
2647
+ arr.push(row);
2648
+ }
2649
+ }
2650
+ else {
2651
+ for (const column_ref of data) {
2652
+ const column: CellValue[] = [];
2653
+ for (const ref of column_ref) {
2654
+ let value: CellValue;
2655
+ if (!export_functions && typeof ref.calculated !== 'undefined') value = ref.calculated;
2656
+ else if (typeof ref.value === 'undefined') value = '';
2657
+ else value = ref.value;
2658
+
2659
+ if (dates_as_strings && ref.style && typeof value === 'number') {
2660
+ const format = NumberFormatCache.Get(ref.style.number_format || '');
2661
+ if (format.date_format) value = format.Format(value);
2662
+ }
2663
+
2664
+ // if (dates_as_strings && ref.style && ref.style.date && typeof value === 'number') {
2665
+ // value = Style.Format(ref.style, value);
2666
+ // }
2667
+ column.push(value);
2668
+ }
2669
+ arr.push(column);
2670
+ }
2671
+ }
2672
+
2673
+ return arr;
2674
+ }
2675
+ */
2676
+
2677
+ /** flushes ALL rendered styles and caches. made public for theme API */
2678
+ public FlushCellStyles(): void {
2679
+ this.style_map = [];
2680
+ this.style_json_map = [];
2681
+ this.cells.FlushCellStyles();
2682
+ }
2683
+
2684
+ public ImportData(data: ImportedSheetData): void {
2685
+
2686
+ const styles = data.styles;
2687
+
2688
+ // adding sheet style...
2689
+
2690
+ // 0 is implicitly just a general style
2691
+
2692
+ const sheet_style = data.sheet_style;
2693
+ if (sheet_style) {
2694
+ this.UpdateAreaStyle(
2695
+ new Area({ row: Infinity, column: Infinity }, { row: Infinity, column: Infinity }),
2696
+ styles[sheet_style]);
2697
+ }
2698
+
2699
+ // and column styles...
2700
+
2701
+ const column_styles = data.column_styles;
2702
+ if (column_styles) {
2703
+ for (let i = 0; i < column_styles.length; i++) {
2704
+
2705
+ // 0 is implicitly just a general style
2706
+
2707
+ if (column_styles[i]) {
2708
+ this.UpdateAreaStyle(new Area({ row: Infinity, column: i }, { row: Infinity, column: i }), styles[column_styles[i]]);
2709
+ }
2710
+ }
2711
+ }
2712
+
2713
+ // this.cells.FromJSON(cell_data);
2714
+ this.cells.FromJSON(data.cells);
2715
+ if (data.name) {
2716
+ this.name = data.name || '';
2717
+ }
2718
+
2719
+ // 0 is implicitly just a general style
2720
+
2721
+ const cs = this.cell_style;
2722
+ for (const info of data.cells) {
2723
+ if (info.style_ref) {
2724
+ if (!cs[info.column]) cs[info.column] = [];
2725
+ cs[info.column][info.row] = styles[info.style_ref];
2726
+ }
2727
+ }
2728
+
2729
+ for (let i = 0; i < data.column_widths.length; i++) {
2730
+ if (typeof data.column_widths[i] !== 'undefined') {
2731
+
2732
+ // OK this is unscaled, we are setting unscaled from source data
2733
+
2734
+ this.SetColumnWidth(i, data.column_widths[i]);
2735
+ }
2736
+ }
2737
+
2738
+ for (let i = 0; i < data.row_heights.length; i++) {
2739
+ if (typeof data.row_heights[i] !== 'undefined') {
2740
+
2741
+ // OK this is unscaled, we are setting unscaled from source data
2742
+
2743
+ this.SetRowHeight(i, data.row_heights[i]);
2744
+ }
2745
+ }
2746
+
2747
+ for (const annotation of data.annotations || []) {
2748
+ this.annotations.push(new Annotation(annotation));
2749
+ }
2750
+
2751
+ if (data.hidden) {
2752
+ this.visible = false;
2753
+ }
2754
+
2755
+ }
2756
+
2757
+ // --- protected ------------------------------------------------------------
2758
+
2759
+ /**
2760
+ * figure out the last row/column of the annotation. this
2761
+ * might set it to 0/0 if there's no rect, just make sure
2762
+ * that it gets cleared on layout changes.
2763
+ */
2764
+ protected CalculateAnnotationExtent(annotation: Annotation): void {
2765
+
2766
+ // this is much easier with layout, but we are leaving the old
2767
+ // coude to support older files -- OTOH, the layout will be created
2768
+ // at some point, we just need to make sure that happens before this
2769
+ // is called
2770
+
2771
+ if (annotation.layout) {
2772
+ annotation.extent = { ...annotation.layout.br.address };
2773
+ return;
2774
+ }
2775
+
2776
+ // 1000 here is just sanity check, it might be larger
2777
+ const sanity = 1000;
2778
+
2779
+ annotation.extent = { row: 0, column: 0 };
2780
+
2781
+ let right = annotation.rect?.right;
2782
+ if (right && this.default_column_width) { // also sanity check
2783
+ for (let i = 0; right >= 0 && i < sanity; i++) {
2784
+ right -= this.GetColumnWidth(i); // FIXME: check // it's ok, rect is scaled to unit
2785
+ if (right < 0) {
2786
+ annotation.extent.column = i;
2787
+ break;
2788
+ }
2789
+ }
2790
+ }
2791
+
2792
+ let bottom = annotation.rect?.bottom;
2793
+ if (bottom && this.default_row_height) {
2794
+ for (let i = 0; bottom >= 0 && i < sanity; i++) {
2795
+ bottom -= this.GetRowHeight(i); // FIXME: check // it's ok, rect is scaled to unit
2796
+ if (bottom < 0) {
2797
+ annotation.extent.row = i;
2798
+ break;
2799
+ }
2800
+ }
2801
+ }
2802
+
2803
+ }
2804
+
2805
+ /* *
2806
+ * when checking style properties, check falsy but not '' or 0
2807
+ * (also strict equivalence)
2808
+ * /
2809
+ protected StyleEquals(a: any, b: any): boolean {
2810
+ return a === b ||
2811
+ ((a === false || a === null || a === undefined)
2812
+ && (b === false || b === null || b === undefined));
2813
+ }
2814
+ */
2815
+
2816
+ /*
2817
+ protected Serialize() {
2818
+ return JSON.stringify(this);
2819
+ }
2820
+ */
2821
+
2822
+ /*
2823
+ protected Deserialize(data: SerializedSheet) {
2824
+ Sheet.FromJSON(data, this.default_style_properties, this);
2825
+
2826
+ // some overlap here... consolidate? actually, doesn't
2827
+ // fromJSON call flush styles? [A: sometimes...]
2828
+
2829
+ this.cells.FlushCachedValues();
2830
+ this.FlushCellStyles();
2831
+ }
2832
+ */
2833
+
2834
+ // --- private methods ------------------------------------------------------
2835
+
2836
+
2837
+ /**
2838
+ * update style properties. merge by default.
2839
+ *
2840
+ * this method will reverse-override properties, meaning if you have set (for
2841
+ * example) a cell style to bold, then you set the whole sheet to unbold, we
2842
+ * expect that the unbold style will control. instead of explicitly setting
2843
+ * the cell style, we go up the chain and remove any matching properties.
2844
+ */
2845
+ private UpdateSheetStyle(properties: Style.Properties, delta = true) {
2846
+
2847
+ this.sheet_style = Style.Merge(this.sheet_style, properties, delta);
2848
+
2849
+ // reverse-override...
2850
+
2851
+ // const keys = Object.keys(properties);
2852
+ const keys = Object.keys(properties) as Style.PropertyKeys[];
2853
+ // const keys = Object.keys(this.sheet_style) as Style.PropertyKeys[];
2854
+
2855
+ for (const style_column of this.cell_style) {
2856
+ if (style_column) {
2857
+ for (const style_ref of style_column) {
2858
+ if (style_ref) {
2859
+ keys.forEach((key) => delete style_ref[key]);
2860
+ }
2861
+ }
2862
+ }
2863
+ }
2864
+
2865
+ for (const index of Object.keys(this.row_styles)) {
2866
+ keys.forEach((key) => delete this.row_styles[index as unknown as number][key]);
2867
+ }
2868
+
2869
+ for (const index of Object.keys(this.column_styles)) {
2870
+ keys.forEach((key) => delete this.column_styles[index as unknown as number][key]);
2871
+ }
2872
+
2873
+ // FIXME: ROW PATTERN
2874
+
2875
+ this.FlushCellStyles(); // not targeted
2876
+
2877
+ }
2878
+
2879
+ /**
2880
+ * updates row properties. reverse-overrides cells (@see UpdateSheetStyle).
2881
+ *
2882
+ * we also need to ensure that the desired effect takes hold, meaning if
2883
+ * there's an overriding column property (columns have priority), we will
2884
+ * need to update the cell property to match the desired output.
2885
+ */
2886
+ private UpdateRowStyle(row: number, properties: Style.Properties, delta = true) {
2887
+
2888
+ this.row_styles[row] = Style.Merge(this.row_styles[row] || {}, properties, delta);
2889
+
2890
+ // reverse-override... remove matching properties from cells in this row
2891
+ // (we can do this in-place)
2892
+
2893
+ // const keys = Object.keys(properties);
2894
+ const keys = Object.keys(properties) as Style.PropertyKeys[];
2895
+ // const keys = Object.keys(this.row_styles[row]) as Style.PropertyKeys[];
2896
+
2897
+ for (const column of this.cell_style) {
2898
+ if (column && column[row]) {
2899
+
2900
+ // FIXME: we don't want to delete. reverse-add.
2901
+ keys.forEach((key) => delete column[row][key]);
2902
+
2903
+ }
2904
+ }
2905
+
2906
+ /*
2907
+
2908
+ //
2909
+ // seems to be related to
2910
+ // https://github.com/microsoft/TypeScript/pull/30769
2911
+ //
2912
+ // not clear why the behavior should be different, but
2913
+ //
2914
+ // "indexed access with generics now works differently inside & outside a function."
2915
+ //
2916
+
2917
+ const FilteredAssign = <T>(test: T, source: T, target: T, keys: Array<keyof T>): void => {
2918
+ for (const key of keys) {
2919
+ if (test[key] !== undefined) {
2920
+ target[key] = source[key];
2921
+ }
2922
+ }
2923
+ };
2924
+ */
2925
+
2926
+ // if there's a column style, it will override the row
2927
+ // style; so we need to set a cell style to compensate.
2928
+
2929
+ // "override" because a reserved word in ts 4.3.2, possibly accidentally?
2930
+ // or possibly it was already a reserved word, and was handled incorrectly?
2931
+ // not sure. stop using it.
2932
+ //
2933
+ // Actually just by the by, if it does work as described in
2934
+ //
2935
+ // https://github.com/microsoft/TypeScript/issues/2000
2936
+ //
2937
+ // then we should start using it where appropriate, because it is good.
2938
+ // just don't use it here as a variable name.
2939
+
2940
+ for (let i = 0; i < this.cells.columns; i++) {
2941
+ if (this.column_styles[i]) {
2942
+ const column_style = this.column_styles[i];
2943
+ const overrides: Style.Properties = this.cell_style[i] ? this.cell_style[i][row] || {} : {};
2944
+
2945
+ for (const key of keys) {
2946
+ if (typeof column_style[key] !== 'undefined') {
2947
+ (overrides as any)[key] = properties[key];
2948
+ }
2949
+ }
2950
+
2951
+ if (Object.keys(overrides).length) {
2952
+ if (!this.cell_style[i]) this.cell_style[i] = [];
2953
+ this.cell_style[i][row] = JSON.parse(JSON.stringify(overrides));
2954
+ }
2955
+ }
2956
+ }
2957
+
2958
+ // FIXME: ROW PATTERN
2959
+
2960
+ this.cells.Apply(this.RealArea(Area.FromRow(row)), (cell) => cell.FlushStyle());
2961
+
2962
+ }
2963
+
2964
+ /* *
2965
+ * styles are applied as a stack,
2966
+ *
2967
+ * sheet
2968
+ * row pattern
2969
+ * row
2970
+ * column
2971
+ * cell
2972
+ *
2973
+ * there are some cases where we wind up with overridden but matching
2974
+ * styles that are duplicative. they can be removed, although it's not
2975
+ * necessarily useful to do it in real time -- we can do it on load/save
2976
+ * or perhaps on idle.
2977
+ *
2978
+ * /
2979
+ private FlattenStyles() {
2980
+
2981
+ this.CompositeStyleForCell
2982
+
2983
+ }
2984
+ */
2985
+
2986
+ /**
2987
+ * updates column properties. reverse-overrides cells (@see UpdateSheetStyle).
2988
+ */
2989
+ private UpdateColumnStyle(column: number, properties: Style.Properties, delta = true) {
2990
+
2991
+ this.column_styles[column] = Style.Merge(this.column_styles[column] || {}, properties, delta);
2992
+
2993
+ // returning to this function after a long time. so what this is doing
2994
+ // is removing unecessary properties from style objects higher in the
2995
+ // style chain, if those properties are overridden. note that this doesn't
2996
+ // seem to prune now-empty styles, which it probably should...
2997
+
2998
+ // in essence, we have a containing style object
2999
+ // { a: 1, c: 2 }
3000
+ //
3001
+ // then we iterate all cells in the column, and if there are any
3002
+ // matching properties they're deleted; so if a cell has
3003
+ // { a: 0, b: 1 }
3004
+ //
3005
+ // we drop the a property, so it becomes
3006
+ // { b: 1 }
3007
+ //
3008
+ // note you can drop and re-create the cell style object, because the cell's
3009
+ // reference is actually to a separate object (composited with the stack),
3010
+ // and the reference is cleared so the composite will be rebuilt when it's
3011
+ // needed next.
3012
+
3013
+ // NOTE this was broken anyway; it wasn't taking the merge into account...
3014
+ // ALTHOUGH that breaks "remove-color" operations. I think the old way
3015
+ // took into account that the styles would be relatively in sync already.
3016
+
3017
+ // reverse-override... I think we only need to override _cell_ values.
3018
+
3019
+ const keys = Object.keys(properties) as Style.PropertyKeys[];
3020
+ // const keys = Object.keys(this.column_styles[column]) as Style.PropertyKeys[];
3021
+
3022
+ if (this.cell_style[column]) {
3023
+ for (const ref of this.cell_style[column]) {
3024
+ if (ref) {
3025
+ // FIXME: we don't want to delete. reverse-add.
3026
+ keys.forEach((key) => delete ref[key]);
3027
+ }
3028
+ }
3029
+ }
3030
+
3031
+ this.cells.Apply(this.RealArea(Area.FromColumn(column)), (cell) => cell.FlushStyle());
3032
+
3033
+ // FIXME: ROW PATTERN
3034
+
3035
+ }
3036
+
3037
+ /**
3038
+ * generates the composite style for the given cell. this
3039
+ * should only be used to generate a cache of styles (Q: really? PERF?)
3040
+ *
3041
+ * the "apply_cell_style" parameter is used for testing when pruning. we
3042
+ * want to check what happens if the cell style is not applied; if nothing
3043
+ * happens, then we can drop the cell style (or the property in the style).
3044
+ */
3045
+ private CompositeStyleForCell(address: ICellAddress, apply_cell_style = true, apply_row_pattern = true, apply_default = true) {
3046
+
3047
+ const { row, column } = address;
3048
+ const stack: Style.Properties[] = [];
3049
+
3050
+ if (apply_default) {
3051
+ stack.push(this.default_style_properties);
3052
+ }
3053
+ stack.push(this.sheet_style);
3054
+
3055
+ if (apply_row_pattern && this.row_pattern.length) {
3056
+ stack.push(this.row_pattern[row % this.row_pattern.length]);
3057
+ }
3058
+
3059
+ if (this.row_styles[row]) {
3060
+ stack.push(this.row_styles[row]);
3061
+ }
3062
+
3063
+ if (this.column_styles[column]) {
3064
+ stack.push(this.column_styles[column]);
3065
+ }
3066
+
3067
+ if (apply_cell_style
3068
+ && this.cell_style[column]
3069
+ && this.cell_style[column][row]) {
3070
+ stack.push(this.cell_style[column][row]);
3071
+ }
3072
+
3073
+ return Style.Composite(stack);
3074
+ }
3075
+
3076
+ /**
3077
+ * can we use the rendered JSON as a key, instead?
3078
+ */
3079
+ private GetStyleIndex(style: Style.Properties) {
3080
+
3081
+ const json = JSON.stringify(style);
3082
+
3083
+ for (let i = 0; i < this.style_json_map.length; i++) {
3084
+ if (json === this.style_json_map[i]) return i; // match
3085
+ }
3086
+
3087
+ // ok we need to add it to the list. make sure to add a copy,
3088
+ // and add json to the json index.
3089
+
3090
+ const new_index = this.style_map.length;
3091
+ this.style_map.push(JSON.parse(json));
3092
+ this.style_json_map.push(json);
3093
+
3094
+ return new_index;
3095
+
3096
+ }
3097
+
3098
+ }
3099
+