@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.
- package/.eslintignore +8 -0
- package/.eslintrc.js +164 -0
- package/README-shadow-DOM.md +88 -0
- package/README.md +37 -130
- package/api-config.json +29 -0
- package/api-generator/api-generator-types.ts +82 -0
- package/api-generator/api-generator.ts +1172 -0
- package/api-generator/package.json +3 -0
- package/build/treb-spreadsheet.mjs +14 -0
- package/{treb.d.ts → build/treb.d.ts} +293 -299
- package/esbuild-custom-element.mjs +336 -0
- package/esbuild.js +305 -0
- package/package.json +43 -14
- package/treb-base-types/package.json +5 -0
- package/treb-base-types/src/api_types.ts +36 -0
- package/treb-base-types/src/area.ts +583 -0
- package/treb-base-types/src/basic_types.ts +45 -0
- package/treb-base-types/src/cell.ts +612 -0
- package/treb-base-types/src/cells.ts +1066 -0
- package/treb-base-types/src/color.ts +124 -0
- package/treb-base-types/src/import.ts +71 -0
- package/treb-base-types/src/index-standalone.ts +29 -0
- package/treb-base-types/src/index.ts +42 -0
- package/treb-base-types/src/layout.ts +47 -0
- package/treb-base-types/src/localization.ts +187 -0
- package/treb-base-types/src/rectangle.ts +145 -0
- package/treb-base-types/src/render_text.ts +72 -0
- package/treb-base-types/src/style.ts +545 -0
- package/treb-base-types/src/table.ts +109 -0
- package/treb-base-types/src/text_part.ts +54 -0
- package/treb-base-types/src/theme.ts +608 -0
- package/treb-base-types/src/union.ts +152 -0
- package/treb-base-types/src/value-type.ts +164 -0
- package/treb-base-types/style/resizable.css +59 -0
- package/treb-calculator/modern.tsconfig.json +11 -0
- package/treb-calculator/package.json +5 -0
- package/treb-calculator/src/calculator.ts +2546 -0
- package/treb-calculator/src/complex-math.ts +558 -0
- package/treb-calculator/src/dag/array-vertex.ts +198 -0
- package/treb-calculator/src/dag/graph.ts +951 -0
- package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
- package/treb-calculator/src/dag/vertex.ts +352 -0
- package/treb-calculator/src/descriptors.ts +162 -0
- package/treb-calculator/src/expression-calculator.ts +1069 -0
- package/treb-calculator/src/function-error.ts +103 -0
- package/treb-calculator/src/function-library.ts +103 -0
- package/treb-calculator/src/functions/base-functions.ts +1214 -0
- package/treb-calculator/src/functions/checkbox.ts +164 -0
- package/treb-calculator/src/functions/complex-functions.ts +253 -0
- package/treb-calculator/src/functions/finance-functions.ts +399 -0
- package/treb-calculator/src/functions/information-functions.ts +102 -0
- package/treb-calculator/src/functions/matrix-functions.ts +182 -0
- package/treb-calculator/src/functions/sparkline.ts +335 -0
- package/treb-calculator/src/functions/statistics-functions.ts +350 -0
- package/treb-calculator/src/functions/text-functions.ts +298 -0
- package/treb-calculator/src/index.ts +27 -0
- package/treb-calculator/src/notifier-types.ts +59 -0
- package/treb-calculator/src/primitives.ts +428 -0
- package/treb-calculator/src/utilities.ts +305 -0
- package/treb-charts/package.json +5 -0
- package/treb-charts/src/chart-functions.ts +156 -0
- package/treb-charts/src/chart-types.ts +230 -0
- package/treb-charts/src/chart.ts +1288 -0
- package/treb-charts/src/index.ts +24 -0
- package/treb-charts/src/main.ts +37 -0
- package/treb-charts/src/rectangle.ts +52 -0
- package/treb-charts/src/renderer.ts +1841 -0
- package/treb-charts/src/util.ts +122 -0
- package/treb-charts/style/charts.scss +221 -0
- package/treb-charts/style/old-charts.scss +250 -0
- package/treb-embed/markup/layout.html +137 -0
- package/treb-embed/markup/toolbar.html +175 -0
- package/treb-embed/modern.tsconfig.json +25 -0
- package/treb-embed/src/custom-element/content-types.d.ts +18 -0
- package/treb-embed/src/custom-element/global.d.ts +11 -0
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
- package/treb-embed/src/custom-element/treb-global.ts +44 -0
- package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
- package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
- package/treb-embed/src/index.ts +16 -0
- package/treb-embed/src/language-model.ts +41 -0
- package/treb-embed/src/options.ts +320 -0
- package/treb-embed/src/progress-dialog.ts +228 -0
- package/treb-embed/src/selection-state.ts +16 -0
- package/treb-embed/src/spinner.ts +42 -0
- package/treb-embed/src/toolbar-message.ts +96 -0
- package/treb-embed/src/types.ts +167 -0
- package/treb-embed/style/autocomplete.scss +103 -0
- package/treb-embed/style/dark-theme.scss +114 -0
- package/treb-embed/style/defaults.scss +36 -0
- package/treb-embed/style/dialog.scss +181 -0
- package/treb-embed/style/dropdown-select.scss +101 -0
- package/treb-embed/style/formula-bar.scss +193 -0
- package/treb-embed/style/grid.scss +374 -0
- package/treb-embed/style/layout.scss +424 -0
- package/treb-embed/style/mouse-mask.scss +67 -0
- package/treb-embed/style/note.scss +92 -0
- package/treb-embed/style/overlay-editor.scss +102 -0
- package/treb-embed/style/spinner.scss +92 -0
- package/treb-embed/style/tab-bar.scss +228 -0
- package/treb-embed/style/table.scss +80 -0
- package/treb-embed/style/theme-defaults.scss +444 -0
- package/treb-embed/style/toolbar.scss +416 -0
- package/treb-embed/style/tooltip.scss +68 -0
- package/treb-embed/style/treb-icons.scss +130 -0
- package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
- package/treb-embed/style/z-index.scss +43 -0
- package/treb-export/docs/charts.md +68 -0
- package/treb-export/modern.tsconfig.json +19 -0
- package/treb-export/package.json +4 -0
- package/treb-export/src/address-type.ts +77 -0
- package/treb-export/src/base-template.ts +22 -0
- package/treb-export/src/column-width.ts +85 -0
- package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
- package/treb-export/src/drawing2/chart2.ts +282 -0
- package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
- package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
- package/treb-export/src/drawing2/drawing2.ts +355 -0
- package/treb-export/src/drawing2/embedded-image.ts +71 -0
- package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
- package/treb-export/src/export-worker/export-worker.ts +99 -0
- package/treb-export/src/export-worker/index-modern.ts +22 -0
- package/treb-export/src/export2.ts +2204 -0
- package/treb-export/src/import2.ts +882 -0
- package/treb-export/src/relationship.ts +36 -0
- package/treb-export/src/shared-strings2.ts +128 -0
- package/treb-export/src/template-2.ts +22 -0
- package/treb-export/src/unescape_xml.ts +47 -0
- package/treb-export/src/workbook-sheet2.ts +182 -0
- package/treb-export/src/workbook-style2.ts +1285 -0
- package/treb-export/src/workbook-theme2.ts +88 -0
- package/treb-export/src/workbook2.ts +491 -0
- package/treb-export/src/xml-utils.ts +201 -0
- package/treb-export/template/base/[Content_Types].xml +2 -0
- package/treb-export/template/base/_rels/.rels +2 -0
- package/treb-export/template/base/docProps/app.xml +2 -0
- package/treb-export/template/base/docProps/core.xml +12 -0
- package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
- package/treb-export/template/base/xl/sharedStrings.xml +2 -0
- package/treb-export/template/base/xl/styles.xml +2 -0
- package/treb-export/template/base/xl/theme/theme1.xml +2 -0
- package/treb-export/template/base/xl/workbook.xml +2 -0
- package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
- package/treb-export/template/base.xlsx +0 -0
- package/treb-format/package.json +8 -0
- package/treb-format/src/format.test.ts +213 -0
- package/treb-format/src/format.ts +942 -0
- package/treb-format/src/format_cache.ts +199 -0
- package/treb-format/src/format_parser.ts +723 -0
- package/treb-format/src/index.ts +25 -0
- package/treb-format/src/number_format_section.ts +100 -0
- package/treb-format/src/value_parser.ts +337 -0
- package/treb-grid/package.json +5 -0
- package/treb-grid/src/editors/autocomplete.ts +394 -0
- package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
- package/treb-grid/src/editors/formula_bar.ts +473 -0
- package/treb-grid/src/editors/formula_editor_base.ts +910 -0
- package/treb-grid/src/editors/overlay_editor.ts +511 -0
- package/treb-grid/src/index.ts +37 -0
- package/treb-grid/src/layout/base_layout.ts +2618 -0
- package/treb-grid/src/layout/grid_layout.ts +299 -0
- package/treb-grid/src/layout/rectangle_cache.ts +86 -0
- package/treb-grid/src/render/selection-renderer.ts +414 -0
- package/treb-grid/src/render/svg_header_overlay.ts +93 -0
- package/treb-grid/src/render/svg_selection_block.ts +187 -0
- package/treb-grid/src/render/tile_renderer.ts +2122 -0
- package/treb-grid/src/types/annotation.ts +216 -0
- package/treb-grid/src/types/border_constants.ts +34 -0
- package/treb-grid/src/types/clipboard_data.ts +31 -0
- package/treb-grid/src/types/data_model.ts +334 -0
- package/treb-grid/src/types/drag_mask.ts +81 -0
- package/treb-grid/src/types/grid.ts +7743 -0
- package/treb-grid/src/types/grid_base.ts +3644 -0
- package/treb-grid/src/types/grid_command.ts +470 -0
- package/treb-grid/src/types/grid_events.ts +124 -0
- package/treb-grid/src/types/grid_options.ts +97 -0
- package/treb-grid/src/types/grid_selection.ts +60 -0
- package/treb-grid/src/types/named_range.ts +369 -0
- package/treb-grid/src/types/scale-control.ts +202 -0
- package/treb-grid/src/types/serialize_options.ts +72 -0
- package/treb-grid/src/types/set_range_options.ts +52 -0
- package/treb-grid/src/types/sheet.ts +3099 -0
- package/treb-grid/src/types/sheet_types.ts +95 -0
- package/treb-grid/src/types/tab_bar.ts +464 -0
- package/treb-grid/src/types/tile.ts +59 -0
- package/treb-grid/src/types/update_flags.ts +75 -0
- package/treb-grid/src/util/dom_utilities.ts +44 -0
- package/treb-grid/src/util/fontmetrics2.ts +179 -0
- package/treb-grid/src/util/ua.ts +104 -0
- package/treb-logo.svg +18 -0
- package/treb-parser/package.json +5 -0
- package/treb-parser/src/csv-parser.ts +122 -0
- package/treb-parser/src/index.ts +25 -0
- package/treb-parser/src/md-parser.ts +526 -0
- package/treb-parser/src/parser-types.ts +397 -0
- package/treb-parser/src/parser.test.ts +298 -0
- package/treb-parser/src/parser.ts +2673 -0
- package/treb-utils/package.json +5 -0
- package/treb-utils/src/dispatch.ts +57 -0
- package/treb-utils/src/event_source.ts +147 -0
- package/treb-utils/src/ievent_source.ts +33 -0
- package/treb-utils/src/index.ts +31 -0
- package/treb-utils/src/measurement.ts +174 -0
- package/treb-utils/src/resizable.ts +160 -0
- package/treb-utils/src/scale.ts +137 -0
- package/treb-utils/src/serialize_html.ts +124 -0
- package/treb-utils/src/template.ts +70 -0
- package/treb-utils/src/validate_uri.ts +61 -0
- package/tsconfig.json +10 -0
- package/tsproject.json +30 -0
- package/util/license-plugin-esbuild.js +86 -0
- package/util/list-css-vars.sh +46 -0
- package/README-esm.md +0 -37
- package/treb-bundle.css +0 -2
- 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
|
+
|