@trebco/treb 23.6.5 → 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} +285 -269
- 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,3644 @@
|
|
|
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
|
+
/**
|
|
23
|
+
* grid base is a superclass for grid that takes over all (most) of the
|
|
24
|
+
* data operations, leaving UI operations (painting and interacting, plus
|
|
25
|
+
* layout) in the grid subclass.
|
|
26
|
+
*
|
|
27
|
+
* this is part of an effort to support running outside of the browser,
|
|
28
|
+
* but still using the command log to handle deltas.
|
|
29
|
+
*
|
|
30
|
+
* this turns out to be a little like the (old) layout where we had modern
|
|
31
|
+
* and legacy layouts -- a lot of stuff can be reused, but a lot can't.
|
|
32
|
+
*
|
|
33
|
+
* calling this "grid" doesn't really make sense anymore, but we're not in
|
|
34
|
+
* a hurry to change it either.
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { EventSource } from 'treb-utils';
|
|
39
|
+
import type { DataModel, MacroFunction, SerializedModel, SerializedNamedExpression, ViewModel } from './data_model';
|
|
40
|
+
import { Parser, type ExpressionUnit, UnitAddress, IllegalSheetNameRegex } from 'treb-parser';
|
|
41
|
+
import { Area, Style, IsCellAddress, ValidationType, ValueType, Table, TableSortOptions, DefaultTableSortOptions, TableTheme } from 'treb-base-types';
|
|
42
|
+
import type { ICellAddress, IArea, Cell, CellValue } from 'treb-base-types';
|
|
43
|
+
import { Sheet } from './sheet';
|
|
44
|
+
import { AutocompleteMatcher, FunctionDescriptor, DescriptorType } from '../editors/autocomplete_matcher';
|
|
45
|
+
import { NumberFormat } from 'treb-format';
|
|
46
|
+
|
|
47
|
+
import { ErrorCode, GridEvent } from './grid_events';
|
|
48
|
+
import type { CommandRecord, DataValidationCommand, DuplicateSheetCommand, FreezeCommand, InsertColumnsCommand, InsertRowsCommand, ResizeColumnsCommand, ResizeRowsCommand, SelectCommand, SetRangeCommand, ShowSheetCommand, SortTableCommand } from './grid_command';
|
|
49
|
+
import { DefaultGridOptions, type GridOptions } from './grid_options';
|
|
50
|
+
import type { SerializeOptions } from './serialize_options';
|
|
51
|
+
|
|
52
|
+
import { BorderConstants } from './border_constants';
|
|
53
|
+
|
|
54
|
+
import { CommandKey } from './grid_command';
|
|
55
|
+
import type { Command, ActivateSheetCommand,
|
|
56
|
+
DeleteSheetCommand, UpdateBordersCommand, SheetSelection } from './grid_command';
|
|
57
|
+
import type { UpdateFlags } from './update_flags';
|
|
58
|
+
import type { LegacySerializedSheet } from './sheet_types';
|
|
59
|
+
import type { Annotation } from './annotation';
|
|
60
|
+
import type { ClipboardCellData } from './clipboard_data';
|
|
61
|
+
|
|
62
|
+
export class GridBase {
|
|
63
|
+
|
|
64
|
+
// --- public members --------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/** events */
|
|
67
|
+
public grid_events = new EventSource<GridEvent>();
|
|
68
|
+
|
|
69
|
+
/** for recording */
|
|
70
|
+
public command_log = new EventSource<CommandRecord>();
|
|
71
|
+
|
|
72
|
+
public readonly model: DataModel;
|
|
73
|
+
|
|
74
|
+
public readonly view: ViewModel;
|
|
75
|
+
|
|
76
|
+
// --- public accessors ------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
public get active_sheet(): Sheet {
|
|
79
|
+
return this.view.active_sheet;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public set active_sheet(sheet: Sheet) {
|
|
83
|
+
this.view.active_sheet = sheet;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** access the view index, if needed */
|
|
87
|
+
public get view_index() {
|
|
88
|
+
return this.view.view_index;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- protected members -----------------------------------------------------
|
|
92
|
+
|
|
93
|
+
protected batch = false;
|
|
94
|
+
|
|
95
|
+
protected batch_events: GridEvent[] = [];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* single instance of AC. editors (function bar, ICE) have references.
|
|
99
|
+
* this is in base, instead of subclass, because we use it to check
|
|
100
|
+
* for valid names.
|
|
101
|
+
*/
|
|
102
|
+
protected autocomplete_matcher = new AutocompleteMatcher();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* flags/state (used for some recordkeeping -- not super important)
|
|
106
|
+
*/
|
|
107
|
+
protected flags: Record<string, boolean> = {};
|
|
108
|
+
|
|
109
|
+
/** */
|
|
110
|
+
protected options: GridOptions;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* spreadsheet language parser. used to pull out address
|
|
114
|
+
* references from functions, for highlighting
|
|
115
|
+
*
|
|
116
|
+
* ...
|
|
117
|
+
*
|
|
118
|
+
* it's used for lots of stuff now, in addition to highlighting.
|
|
119
|
+
* copy/paste with translation; csv; defines; and some other stuff.
|
|
120
|
+
* still would like to share w/ parent though, if possible.
|
|
121
|
+
*
|
|
122
|
+
*
|
|
123
|
+
* FIXME: need a way to share/pass parser flags
|
|
124
|
+
* UPDATE: sharing parser w/ owner (embedded sheet)
|
|
125
|
+
*/
|
|
126
|
+
protected parser: Parser;
|
|
127
|
+
|
|
128
|
+
// --- constructor -----------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
options: GridOptions = {},
|
|
132
|
+
parser: Parser,
|
|
133
|
+
model: DataModel) {
|
|
134
|
+
|
|
135
|
+
this.model = model;
|
|
136
|
+
|
|
137
|
+
this.view = {
|
|
138
|
+
active_sheet: this.model.sheets.list[0],
|
|
139
|
+
view_index: this.model.view_count++,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// shared parser
|
|
143
|
+
|
|
144
|
+
this.parser = parser;
|
|
145
|
+
|
|
146
|
+
// apply default options, meaning that you need to explicitly set/unset
|
|
147
|
+
// in order to change behavior. FIXME: this is ok for flat structure, but
|
|
148
|
+
// anything more complicated will need a nested merge
|
|
149
|
+
|
|
150
|
+
this.options = { ...DefaultGridOptions, ...options };
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- API methods -----------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/** remove a table. doesn't remove any data, just removes the overlay. */
|
|
157
|
+
public RemoveTable(table: Table) {
|
|
158
|
+
this.ExecCommand({
|
|
159
|
+
key: CommandKey.RemoveTable,
|
|
160
|
+
table,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* create a table in the given area. the area cannot contain any
|
|
166
|
+
* merge cells, arrays, or be part of another table. if you add a table
|
|
167
|
+
* with a totals row, we don't insert a new row -- allocate enough space
|
|
168
|
+
* when you create it.
|
|
169
|
+
*
|
|
170
|
+
* @param area - the total area for the table, including headers and totals
|
|
171
|
+
* @param totals - set true to include a totals row. tables have different
|
|
172
|
+
* formatting and slightly different behavior when there's a totals row.
|
|
173
|
+
*/
|
|
174
|
+
public InsertTable(area: IArea, totals = true, sortable: boolean|undefined = undefined, theme?: TableTheme) {
|
|
175
|
+
|
|
176
|
+
// we should validate here, so that we can throw.
|
|
177
|
+
|
|
178
|
+
if (!area.start.sheet_id) {
|
|
179
|
+
area.start.sheet_id = this.active_sheet.id;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const sheet = this.FindSheet(area);
|
|
183
|
+
|
|
184
|
+
for (let row = area.start.row; row <= area.end.row; row++) {
|
|
185
|
+
for (let column = area.start.column; column <= area.end.column; column++) {
|
|
186
|
+
const cell = sheet.cells.GetCell({row, column}, false);
|
|
187
|
+
if (cell && (cell.area || cell.merge_area || cell.table)) {
|
|
188
|
+
// throw new Error('invalid area for table');
|
|
189
|
+
this.Error(ErrorCode.invalid_area_for_table);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.ExecCommand({
|
|
196
|
+
key: CommandKey.InsertTable,
|
|
197
|
+
area: JSON.parse(JSON.stringify(area)),
|
|
198
|
+
totals,
|
|
199
|
+
sortable,
|
|
200
|
+
theme,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* sort table. column is absolute.
|
|
207
|
+
*/
|
|
208
|
+
public SortTable(table: Table, options: Partial<TableSortOptions> = {}) {
|
|
209
|
+
|
|
210
|
+
//
|
|
211
|
+
// table typically has an actual area, while we want a plain
|
|
212
|
+
// object in the command queue for serialization purposes. not
|
|
213
|
+
// sure how we wound up with this situation, it's problematic.
|
|
214
|
+
//
|
|
215
|
+
|
|
216
|
+
this.ExecCommand({
|
|
217
|
+
key: CommandKey.SortTable,
|
|
218
|
+
table: JSON.parse(JSON.stringify(table)),
|
|
219
|
+
...DefaultTableSortOptions,
|
|
220
|
+
...options,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* filter table. what this means is "show the rows that match the filter
|
|
227
|
+
* and hide the other rows". it doesn't actually change data, but it does
|
|
228
|
+
* show/hide rows which (now) has some data effects.
|
|
229
|
+
*
|
|
230
|
+
* note that we don't pass the filter command through the command queue.
|
|
231
|
+
* it uses a callback, so that would not work. rather we filter first,
|
|
232
|
+
* then send hide/show row commands through the command queue. that will
|
|
233
|
+
* propagate updates.
|
|
234
|
+
*/
|
|
235
|
+
public FilterTable(table: Table, column: number, filter: (cell: Cell) => boolean) {
|
|
236
|
+
|
|
237
|
+
const command: Command[] = [];
|
|
238
|
+
|
|
239
|
+
if (!table.area.start.sheet_id) {
|
|
240
|
+
throw new Error('invalid table area');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sheet = this.model.sheets.Find(table.area.start.sheet_id);
|
|
244
|
+
if (!sheet) {
|
|
245
|
+
throw new Error('invalid table sheet');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const show_rows: number[] = [];
|
|
249
|
+
const hide_rows: number[] = [];
|
|
250
|
+
|
|
251
|
+
const end = table.totals_row ? table.area.end.row - 1 : table.area.end.row;
|
|
252
|
+
|
|
253
|
+
column += table.area.start.column;
|
|
254
|
+
for (let row = table.area.start.row + 1; row <= end; row++) {
|
|
255
|
+
const cell = sheet.CellData({row, column});
|
|
256
|
+
const show = filter(cell);
|
|
257
|
+
const current = sheet.GetRowHeight(row);
|
|
258
|
+
|
|
259
|
+
if (show && !current) {
|
|
260
|
+
show_rows.push(row);
|
|
261
|
+
}
|
|
262
|
+
else if (!show && current) {
|
|
263
|
+
hide_rows.push(row);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (show_rows) {
|
|
269
|
+
command.push({
|
|
270
|
+
key: CommandKey.ResizeRows,
|
|
271
|
+
sheet_id: sheet.id,
|
|
272
|
+
row: show_rows,
|
|
273
|
+
height: sheet.default_row_height,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (hide_rows) {
|
|
277
|
+
command.push({
|
|
278
|
+
key: CommandKey.ResizeRows,
|
|
279
|
+
sheet_id: sheet.id,
|
|
280
|
+
row: hide_rows,
|
|
281
|
+
height: 0,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (command.length) {
|
|
286
|
+
this.ExecCommand(command);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* UpdateSheets means "set these as the sheets, drop any old stuff". there's
|
|
293
|
+
* an implicit reset (in fact we may do that twice in some cases).
|
|
294
|
+
*
|
|
295
|
+
* this is non-UI; specialization should handle the UI part
|
|
296
|
+
*/
|
|
297
|
+
public UpdateSheets(data: LegacySerializedSheet[], render = false, activate_sheet?: number | string): void {
|
|
298
|
+
|
|
299
|
+
Sheet.Reset(); // reset ID generation
|
|
300
|
+
|
|
301
|
+
const sheets = data.map((sheet) => Sheet.FromJSON(sheet, this.model.theme_style_properties));
|
|
302
|
+
|
|
303
|
+
// ensure we have a sheets[0] so we can set active
|
|
304
|
+
|
|
305
|
+
if (sheets.length === 0) {
|
|
306
|
+
sheets.push(Sheet.Blank(this.model.theme_style_properties));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// now assign sheets
|
|
310
|
+
|
|
311
|
+
this.model.sheets.Assign(sheets);
|
|
312
|
+
this.ResetMetadata(); // FIXME: shouldn't we just set metadata from the file?
|
|
313
|
+
|
|
314
|
+
// set active
|
|
315
|
+
|
|
316
|
+
this.active_sheet = sheets[0];
|
|
317
|
+
|
|
318
|
+
// possibly set an active sheet on load (shortcut)
|
|
319
|
+
// could we not use a command for this?
|
|
320
|
+
|
|
321
|
+
if (activate_sheet) {
|
|
322
|
+
const sheet = this.model.sheets.Find(activate_sheet);
|
|
323
|
+
if (sheet) {
|
|
324
|
+
this.active_sheet = sheet;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// NOTE: we're not handling annotations here. do we need to? (...)
|
|
329
|
+
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* set functions for AC matcher. should be called by calculator on init,
|
|
335
|
+
* or when any functions are added/removed.
|
|
336
|
+
*
|
|
337
|
+
* FIXME: we should use this to normalize function names, on insert and
|
|
338
|
+
* on paste (if we're doing that).
|
|
339
|
+
*
|
|
340
|
+
* FIXME: are named expressions included here? (this function predates
|
|
341
|
+
* named expressions).
|
|
342
|
+
*
|
|
343
|
+
*
|
|
344
|
+
* this moved to grid base because we use the list to check for conflicts
|
|
345
|
+
* when setting names.
|
|
346
|
+
*
|
|
347
|
+
*/
|
|
348
|
+
public SetAutocompleteFunctions(functions: FunctionDescriptor[]): void {
|
|
349
|
+
|
|
350
|
+
// why does iterable support forEach but not map?
|
|
351
|
+
|
|
352
|
+
const expressions: FunctionDescriptor[] = [];
|
|
353
|
+
for (const name of this.model.named_expressions.keys()) {
|
|
354
|
+
expressions.push({
|
|
355
|
+
name, type: DescriptorType.Function,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const consolidated = functions.slice(0).concat(
|
|
360
|
+
this.model.named_ranges.List().map((named_range) => {
|
|
361
|
+
return { name: named_range.name, type: DescriptorType.Token };
|
|
362
|
+
}),
|
|
363
|
+
expressions,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
//this.autocomplete_matcher.SetFunctions(functions);
|
|
367
|
+
this.autocomplete_matcher.SetFunctions(consolidated);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public ResetMetadata(): void {
|
|
371
|
+
this.model.document_name = undefined;
|
|
372
|
+
this.model.user_data = undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* serialize data. this function used to (optionally) stringify
|
|
377
|
+
* by typescript has a problem figuring this out, so we will simplify
|
|
378
|
+
* the function.
|
|
379
|
+
*/
|
|
380
|
+
public Serialize(options: SerializeOptions = {}): SerializedModel {
|
|
381
|
+
|
|
382
|
+
// (removed UI stuff, that goes in subclass)
|
|
383
|
+
|
|
384
|
+
// selection moved to sheet, but it's not "live"; so we need to
|
|
385
|
+
// capture the primary selection in the current active sheet before
|
|
386
|
+
// we serialize it
|
|
387
|
+
|
|
388
|
+
// this.active_sheet.selection = JSON.parse(JSON.stringify(this.primary_selection));
|
|
389
|
+
|
|
390
|
+
// same for scroll offset
|
|
391
|
+
|
|
392
|
+
// this.active_sheet.scroll_offset = this.layout.scroll_offset;
|
|
393
|
+
|
|
394
|
+
// NOTE: annotations moved to sheets, they will be serialized in the sheets
|
|
395
|
+
|
|
396
|
+
const sheet_data = this.model.sheets.list.map((sheet) => sheet.toJSON(options));
|
|
397
|
+
|
|
398
|
+
// OK, not serializing tables in cells anymore. old comment about this:
|
|
399
|
+
//
|
|
400
|
+
// at the moment, tables are being serialized in cells. if we put them
|
|
401
|
+
// in here, then we have two records of the same data. that would be bad.
|
|
402
|
+
// I think this is probably the correct place, but if we put them here
|
|
403
|
+
// we need to stop serializing in cells. and I'm not sure that there are
|
|
404
|
+
// not some side-effects to that. hopefully not, but (...)
|
|
405
|
+
//
|
|
406
|
+
|
|
407
|
+
let tables: Table[] | undefined;
|
|
408
|
+
if (this.model.tables.size > 0) {
|
|
409
|
+
tables = Array.from(this.model.tables.values());
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// NOTE: moving into a structured object (the sheet data is also structured,
|
|
413
|
+
// of course) but we are moving things out of sheet (just named ranges atm))
|
|
414
|
+
|
|
415
|
+
let macro_functions: MacroFunction[] | undefined;
|
|
416
|
+
|
|
417
|
+
if (this.model.macro_functions.size) {
|
|
418
|
+
macro_functions = [];
|
|
419
|
+
for (const macro of this.model.macro_functions.values()) {
|
|
420
|
+
macro_functions.push({
|
|
421
|
+
...macro,
|
|
422
|
+
expression: undefined,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// when serializing named expressions, we have to make sure
|
|
428
|
+
// that there's a sheet name in any address/range.
|
|
429
|
+
|
|
430
|
+
const named_expressions: SerializedNamedExpression[] = [];
|
|
431
|
+
if (this.model.named_expressions) {
|
|
432
|
+
|
|
433
|
+
for (const [name, expr] of this.model.named_expressions) {
|
|
434
|
+
this.parser.Walk(expr, unit => {
|
|
435
|
+
if (unit.type === 'address' || unit.type === 'range') {
|
|
436
|
+
const test = unit.type === 'range' ? unit.start : unit;
|
|
437
|
+
|
|
438
|
+
test.absolute_column = test.absolute_row = true;
|
|
439
|
+
|
|
440
|
+
if (!test.sheet) {
|
|
441
|
+
if (test.sheet_id) {
|
|
442
|
+
const sheet = this.model.sheets.Find(test.sheet_id);
|
|
443
|
+
if (sheet) {
|
|
444
|
+
test.sheet = sheet.name;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!test.sheet) {
|
|
448
|
+
test.sheet = this.active_sheet.name;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (unit.type === 'range') {
|
|
453
|
+
unit.end.absolute_column = unit.end.absolute_row = true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return true;
|
|
459
|
+
});
|
|
460
|
+
const rendered = this.parser.Render(expr, { missing: '' });
|
|
461
|
+
named_expressions.push({
|
|
462
|
+
name, expression: rendered
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
sheet_data,
|
|
469
|
+
active_sheet: this.active_sheet.id,
|
|
470
|
+
named_ranges: this.model.named_ranges.Count() ?
|
|
471
|
+
this.model.named_ranges.Serialize() :
|
|
472
|
+
undefined,
|
|
473
|
+
macro_functions,
|
|
474
|
+
tables,
|
|
475
|
+
named_expressions: named_expressions.length ? named_expressions : undefined,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// --- protected methods -----------------------------------------------------
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* see ResizeRowsInternal
|
|
484
|
+
*/
|
|
485
|
+
protected ResizeColumnsInternal(command: ResizeColumnsCommand) {
|
|
486
|
+
|
|
487
|
+
const sheet = command.sheet_id ? this.FindSheet(command.sheet_id) : this.active_sheet;
|
|
488
|
+
|
|
489
|
+
// normalize
|
|
490
|
+
|
|
491
|
+
let column = command.column;
|
|
492
|
+
if (typeof column === 'undefined') {
|
|
493
|
+
column = [];
|
|
494
|
+
for (let i = 0; i < sheet.columns; i++) column.push(i);
|
|
495
|
+
}
|
|
496
|
+
if (typeof column === 'number') column = [column];
|
|
497
|
+
|
|
498
|
+
if (command.width) {
|
|
499
|
+
for (const entry of column) {
|
|
500
|
+
sheet.SetColumnWidth(entry, command.width);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
console.error('auto size not supported');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* resize rows. this supports auto size, but that will fail in !ui grid,
|
|
511
|
+
* because it uses HTML. also non-ui doesn't really need to worry about
|
|
512
|
+
* scale... we should split.
|
|
513
|
+
*/
|
|
514
|
+
protected ResizeRowsInternal(command: ResizeRowsCommand): IArea|undefined {
|
|
515
|
+
|
|
516
|
+
// we're guaranteed this now, we should have a way to represent that...
|
|
517
|
+
|
|
518
|
+
const sheet = command.sheet_id ? this.FindSheet(command.sheet_id) : this.active_sheet;
|
|
519
|
+
|
|
520
|
+
// normalize rows -> array. undefined means all rows.
|
|
521
|
+
|
|
522
|
+
let row = command.row;
|
|
523
|
+
if (typeof row === 'undefined') {
|
|
524
|
+
row = [];
|
|
525
|
+
for (let i = 0; i < sheet.rows; i++) row.push(i);
|
|
526
|
+
}
|
|
527
|
+
if (typeof row === 'number') row = [row];
|
|
528
|
+
|
|
529
|
+
// I guess this was intended to prevent auto-size, but what about 0?
|
|
530
|
+
|
|
531
|
+
if (command.height) {
|
|
532
|
+
for (const entry of row) {
|
|
533
|
+
sheet.SetRowHeight(entry, command.height);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.error('auto size not supported');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return undefined;
|
|
541
|
+
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
protected ResetInternal() {
|
|
545
|
+
|
|
546
|
+
Sheet.Reset();
|
|
547
|
+
this.UpdateSheets([], true);
|
|
548
|
+
this.model.named_ranges.Reset();
|
|
549
|
+
this.model.named_expressions.clear();
|
|
550
|
+
this.model.macro_functions.clear(); // = {};
|
|
551
|
+
this.model.tables.clear();
|
|
552
|
+
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
protected SetValidationInternal(command: DataValidationCommand): void {
|
|
556
|
+
|
|
557
|
+
let cell: Cell|undefined;
|
|
558
|
+
|
|
559
|
+
const sheet = this.FindSheet(command.area);
|
|
560
|
+
|
|
561
|
+
if (sheet) {
|
|
562
|
+
cell = sheet.cells.GetCell(command.area, true);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!cell) {
|
|
566
|
+
throw new Error('invalid cell in set validation');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (command.range) {
|
|
570
|
+
cell.validation = {
|
|
571
|
+
type: ValidationType.Range,
|
|
572
|
+
area: command.range,
|
|
573
|
+
error: !!command.error,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
else if (command.list) {
|
|
577
|
+
cell.validation = {
|
|
578
|
+
type: ValidationType.List,
|
|
579
|
+
list: JSON.parse(JSON.stringify(command.list)),
|
|
580
|
+
error: !!command.error,
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
cell.validation = undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* get values from a range of data
|
|
591
|
+
* @param area
|
|
592
|
+
*/
|
|
593
|
+
protected GetValidationRange(area: IArea): CellValue[]|undefined {
|
|
594
|
+
|
|
595
|
+
let list: CellValue[]|undefined;
|
|
596
|
+
|
|
597
|
+
const sheet = this.FindSheet(area);
|
|
598
|
+
|
|
599
|
+
if (sheet) {
|
|
600
|
+
|
|
601
|
+
list = [];
|
|
602
|
+
|
|
603
|
+
// clamp to actual area to avoid screwing up sheet
|
|
604
|
+
// FIXME: what does that cause [problem with selections], why, and fix it
|
|
605
|
+
|
|
606
|
+
area = sheet.RealArea(new Area(area.start, area.end), true);
|
|
607
|
+
|
|
608
|
+
for (let row = area.start.row; row <= area.end.row; row++) {
|
|
609
|
+
for (let column = area.start.column; column <= area.end.column; column++) {
|
|
610
|
+
const cell = sheet.CellData({row, column});
|
|
611
|
+
if (cell && cell.formatted) {
|
|
612
|
+
if (typeof cell.formatted === 'string') {
|
|
613
|
+
list.push(cell.formatted);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
list.push(NumberFormat.FormatPartsAsText(cell.formatted));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return list;
|
|
624
|
+
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* @returns true if we need a recalc, because references have broken.
|
|
631
|
+
*/
|
|
632
|
+
protected DeleteSheetInternal(command: DeleteSheetCommand): boolean {
|
|
633
|
+
|
|
634
|
+
let is_active = false;
|
|
635
|
+
let index = -1;
|
|
636
|
+
let target_name = '';
|
|
637
|
+
|
|
638
|
+
let requires_recalc = false;
|
|
639
|
+
|
|
640
|
+
// remove from array. check if this is the active sheet
|
|
641
|
+
|
|
642
|
+
const named_sheet = command.name ? command.name.toLowerCase() : '';
|
|
643
|
+
const sheets = this.model.sheets.list.filter((sheet, i) => {
|
|
644
|
+
if (i === command.index || sheet.id === command.id || sheet.name.toLowerCase() === named_sheet) {
|
|
645
|
+
is_active = (sheet === this.active_sheet);
|
|
646
|
+
|
|
647
|
+
this.model.named_ranges.RemoveRangesForSheet(sheet.id);
|
|
648
|
+
target_name = sheet.name;
|
|
649
|
+
|
|
650
|
+
index = i;
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
return true;
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// NOTE: we might want to remove references to this sheet. see
|
|
657
|
+
// how we patch references in insert columns/rows functions.
|
|
658
|
+
|
|
659
|
+
// actually note the logic we need is already in the rename sheet
|
|
660
|
+
// function; we just need to split it out from actually renaming the
|
|
661
|
+
// sheet, then we can use it
|
|
662
|
+
|
|
663
|
+
if (target_name) {
|
|
664
|
+
const count = this.RenameSheetReferences(sheets, target_name, '#REF');
|
|
665
|
+
if (count > 0) {
|
|
666
|
+
requires_recalc = true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// empty? create new, activate
|
|
671
|
+
// UPDATE: we also need to create if all remaining sheets are hidden
|
|
672
|
+
|
|
673
|
+
if (!sheets.length) {
|
|
674
|
+
sheets.push(Sheet.Blank(this.model.theme_style_properties));
|
|
675
|
+
index = 0;
|
|
676
|
+
}
|
|
677
|
+
else if (sheets.every(test => !test.visible)) {
|
|
678
|
+
|
|
679
|
+
// why insert at 0 here? shouldn't it still be last,
|
|
680
|
+
// even if all the others are empty?
|
|
681
|
+
|
|
682
|
+
sheets.unshift(Sheet.Blank(this.model.theme_style_properties));
|
|
683
|
+
index = 0;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
if (index >= sheets.length) {
|
|
687
|
+
index = 0;
|
|
688
|
+
}
|
|
689
|
+
while (!sheets[index].visible) {
|
|
690
|
+
index++;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// this.model.sheets = sheets;
|
|
695
|
+
this.model.sheets.Assign(sheets);
|
|
696
|
+
|
|
697
|
+
// need to activate a new sheet? use the next one (now in the slot
|
|
698
|
+
// we just removed). this will roll over properly if we're at the end.
|
|
699
|
+
|
|
700
|
+
// UPDATE: we need to make sure that the target is not hidden, or we
|
|
701
|
+
// can't activate it
|
|
702
|
+
|
|
703
|
+
if (is_active) {
|
|
704
|
+
// console.info('activate @', index);
|
|
705
|
+
this.ActivateSheetInternal({ key: CommandKey.ActivateSheet, index });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return requires_recalc;
|
|
709
|
+
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* rename a sheet. this requires changing any formulae that refer to the
|
|
715
|
+
* old name to refer to the new name. if there are any references by ID
|
|
716
|
+
* those don't have to change.
|
|
717
|
+
*
|
|
718
|
+
* FIXME: can we do this using the dependency graph? (...)
|
|
719
|
+
*/
|
|
720
|
+
protected RenameSheetInternal(target: Sheet, name: string) {
|
|
721
|
+
|
|
722
|
+
// validate name... ?
|
|
723
|
+
|
|
724
|
+
if (!name || IllegalSheetNameRegex.test(name)) {
|
|
725
|
+
throw new Error('invalid sheet name');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// also can't have two sheets with the same name
|
|
729
|
+
|
|
730
|
+
const compare = name.toLowerCase();
|
|
731
|
+
for (const sheet of this.model.sheets.list) {
|
|
732
|
+
if (sheet !== target && sheet.name.toLowerCase() === compare) {
|
|
733
|
+
throw new Error('sheet name already exists');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// function will LC the name
|
|
738
|
+
// const old_name = target.name.toLowerCase();
|
|
739
|
+
const old_name = target.name;
|
|
740
|
+
target.name = name;
|
|
741
|
+
|
|
742
|
+
// need to update indexes
|
|
743
|
+
this.model.sheets.Assign(this.model.sheets.list);
|
|
744
|
+
|
|
745
|
+
this.RenameSheetReferences(this.model.sheets.list, old_name, name);
|
|
746
|
+
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
*
|
|
751
|
+
*/
|
|
752
|
+
protected SortTableInternal(command: SortTableCommand): Area|undefined {
|
|
753
|
+
|
|
754
|
+
if (!command.table.area.start.sheet_id) {
|
|
755
|
+
throw new Error('table has invalid area');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const sheet = this.model.sheets.Find(command.table.area.start.sheet_id);
|
|
759
|
+
|
|
760
|
+
if (!sheet) {
|
|
761
|
+
throw new Error('invalid sheet in table area');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// I guess we're sorting on calculated value? seems weird.
|
|
765
|
+
|
|
766
|
+
// NOTE: only sort hidden rows... what to do with !hidden rows? do they
|
|
767
|
+
// get sorted anyway? [A: no, leave them as-is]
|
|
768
|
+
|
|
769
|
+
const ranked: Array<{
|
|
770
|
+
row: number;
|
|
771
|
+
text: string;
|
|
772
|
+
number: number;
|
|
773
|
+
type: ValueType;
|
|
774
|
+
data: ClipboardCellData[];
|
|
775
|
+
}> = [];
|
|
776
|
+
|
|
777
|
+
// get a list of visible table rows. that will be our insert map at the end
|
|
778
|
+
|
|
779
|
+
const visible: number[] = [];
|
|
780
|
+
|
|
781
|
+
let end = command.table.area.end.row;
|
|
782
|
+
if (command.table.totals_row) {
|
|
783
|
+
end--;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// for auto-sort
|
|
787
|
+
|
|
788
|
+
let text_count = 0;
|
|
789
|
+
let number_count = 0;
|
|
790
|
+
|
|
791
|
+
for (let row = command.table.area.start.row + 1; row <= end; row++) {
|
|
792
|
+
|
|
793
|
+
const height = sheet.GetRowHeight(row);
|
|
794
|
+
|
|
795
|
+
if (height) {
|
|
796
|
+
visible.push(row);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const row_data = {
|
|
803
|
+
row,
|
|
804
|
+
number: 0,
|
|
805
|
+
text: '',
|
|
806
|
+
type: ValueType.undefined,
|
|
807
|
+
data: [] as ClipboardCellData[],
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
for (let column = command.table.area.start.column; column <= command.table.area.end.column; column++) {
|
|
811
|
+
|
|
812
|
+
const cd = sheet.CellData({row, column});
|
|
813
|
+
|
|
814
|
+
// sort column is relative to table
|
|
815
|
+
|
|
816
|
+
if (column === command.column + command.table.area.start.column) {
|
|
817
|
+
|
|
818
|
+
const check_type = cd.calculated_type || cd.type;
|
|
819
|
+
if (check_type === ValueType.string) {
|
|
820
|
+
text_count++;
|
|
821
|
+
}
|
|
822
|
+
else if (check_type === ValueType.number) {
|
|
823
|
+
number_count++;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// we can precalculate the type for sorting
|
|
827
|
+
|
|
828
|
+
const value = cd.calculated_type ? cd.calculated : cd.value;
|
|
829
|
+
row_data.text = value?.toString() || '';
|
|
830
|
+
row_data.number = Number(value) || 0;
|
|
831
|
+
row_data.type = cd.calculated_type || cd.type;
|
|
832
|
+
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
row_data.data.push({
|
|
836
|
+
address: {row, column},
|
|
837
|
+
data: cd.value,
|
|
838
|
+
type: cd.type,
|
|
839
|
+
style: cd.style,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
ranked.push(row_data);
|
|
845
|
+
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// auto sort - default to text, unless we see more numbers
|
|
849
|
+
|
|
850
|
+
let sort_type = command.type;
|
|
851
|
+
if (sort_type === 'auto') {
|
|
852
|
+
if (number_count > text_count) {
|
|
853
|
+
sort_type = 'numeric';
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
sort_type = 'text';
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// console.info(visible, ranked);
|
|
861
|
+
|
|
862
|
+
// rank
|
|
863
|
+
|
|
864
|
+
const invert = command.asc ? 1 : -1;
|
|
865
|
+
|
|
866
|
+
switch (sort_type) {
|
|
867
|
+
case 'numeric':
|
|
868
|
+
ranked.sort((a, b) => {
|
|
869
|
+
if (a.type === ValueType.undefined) {
|
|
870
|
+
return ((b.type === ValueType.undefined) ? 0 : 1);
|
|
871
|
+
}
|
|
872
|
+
if (b.type === ValueType.undefined) {
|
|
873
|
+
return -1;
|
|
874
|
+
}
|
|
875
|
+
return (a.number - b.number) * invert;
|
|
876
|
+
});
|
|
877
|
+
break;
|
|
878
|
+
|
|
879
|
+
case 'text':
|
|
880
|
+
default:
|
|
881
|
+
ranked.sort((a, b) => {
|
|
882
|
+
if (a.type === ValueType.undefined) {
|
|
883
|
+
return ((b.type === ValueType.undefined) ? 0 : 1);
|
|
884
|
+
}
|
|
885
|
+
if (b.type === ValueType.undefined) {
|
|
886
|
+
return -1;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return a.text.localeCompare(b.text) * invert;
|
|
890
|
+
});
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// now apply the sort
|
|
895
|
+
|
|
896
|
+
const insert = {row: command.table.area.start.row + 1, column: command.table.area.start.column };
|
|
897
|
+
|
|
898
|
+
for (let i = 0; i < visible.length; i++) {
|
|
899
|
+
|
|
900
|
+
insert.row = visible[i];
|
|
901
|
+
const entry = ranked[i];
|
|
902
|
+
|
|
903
|
+
insert.column = command.table.area.start.column; // reset
|
|
904
|
+
for (const cell of entry.data) {
|
|
905
|
+
if (cell.type === ValueType.formula) {
|
|
906
|
+
|
|
907
|
+
let data = cell.data as string;
|
|
908
|
+
const offsets = { columns: 0, rows: insert.row - entry.row };
|
|
909
|
+
const parse_result = this.parser.Parse(data);
|
|
910
|
+
if (parse_result.expression) {
|
|
911
|
+
data = '=' + this.parser.Render(parse_result.expression, { offset: offsets, missing: ''});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
sheet.SetCellValue(insert, data);
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
sheet.SetCellValue(insert, cell.data);
|
|
918
|
+
}
|
|
919
|
+
sheet.UpdateCellStyle(insert, cell.style || {}, false);
|
|
920
|
+
insert.column++; // step
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// keep reference. we don't have the actual table, we have a copy.
|
|
926
|
+
// this is done because the command queue might be broadcast, so
|
|
927
|
+
// references won't work.
|
|
928
|
+
|
|
929
|
+
const ref = this.model.tables.get(command.table.name.toLowerCase());
|
|
930
|
+
if (ref) {
|
|
931
|
+
ref.sort = {
|
|
932
|
+
type: command.type,
|
|
933
|
+
asc: !!command.asc,
|
|
934
|
+
column: command.column,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// flush style in rows that don't change, to force repainting. this
|
|
939
|
+
// has to do with how table styles are overlaid on other styles; it's
|
|
940
|
+
// not optimal at the moment.
|
|
941
|
+
{
|
|
942
|
+
|
|
943
|
+
let row = command.table.area.start.row;
|
|
944
|
+
for (let column = command.table.area.start.column; column <= command.table.area.end.column; column++) {
|
|
945
|
+
sheet.cells.data[row][column]?.FlushStyle();
|
|
946
|
+
}
|
|
947
|
+
if (command.table.totals_row) {
|
|
948
|
+
row = command.table.area.end.row;
|
|
949
|
+
for (let column = command.table.area.start.column; column <= command.table.area.end.column; column++) {
|
|
950
|
+
sheet.cells.data[row][column]?.FlushStyle();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// console.info(ordered);
|
|
957
|
+
|
|
958
|
+
return new Area(command.table.area.start, command.table.area.end);
|
|
959
|
+
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* update all columns of a table (collect column names). this
|
|
964
|
+
* method rebuilds all columns; that's probably unecessary in
|
|
965
|
+
* many cases, but we'll start here and we can drill down later.
|
|
966
|
+
*
|
|
967
|
+
* we do two things here: we normalize column header values, and
|
|
968
|
+
* we collect them for table headers.
|
|
969
|
+
*
|
|
970
|
+
* @param table
|
|
971
|
+
*/
|
|
972
|
+
protected UpdateTableColumns(table: Table): IArea {
|
|
973
|
+
|
|
974
|
+
if (!table.area.start.sheet_id) {
|
|
975
|
+
throw new Error('invalid area in table');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const sheet = this.model.sheets.Find(table.area.start.sheet_id);
|
|
979
|
+
if (!sheet) {
|
|
980
|
+
throw new Error('invalid sheet in table');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// this can get called when a document is loaded, we might
|
|
984
|
+
// not have column names when we start. but if we do, we will
|
|
985
|
+
// need to keep the old ones so we can check deltas.
|
|
986
|
+
|
|
987
|
+
const current_columns = table.columns?.slice(0) || undefined;
|
|
988
|
+
|
|
989
|
+
const columns: string[] = [];
|
|
990
|
+
|
|
991
|
+
const row = table.area.start.row;
|
|
992
|
+
const count = table.area.end.column - table.area.start.column + 1;
|
|
993
|
+
|
|
994
|
+
let column = table.area.start.column;
|
|
995
|
+
for (let i = 0; i < count; i++, column++) {
|
|
996
|
+
|
|
997
|
+
const header = sheet.CellData({row, column});
|
|
998
|
+
let value = '';
|
|
999
|
+
|
|
1000
|
+
if (header.type !== ValueType.string) {
|
|
1001
|
+
|
|
1002
|
+
if (typeof header.formatted !== 'undefined') {
|
|
1003
|
+
value = (header.formatted).toString();
|
|
1004
|
+
}
|
|
1005
|
+
else if (typeof header.calculated !== 'undefined') {
|
|
1006
|
+
value = (header.calculated).toString();
|
|
1007
|
+
}
|
|
1008
|
+
else if (typeof header.value !== 'undefined') {
|
|
1009
|
+
value = (header.value).toString();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
header.Set(value, ValueType.string);
|
|
1013
|
+
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
value = (header.value as string) || '';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (!value) {
|
|
1020
|
+
value = `Column${i + 1}`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
let proposed = value;
|
|
1024
|
+
let success = false;
|
|
1025
|
+
let index = 1;
|
|
1026
|
+
|
|
1027
|
+
while (!success) {
|
|
1028
|
+
success = true;
|
|
1029
|
+
inner_loop:
|
|
1030
|
+
for (const check of columns) {
|
|
1031
|
+
if (check.toLowerCase() === proposed.toLowerCase()) {
|
|
1032
|
+
success = false;
|
|
1033
|
+
proposed = `${value}${++index}`;
|
|
1034
|
+
break inner_loop;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
header.Set(proposed, ValueType.string);
|
|
1040
|
+
columns.push(proposed.toLowerCase());
|
|
1041
|
+
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// TODO: this is good, and works, but we are going to have to
|
|
1045
|
+
// look for structured references and update them if the column
|
|
1046
|
+
// names change.
|
|
1047
|
+
|
|
1048
|
+
if (current_columns) {
|
|
1049
|
+
|
|
1050
|
+
// if we are inserting/removing columns, we're probably
|
|
1051
|
+
// not changing names at the same time. on remove, some
|
|
1052
|
+
// references will break, but that's to be expected. on
|
|
1053
|
+
// insert, new columns will get added but we don't have
|
|
1054
|
+
// to change references.
|
|
1055
|
+
|
|
1056
|
+
if (current_columns.length === columns.length) {
|
|
1057
|
+
|
|
1058
|
+
const update: Map<string, string> = new Map();
|
|
1059
|
+
for (let i = 0; i < current_columns.length; i++) {
|
|
1060
|
+
const compare = current_columns[i].toLowerCase();
|
|
1061
|
+
if (compare !== columns[i]) {
|
|
1062
|
+
update.set(compare, columns[i]); // add old -> new
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (update.size) {
|
|
1067
|
+
|
|
1068
|
+
// OK, we need to update. we're iterating cells, then
|
|
1069
|
+
// updates, so we don't accidentally oscillate if we have
|
|
1070
|
+
// columns that swap names. going through once should
|
|
1071
|
+
// ensure that doesn't happen.
|
|
1072
|
+
|
|
1073
|
+
const table_name = table.name.toLowerCase();
|
|
1074
|
+
|
|
1075
|
+
for (const sheet of this.model.sheets.list) {
|
|
1076
|
+
|
|
1077
|
+
// there's an additional complication: we support anonymous
|
|
1078
|
+
// tables, if the cell is in the table. so we also have to
|
|
1079
|
+
// know the address. so we can't use the IterateAll method.
|
|
1080
|
+
|
|
1081
|
+
// duh, no we don't. if the cell is in the table it will have
|
|
1082
|
+
// a reference.
|
|
1083
|
+
|
|
1084
|
+
sheet.cells.IterateAll(cell => {
|
|
1085
|
+
if (cell.ValueIsFormula()) {
|
|
1086
|
+
let updated_formula = false;
|
|
1087
|
+
const parse_result = this.parser.Parse(cell.value);
|
|
1088
|
+
if (parse_result.expression) {
|
|
1089
|
+
|
|
1090
|
+
this.parser.Walk(parse_result.expression, (unit) => {
|
|
1091
|
+
if (unit.type === 'structured-reference') {
|
|
1092
|
+
|
|
1093
|
+
if (unit.table.toLowerCase() === table_name ||
|
|
1094
|
+
(!unit.table && cell.table === table)) {
|
|
1095
|
+
|
|
1096
|
+
// we may need to rewrite...
|
|
1097
|
+
for (const [key, value] of update.entries()) {
|
|
1098
|
+
if (unit.column.toLowerCase() === key) {
|
|
1099
|
+
|
|
1100
|
+
// ok we need to update
|
|
1101
|
+
unit.column = value;
|
|
1102
|
+
updated_formula = true;
|
|
1103
|
+
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return true;
|
|
1110
|
+
});
|
|
1111
|
+
if (updated_formula) {
|
|
1112
|
+
console.info('updating value');
|
|
1113
|
+
cell.value = '=' + this.parser.Render(parse_result.expression, {
|
|
1114
|
+
missing: '',
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
table.columns = columns;
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
start: {
|
|
1133
|
+
...table.area.start,
|
|
1134
|
+
}, end: {
|
|
1135
|
+
row: table.area.start.row,
|
|
1136
|
+
column: table.area.end.column,
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* set range, via command. returns affected area.
|
|
1144
|
+
*
|
|
1145
|
+
* Adding a flags parameter (in/out) to support indicating
|
|
1146
|
+
* that we need to update layout.
|
|
1147
|
+
*/
|
|
1148
|
+
protected SetRangeInternal(command: SetRangeCommand, flags: UpdateFlags = {}): Area|undefined {
|
|
1149
|
+
|
|
1150
|
+
// NOTE: apparently if we call SetRange with a single target
|
|
1151
|
+
// and the array flag set, it gets translated to an area. which
|
|
1152
|
+
// is OK, I guess, but there may be an unecessary branch in here.
|
|
1153
|
+
|
|
1154
|
+
const area = IsCellAddress(command.area)
|
|
1155
|
+
? new Area(command.area)
|
|
1156
|
+
: new Area(command.area.start, command.area.end);
|
|
1157
|
+
|
|
1158
|
+
const sheet = this.FindSheet(area);
|
|
1159
|
+
|
|
1160
|
+
if (!area.start.sheet_id) {
|
|
1161
|
+
area.start.sheet_id = sheet.id;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (!area.entire_row && !area.entire_column && (
|
|
1165
|
+
area.end.row >= sheet.rows
|
|
1166
|
+
|| area.end.column >= sheet.columns)) {
|
|
1167
|
+
|
|
1168
|
+
// we have to call this because the 'set area' method calls RealArea
|
|
1169
|
+
sheet.cells.EnsureCell(area.end);
|
|
1170
|
+
|
|
1171
|
+
// should we send a structure event here? we may be increasing the
|
|
1172
|
+
// size, in which case we should send the event. even though no addresses
|
|
1173
|
+
// change, there are new cells.
|
|
1174
|
+
|
|
1175
|
+
if (sheet === this.active_sheet) {
|
|
1176
|
+
flags.layout = true;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// originally we called sheet methods here, but all the sheet
|
|
1182
|
+
// does is call methods on the cells object -- we can shortcut.
|
|
1183
|
+
|
|
1184
|
+
// is that a good idea? (...)
|
|
1185
|
+
|
|
1186
|
+
// at a minimum we can consolidate...
|
|
1187
|
+
|
|
1188
|
+
if (IsCellAddress(command.area)) {
|
|
1189
|
+
|
|
1190
|
+
// FIXME: should throw if we try to set part of an array
|
|
1191
|
+
|
|
1192
|
+
const cell = sheet.CellData(command.area);
|
|
1193
|
+
if (cell.area && (cell.area.rows > 1 || cell.area.columns > 1)) {
|
|
1194
|
+
this.Error(ErrorCode.array);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// single cell
|
|
1199
|
+
// UPDATE: could be array
|
|
1200
|
+
|
|
1201
|
+
// type is value|value[][], pull out first value. at some point
|
|
1202
|
+
// we may have supported value[], or maybe they were passed in
|
|
1203
|
+
// accidentally, but check regardless.
|
|
1204
|
+
|
|
1205
|
+
// FIXME: no, that should throw (or otherwise error) (or fix the data?).
|
|
1206
|
+
// we can't handle errors all the way down the call stack.
|
|
1207
|
+
|
|
1208
|
+
let value = Array.isArray(command.value) ?
|
|
1209
|
+
Array.isArray(command.value[0]) ? command.value[0][0] : command.value[0] : command.value;
|
|
1210
|
+
|
|
1211
|
+
// translate R1C1. in this case, we translate relative to the
|
|
1212
|
+
// target address, irrspective of the array flag. this is the
|
|
1213
|
+
// easiest case?
|
|
1214
|
+
|
|
1215
|
+
// NOTE: as noted above (top of function), if a single cell target
|
|
1216
|
+
// is set with the array flag, it may fall into the next branch. not
|
|
1217
|
+
// sure that makes much of a difference.
|
|
1218
|
+
|
|
1219
|
+
if (command.r1c1) {
|
|
1220
|
+
value = this.TranslateR1C1(command.area, value);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (command.array) {
|
|
1224
|
+
|
|
1225
|
+
// what is the case for this? not saying it doesn't happen, just
|
|
1226
|
+
// when is it useful?
|
|
1227
|
+
|
|
1228
|
+
// A: there is the case in Excel where there are different semantics
|
|
1229
|
+
// for array calculation; something we mentioned in one of the kb
|
|
1230
|
+
// articles, something about array functions... [FIXME: ref?]
|
|
1231
|
+
|
|
1232
|
+
sheet.SetArrayValue(area, value);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
sheet.SetCellValue(command.area, value);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return area;
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
|
|
1242
|
+
// there are a couple of options here, from the methods that
|
|
1243
|
+
// have accumulated in Sheet.
|
|
1244
|
+
|
|
1245
|
+
// SetArrayValue -- set data as an array
|
|
1246
|
+
// SetAreaValues -- set values from data one-to-one
|
|
1247
|
+
// SetAreaValue -- single value repeated in range
|
|
1248
|
+
|
|
1249
|
+
// FIXME: clean this up!
|
|
1250
|
+
|
|
1251
|
+
if (command.array) {
|
|
1252
|
+
|
|
1253
|
+
let value = Array.isArray(command.value) ?
|
|
1254
|
+
Array.isArray(command.value[0]) ? command.value[0][0] : command.value[0] : command.value;
|
|
1255
|
+
|
|
1256
|
+
if (command.r1c1) {
|
|
1257
|
+
value = this.TranslateR1C1(area.start, value);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
sheet.SetArrayValue(area, value);
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
|
|
1264
|
+
// in this case, either value is a single value or it's a 2D array;
|
|
1265
|
+
// and area is a range of unknown size. we do a 1-1 map from area
|
|
1266
|
+
// member to data member. if the data is not the same shape, it just
|
|
1267
|
+
// results in empty cells (if area is larger) or dropped data (if value
|
|
1268
|
+
// is larger).
|
|
1269
|
+
|
|
1270
|
+
// so for the purposes of R1C1, we have to run the same loop that
|
|
1271
|
+
// happens internally in the Cells.SetArea routine. but I definitely
|
|
1272
|
+
// don't want R1C1 to get all the way in there.
|
|
1273
|
+
|
|
1274
|
+
// FIXME/TODO: we're doing this the naive way for now. it could be
|
|
1275
|
+
// optimized in several ways.
|
|
1276
|
+
|
|
1277
|
+
if (command.r1c1) {
|
|
1278
|
+
if (Array.isArray(command.value)) {
|
|
1279
|
+
|
|
1280
|
+
// loop on DATA, since that's what we care about here. we can
|
|
1281
|
+
// expand data, since it won't spill in the next call (spill is
|
|
1282
|
+
// handled earlier in the call stack).
|
|
1283
|
+
|
|
1284
|
+
for (let r = 0; r < command.value.length && r < area.rows; r++) {
|
|
1285
|
+
if (!command.value[r]) {
|
|
1286
|
+
command.value[r] = [];
|
|
1287
|
+
}
|
|
1288
|
+
const row = command.value[r];
|
|
1289
|
+
for (let c = 0; c < row.length && c < area.columns; c++) {
|
|
1290
|
+
const target: ICellAddress = { ...area.start, row: area.start.row + r, column: area.start.column + c };
|
|
1291
|
+
row[c] = this.TranslateR1C1(target, row[c]);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
|
|
1298
|
+
// only have to do this for strings
|
|
1299
|
+
if (typeof command.value === 'string' && command.value[0] === '=') {
|
|
1300
|
+
|
|
1301
|
+
// we need to rebuild the value so it is an array, so that
|
|
1302
|
+
// relative addresses will be relative to the cell.
|
|
1303
|
+
|
|
1304
|
+
const value: CellValue[][] = [];
|
|
1305
|
+
|
|
1306
|
+
for (let r = 0; r < area.rows; r++) {
|
|
1307
|
+
const row: CellValue[] = [];
|
|
1308
|
+
for (let c = 0; c < area.columns; c++) {
|
|
1309
|
+
const target: ICellAddress = { ...area.start, row: area.start.row + r, column: area.start.column + c };
|
|
1310
|
+
row.push(this.TranslateR1C1(target, command.value));
|
|
1311
|
+
}
|
|
1312
|
+
value.push(row);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
command.value = value;
|
|
1316
|
+
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
sheet.SetAreaValues2(area, command.value);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return area;
|
|
1325
|
+
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* basic implementation does not handle any UI, painting, or layout.
|
|
1332
|
+
*/
|
|
1333
|
+
protected ActivateSheetInternal(command: ActivateSheetCommand) {
|
|
1334
|
+
|
|
1335
|
+
const candidate = this.ResolveSheet(command) || this.model.sheets.list[0];
|
|
1336
|
+
|
|
1337
|
+
if (this.active_sheet === candidate && !command.force) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (!candidate.visible) {
|
|
1342
|
+
throw new Error('cannot activate hidden sheet');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// hold this for the event (later)
|
|
1346
|
+
|
|
1347
|
+
const deactivate = this.active_sheet;
|
|
1348
|
+
|
|
1349
|
+
// select target
|
|
1350
|
+
|
|
1351
|
+
this.active_sheet = candidate;
|
|
1352
|
+
|
|
1353
|
+
/*
|
|
1354
|
+
// scrub, then add any sheet annotations. note the caller will
|
|
1355
|
+
// still have to inflate these or do whatever step is necessary to
|
|
1356
|
+
// render.
|
|
1357
|
+
|
|
1358
|
+
const annotations = this.active_sheet.annotations;
|
|
1359
|
+
for (const element of annotations) {
|
|
1360
|
+
this.AddAnnotation(element, true);
|
|
1361
|
+
}
|
|
1362
|
+
*/
|
|
1363
|
+
|
|
1364
|
+
this.grid_events.Publish({
|
|
1365
|
+
type: 'sheet-change',
|
|
1366
|
+
deactivate,
|
|
1367
|
+
activate: this.active_sheet,
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
protected ShowSheetInternal(command: ShowSheetCommand) {
|
|
1373
|
+
|
|
1374
|
+
const sheet = this.ResolveSheet(command);
|
|
1375
|
+
|
|
1376
|
+
// invalid
|
|
1377
|
+
if (!sheet) { return; }
|
|
1378
|
+
|
|
1379
|
+
// not changed
|
|
1380
|
+
if (sheet.visible === command.show) { return; }
|
|
1381
|
+
|
|
1382
|
+
// make sure at least one will be visible after the operation
|
|
1383
|
+
if (!command.show) {
|
|
1384
|
+
|
|
1385
|
+
let count = 0;
|
|
1386
|
+
for (const test of this.model.sheets.list) {
|
|
1387
|
+
if (!sheet.visible || test === sheet) { count++; }
|
|
1388
|
+
}
|
|
1389
|
+
if (count >= this.model.sheets.length) {
|
|
1390
|
+
throw new Error('can\'t hide all sheets');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// ok, set
|
|
1396
|
+
sheet.visible = command.show;
|
|
1397
|
+
|
|
1398
|
+
// is this current?
|
|
1399
|
+
if (sheet === this.active_sheet) {
|
|
1400
|
+
|
|
1401
|
+
// this needs to check the visibility field, or else we'll throw
|
|
1402
|
+
// when we call the activate method. given the above check we know
|
|
1403
|
+
// that there's at least one visible sheet.
|
|
1404
|
+
|
|
1405
|
+
const list = this.model.sheets.list;
|
|
1406
|
+
|
|
1407
|
+
// first find the _next_ visible sheet...
|
|
1408
|
+
|
|
1409
|
+
for (let i = 0; i < list.length; i++) {
|
|
1410
|
+
if (list[i] === this.active_sheet) {
|
|
1411
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
1412
|
+
if (list[j].visible) {
|
|
1413
|
+
this.ActivateSheetInternal({
|
|
1414
|
+
key: CommandKey.ActivateSheet,
|
|
1415
|
+
index: j,
|
|
1416
|
+
});
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// if we got here, then we need to start again from the beginning
|
|
1422
|
+
|
|
1423
|
+
for (let j = 0; j< list.length; j++) {
|
|
1424
|
+
if (list[j].visible) {
|
|
1425
|
+
this.ActivateSheetInternal({
|
|
1426
|
+
key: CommandKey.ActivateSheet,
|
|
1427
|
+
index: j,
|
|
1428
|
+
});
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// should not be possible
|
|
1434
|
+
throw new Error('no visible sheet');
|
|
1435
|
+
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* normalize commands. for co-editing support we need to ensure that
|
|
1444
|
+
* commands properly have sheet IDs in areas/addresses (and explicit
|
|
1445
|
+
* fields in some cases).
|
|
1446
|
+
*
|
|
1447
|
+
* at the same time we're editing the commands a little bit to make
|
|
1448
|
+
* them a little more consistent (within reason).
|
|
1449
|
+
*
|
|
1450
|
+
* @param commands
|
|
1451
|
+
*/
|
|
1452
|
+
protected NormalizeCommands(commands: Command|Command[]): Command[] {
|
|
1453
|
+
|
|
1454
|
+
if (!Array.isArray(commands)) {
|
|
1455
|
+
commands = [commands];
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const id = this.active_sheet.id;
|
|
1459
|
+
|
|
1460
|
+
for (const command of commands) {
|
|
1461
|
+
switch (command.key) {
|
|
1462
|
+
|
|
1463
|
+
// nothing
|
|
1464
|
+
case CommandKey.Null:
|
|
1465
|
+
case CommandKey.ShowHeaders:
|
|
1466
|
+
case CommandKey.ShowSheet:
|
|
1467
|
+
case CommandKey.AddSheet:
|
|
1468
|
+
case CommandKey.DuplicateSheet:
|
|
1469
|
+
case CommandKey.DeleteSheet:
|
|
1470
|
+
case CommandKey.ActivateSheet:
|
|
1471
|
+
case CommandKey.RenameSheet:
|
|
1472
|
+
case CommandKey.ReorderSheet:
|
|
1473
|
+
case CommandKey.Reset:
|
|
1474
|
+
break;
|
|
1475
|
+
|
|
1476
|
+
/*
|
|
1477
|
+
// both
|
|
1478
|
+
case CommandKey.Clear:
|
|
1479
|
+
if (command.area) {
|
|
1480
|
+
if (!command.area.start.sheet_id) {
|
|
1481
|
+
command.area.start.sheet_id = id;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
if (!command.sheet_id) {
|
|
1486
|
+
command.sheet_id = id;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
break;
|
|
1490
|
+
*/
|
|
1491
|
+
|
|
1492
|
+
// special case
|
|
1493
|
+
case CommandKey.SortTable:
|
|
1494
|
+
case CommandKey.RemoveTable:
|
|
1495
|
+
if (!command.table.area.start.sheet_id) {
|
|
1496
|
+
command.table.area.start.sheet_id = id;
|
|
1497
|
+
}
|
|
1498
|
+
break;
|
|
1499
|
+
|
|
1500
|
+
// field
|
|
1501
|
+
case CommandKey.ResizeRows:
|
|
1502
|
+
case CommandKey.ResizeColumns:
|
|
1503
|
+
case CommandKey.InsertColumns:
|
|
1504
|
+
case CommandKey.InsertRows:
|
|
1505
|
+
case CommandKey.Freeze:
|
|
1506
|
+
if (!command.sheet_id) {
|
|
1507
|
+
command.sheet_id = id;
|
|
1508
|
+
}
|
|
1509
|
+
break;
|
|
1510
|
+
|
|
1511
|
+
// area: Area|Address (may be optional)
|
|
1512
|
+
case CommandKey.Clear:
|
|
1513
|
+
case CommandKey.SetNote:
|
|
1514
|
+
case CommandKey.SetLink:
|
|
1515
|
+
case CommandKey.UpdateBorders:
|
|
1516
|
+
case CommandKey.MergeCells:
|
|
1517
|
+
case CommandKey.UnmergeCells:
|
|
1518
|
+
case CommandKey.DataValidation:
|
|
1519
|
+
case CommandKey.SetRange:
|
|
1520
|
+
case CommandKey.UpdateStyle:
|
|
1521
|
+
case CommandKey.SetName:
|
|
1522
|
+
case CommandKey.Select:
|
|
1523
|
+
case CommandKey.InsertTable:
|
|
1524
|
+
|
|
1525
|
+
if (command.area) {
|
|
1526
|
+
if (IsCellAddress(command.area)) {
|
|
1527
|
+
if (!command.area.sheet_id) {
|
|
1528
|
+
command.area.sheet_id = id;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
if (!command.area.start.sheet_id) {
|
|
1533
|
+
command.area.start.sheet_id = id;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
break;
|
|
1538
|
+
|
|
1539
|
+
// default:
|
|
1540
|
+
// // command key here should be `never` if we've covered all the
|
|
1541
|
+
// // cases (ts will complain)
|
|
1542
|
+
// // console.warn('unhandled command key', command.key);
|
|
1543
|
+
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return commands;
|
|
1548
|
+
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* add sheet. data only.
|
|
1553
|
+
*/
|
|
1554
|
+
protected AddSheetInternal(name = Sheet.default_sheet_name, insert_index = -1): number|undefined {
|
|
1555
|
+
|
|
1556
|
+
if (!this.options.add_tab) {
|
|
1557
|
+
console.warn('add tab option not set or false');
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// validate name...
|
|
1562
|
+
|
|
1563
|
+
while (this.model.sheets.list.some((test) => test.name === name)) {
|
|
1564
|
+
|
|
1565
|
+
const match = name.match(/^(.*?)(\d+)$/);
|
|
1566
|
+
if (match) {
|
|
1567
|
+
name = match[1] + (Number(match[2]) + 1);
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
name = name + '2';
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// FIXME: structure event
|
|
1576
|
+
|
|
1577
|
+
const sheet = Sheet.Blank(this.model.theme_style_properties, name);
|
|
1578
|
+
|
|
1579
|
+
if (insert_index >= 0) {
|
|
1580
|
+
this.model.sheets.Splice(insert_index, 0, sheet);
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
this.model.sheets.Add(sheet);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// moved to ExecCmomand
|
|
1587
|
+
// if (this.tab_bar) { this.tab_bar.Update(); }
|
|
1588
|
+
|
|
1589
|
+
return sheet.id;
|
|
1590
|
+
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* resolve sheet in a command that uses the SheetSelection interface;
|
|
1595
|
+
* that allows sheet selection by name, id or index.
|
|
1596
|
+
*/
|
|
1597
|
+
protected ResolveSheet(command: SheetSelection): Sheet|undefined {
|
|
1598
|
+
|
|
1599
|
+
// NOTE: since you are using typeof here to check for undefined,
|
|
1600
|
+
// it seems like it would be efficient to use typeof to check
|
|
1601
|
+
// the actual type; hence merging "index" and "name" might be
|
|
1602
|
+
// more efficient than checking each one separately.
|
|
1603
|
+
|
|
1604
|
+
if (typeof command.index !== 'undefined') {
|
|
1605
|
+
return this.model.sheets.list[command.index];
|
|
1606
|
+
}
|
|
1607
|
+
if (typeof command.name !== 'undefined') {
|
|
1608
|
+
return this.model.sheets.Find(command.name);
|
|
1609
|
+
}
|
|
1610
|
+
if (command.id) {
|
|
1611
|
+
return this.model.sheets.Find(command.id);
|
|
1612
|
+
}
|
|
1613
|
+
return undefined;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* find sheet matching sheet_id in area.start, or active sheet
|
|
1618
|
+
*
|
|
1619
|
+
* FIXME: should return undefined on !match
|
|
1620
|
+
* FIXME: should be in model, which should be a class
|
|
1621
|
+
*/
|
|
1622
|
+
protected FindSheet(identifier: number|IArea|ICellAddress|undefined): Sheet {
|
|
1623
|
+
|
|
1624
|
+
if (identifier === undefined) {
|
|
1625
|
+
return this.active_sheet;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const id = typeof identifier === 'number' ? identifier : IsCellAddress(identifier) ? identifier.sheet_id : identifier.start.sheet_id;
|
|
1629
|
+
|
|
1630
|
+
if (!id || id === this.active_sheet.id) {
|
|
1631
|
+
return this.active_sheet;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const sheet = this.model.sheets.Find(id);
|
|
1635
|
+
if (sheet) {
|
|
1636
|
+
return sheet;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/*
|
|
1640
|
+
for (const test of this.model.sheets) {
|
|
1641
|
+
if (test.id === id) {
|
|
1642
|
+
return test;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
*/
|
|
1646
|
+
|
|
1647
|
+
// FIXME: should return undefined here
|
|
1648
|
+
return this.active_sheet;
|
|
1649
|
+
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* this function now works for both rows and columns, and can handle
|
|
1655
|
+
* sheets other than the active sheet. it does assume that you only ever
|
|
1656
|
+
* add rows/columns on the active sheet, but since that's all parameterized
|
|
1657
|
+
* you could get it to work either way.
|
|
1658
|
+
*
|
|
1659
|
+
* in fact we should change the names of those parameters so it's a little
|
|
1660
|
+
* more generic.
|
|
1661
|
+
*/
|
|
1662
|
+
protected PatchFormulasInternal(source: string,
|
|
1663
|
+
before_row: number,
|
|
1664
|
+
row_count: number,
|
|
1665
|
+
before_column: number,
|
|
1666
|
+
column_count: number,
|
|
1667
|
+
target_sheet_name: string,
|
|
1668
|
+
is_target: boolean) {
|
|
1669
|
+
|
|
1670
|
+
const parsed = this.parser.Parse(source || '');
|
|
1671
|
+
let modified = false;
|
|
1672
|
+
|
|
1673
|
+
// the sheet test is different for active sheet/non-active sheet.
|
|
1674
|
+
|
|
1675
|
+
// on the active sheet, check for no name OR name === active sheet name.
|
|
1676
|
+
// on other sheets, check for name AND name === active sheet name.
|
|
1677
|
+
|
|
1678
|
+
if (parsed.expression) {
|
|
1679
|
+
this.parser.Walk(parsed.expression, (element: ExpressionUnit) => {
|
|
1680
|
+
|
|
1681
|
+
if (element.type === 'range' || element.type === 'address') {
|
|
1682
|
+
|
|
1683
|
+
// we can test if we need to modify a range or an address, but the
|
|
1684
|
+
// second address in a range can't be tested properly. so the solution
|
|
1685
|
+
// here is to just capture the addresses that need to be modified
|
|
1686
|
+
// from the range, and then not recurse (we should never get here
|
|
1687
|
+
// as an address in a range).
|
|
1688
|
+
|
|
1689
|
+
const addresses: UnitAddress[] = [];
|
|
1690
|
+
|
|
1691
|
+
if (element.type === 'range') {
|
|
1692
|
+
|
|
1693
|
+
// there's a problem: this breaks because the inner test fails when
|
|
1694
|
+
// this is TRUE... we may need to modify
|
|
1695
|
+
|
|
1696
|
+
// recurse if (1) explicit name match; or (2) no name AND we are on the active sheet
|
|
1697
|
+
|
|
1698
|
+
// return ((element.start.sheet && element.start.sheet.toLowerCase() === active_sheet_name) || (!element.start.sheet && active_sheet));
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
if ((element.start.sheet && element.start.sheet.toLowerCase() === target_sheet_name) || (!element.start.sheet && is_target)) {
|
|
1702
|
+
addresses.push(element.start, element.end);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
}
|
|
1706
|
+
else if (element.type === 'address') {
|
|
1707
|
+
if ((element.sheet && element.sheet.toLowerCase() === target_sheet_name) || (!element.sheet && is_target)) {
|
|
1708
|
+
addresses.push(element);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// could switch the tests around? (referring to the count
|
|
1714
|
+
// tests, which switch on operation)
|
|
1715
|
+
|
|
1716
|
+
for (const address of addresses) {
|
|
1717
|
+
|
|
1718
|
+
if (row_count && address.row >= before_row) {
|
|
1719
|
+
if (row_count < 0 && address.row + row_count < before_row) {
|
|
1720
|
+
address.column = address.row = -1;
|
|
1721
|
+
}
|
|
1722
|
+
else {
|
|
1723
|
+
address.row += row_count;
|
|
1724
|
+
}
|
|
1725
|
+
modified = true;
|
|
1726
|
+
}
|
|
1727
|
+
if (column_count && address.column >= before_column) {
|
|
1728
|
+
if (column_count < 0 && address.column + column_count < before_column) {
|
|
1729
|
+
address.column = address.row = -1; // set as invalid (-1)
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
address.column += column_count;
|
|
1733
|
+
}
|
|
1734
|
+
modified = true;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
return false; // always explicit
|
|
1740
|
+
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return true; // recurse for everything else
|
|
1744
|
+
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
if (modified) {
|
|
1748
|
+
return '=' + this.parser.Render(parsed.expression, { missing: '' });
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
return undefined;
|
|
1753
|
+
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/**
|
|
1757
|
+
* splitting this logic into a new function so we can reuse it
|
|
1758
|
+
* for invalidating broken references. generally we'll call this
|
|
1759
|
+
* on all sheets, but I wanted to leave the option open.
|
|
1760
|
+
*
|
|
1761
|
+
* @returns count of changes made. it's useful for the delete routine,
|
|
1762
|
+
* so we can force a recalc.
|
|
1763
|
+
*/
|
|
1764
|
+
protected RenameSheetReferences(sheets: Sheet[], old_name: string, name: string): number {
|
|
1765
|
+
|
|
1766
|
+
let changes = 0;
|
|
1767
|
+
|
|
1768
|
+
old_name = old_name.toLowerCase();
|
|
1769
|
+
|
|
1770
|
+
for (const sheet of sheets) {
|
|
1771
|
+
|
|
1772
|
+
// cells
|
|
1773
|
+
sheet.cells.IterateAll((cell: Cell) => {
|
|
1774
|
+
if (cell.ValueIsFormula()) {
|
|
1775
|
+
let modified = false;
|
|
1776
|
+
const parsed = this.parser.Parse(cell.value || '');
|
|
1777
|
+
if (parsed.expression) {
|
|
1778
|
+
this.parser.Walk(parsed.expression, (element: ExpressionUnit) => {
|
|
1779
|
+
if (element.type === 'address') {
|
|
1780
|
+
if (element.sheet && element.sheet.toLowerCase() === old_name) {
|
|
1781
|
+
element.sheet = name;
|
|
1782
|
+
modified = true;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return true; // continue walk
|
|
1786
|
+
});
|
|
1787
|
+
if (modified) {
|
|
1788
|
+
cell.value = '=' + this.parser.Render(parsed.expression, { missing: '' });
|
|
1789
|
+
changes++;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// annotations
|
|
1796
|
+
for (const annotation of sheet.annotations) {
|
|
1797
|
+
if (annotation.formula) {
|
|
1798
|
+
let modified = false;
|
|
1799
|
+
const parsed = this.parser.Parse(annotation.formula || '');
|
|
1800
|
+
if (parsed.expression) {
|
|
1801
|
+
this.parser.Walk(parsed.expression, (element: ExpressionUnit) => {
|
|
1802
|
+
if (element.type === 'address') {
|
|
1803
|
+
if (element.sheet && element.sheet.toLowerCase() === old_name) {
|
|
1804
|
+
element.sheet = name;
|
|
1805
|
+
modified = true;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return true; // continue walk
|
|
1809
|
+
});
|
|
1810
|
+
if (modified) {
|
|
1811
|
+
annotation.formula = '=' + this.parser.Render(parsed.expression, { missing: '' });
|
|
1812
|
+
changes++;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
return changes;
|
|
1820
|
+
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* these are all addative except for "none", which removes all borders.
|
|
1826
|
+
*
|
|
1827
|
+
* we no longer put borders into two cells at once (hurrah!). however
|
|
1828
|
+
* we still need to do some maintenance on the mirror cells -- because
|
|
1829
|
+
* if you apply a border to cell A1, then that should take precedence
|
|
1830
|
+
* over any border previously applied to cell A2.
|
|
1831
|
+
*
|
|
1832
|
+
* FIXME: is that right? perhaps we should just leave whatever the user
|
|
1833
|
+
* did -- with the exception of clearing, which should always mirror.
|
|
1834
|
+
*
|
|
1835
|
+
*
|
|
1836
|
+
* UPDATE: modifying function for use with ExecCommand. runs the style
|
|
1837
|
+
* updates and returns the affected area.
|
|
1838
|
+
*
|
|
1839
|
+
*/
|
|
1840
|
+
protected ApplyBordersInternal(command: UpdateBordersCommand) {
|
|
1841
|
+
|
|
1842
|
+
const borders = command.borders;
|
|
1843
|
+
const width = (command.borders === BorderConstants.None)
|
|
1844
|
+
? 0 : command.width;
|
|
1845
|
+
|
|
1846
|
+
const area = new Area(command.area.start, command.area.end);
|
|
1847
|
+
const sheet = this.FindSheet(area);
|
|
1848
|
+
|
|
1849
|
+
area.start.sheet_id = sheet.id; // ensure
|
|
1850
|
+
|
|
1851
|
+
/*
|
|
1852
|
+
let sheet = this.active_sheet;
|
|
1853
|
+
if (command.area.start.sheet_id && command.area.start.sheet_id !== this.active_sheet.id) {
|
|
1854
|
+
for (const compare of this.model.sheets) {
|
|
1855
|
+
if (compare.id === command.area.start.sheet_id) {
|
|
1856
|
+
sheet = compare;
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
*/
|
|
1862
|
+
|
|
1863
|
+
const top: Style.Properties = { border_top: width };
|
|
1864
|
+
const bottom: Style.Properties = { border_bottom: width };
|
|
1865
|
+
const left: Style.Properties = { border_left: width };
|
|
1866
|
+
const right: Style.Properties = { border_right: width };
|
|
1867
|
+
|
|
1868
|
+
const clear_top: Style.Properties = { border_top: 0, border_top_fill: {} };
|
|
1869
|
+
const clear_bottom: Style.Properties = { border_bottom: 0, border_bottom_fill: {} };
|
|
1870
|
+
const clear_left: Style.Properties = { border_left: 0, border_left_fill: {} };
|
|
1871
|
+
const clear_right: Style.Properties = { border_right: 0, border_right_fill: {} };
|
|
1872
|
+
|
|
1873
|
+
// default to "none", which means "default"
|
|
1874
|
+
|
|
1875
|
+
//if (!command.color) {
|
|
1876
|
+
// command.color = 'none';
|
|
1877
|
+
//}
|
|
1878
|
+
|
|
1879
|
+
//if (typeof command.color !== 'undefined') {
|
|
1880
|
+
if (command.color) {
|
|
1881
|
+
|
|
1882
|
+
// this is now an object so we need to clone it (might be faster to JSON->JSON)
|
|
1883
|
+
|
|
1884
|
+
top.border_top_fill = {...command.color};
|
|
1885
|
+
bottom.border_bottom_fill = {...command.color};
|
|
1886
|
+
left.border_left_fill = {...command.color};
|
|
1887
|
+
right.border_right_fill = {...command.color};
|
|
1888
|
+
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
|
|
1892
|
+
// otherwise we should be sure to clear any color
|
|
1893
|
+
|
|
1894
|
+
top.border_top_fill = {};
|
|
1895
|
+
bottom.border_bottom_fill = {};
|
|
1896
|
+
left.border_left_fill = {};
|
|
1897
|
+
right.border_right_fill = {};
|
|
1898
|
+
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// inside all/none
|
|
1902
|
+
if (borders === BorderConstants.None || borders === BorderConstants.All) {
|
|
1903
|
+
sheet.UpdateAreaStyle(area, {
|
|
1904
|
+
...top, ...bottom, ...left, ...right,
|
|
1905
|
+
}, true);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// top
|
|
1909
|
+
if (borders === BorderConstants.Top || borders === BorderConstants.Outside) {
|
|
1910
|
+
if (!area.entire_column) {
|
|
1911
|
+
sheet.UpdateAreaStyle(area.top, { ...top }, true);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// mirror top (CLEAR)
|
|
1916
|
+
if (borders === BorderConstants.None || borders === BorderConstants.All ||
|
|
1917
|
+
borders === BorderConstants.Outside || borders === BorderConstants.Top) {
|
|
1918
|
+
if (!area.entire_column) {
|
|
1919
|
+
if (area.start.row) {
|
|
1920
|
+
sheet.UpdateAreaStyle(new Area(
|
|
1921
|
+
{ row: area.start.row - 1, column: area.start.column },
|
|
1922
|
+
{ row: area.start.row - 1, column: area.end.column }), { ...clear_bottom }, true);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// bottom
|
|
1928
|
+
if (borders === BorderConstants.Bottom || borders === BorderConstants.Outside) {
|
|
1929
|
+
if (!area.entire_column) {
|
|
1930
|
+
sheet.UpdateAreaStyle(area.bottom, { ...bottom }, true);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// mirror bottom (CLEAR)
|
|
1935
|
+
if (borders === BorderConstants.None || borders === BorderConstants.All ||
|
|
1936
|
+
borders === BorderConstants.Outside || borders === BorderConstants.Bottom) {
|
|
1937
|
+
if (!area.entire_column) {
|
|
1938
|
+
sheet.UpdateAreaStyle(new Area(
|
|
1939
|
+
{ row: area.end.row + 1, column: area.start.column },
|
|
1940
|
+
{ row: area.end.row + 1, column: area.end.column }), { ...clear_top }, true);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// left
|
|
1945
|
+
if (borders === BorderConstants.Left || borders === BorderConstants.Outside) {
|
|
1946
|
+
if (!area.entire_row) {
|
|
1947
|
+
sheet.UpdateAreaStyle(area.left, { ...left }, true);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// mirror left (CLEAR)
|
|
1952
|
+
if (borders === BorderConstants.None || borders === BorderConstants.All ||
|
|
1953
|
+
borders === BorderConstants.Outside || borders === BorderConstants.Left) {
|
|
1954
|
+
if (!area.entire_row) {
|
|
1955
|
+
if (area.start.column) {
|
|
1956
|
+
sheet.UpdateAreaStyle(new Area(
|
|
1957
|
+
{ row: area.start.row, column: area.start.column - 1 },
|
|
1958
|
+
{ row: area.end.row, column: area.start.column - 1 }), { ...clear_right }, true);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// right
|
|
1964
|
+
if (borders === BorderConstants.Right || borders === BorderConstants.Outside) {
|
|
1965
|
+
if (!area.entire_row) {
|
|
1966
|
+
sheet.UpdateAreaStyle(area.right, { ...right }, true);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// mirror right (CLEAR)
|
|
1971
|
+
if (borders === BorderConstants.None || borders === BorderConstants.All ||
|
|
1972
|
+
borders === BorderConstants.Outside || borders === BorderConstants.Right) {
|
|
1973
|
+
if (!area.entire_row) {
|
|
1974
|
+
sheet.UpdateAreaStyle(new Area(
|
|
1975
|
+
{ row: area.start.row, column: area.end.column + 1 },
|
|
1976
|
+
{ row: area.end.row, column: area.end.column + 1 }), { ...clear_left }, true);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/*
|
|
1981
|
+
// why is there not an expand method on area? (FIXME)
|
|
1982
|
+
|
|
1983
|
+
this.DelayedRender(false, new Area({
|
|
1984
|
+
row: Math.max(0, area.start.row - 1),
|
|
1985
|
+
column: Math.max(0, area.start.column - 1),
|
|
1986
|
+
}, {
|
|
1987
|
+
row: area.end.row + 1,
|
|
1988
|
+
column: area.end.column + 1,
|
|
1989
|
+
}));
|
|
1990
|
+
|
|
1991
|
+
// NOTE: we don't have to route through the sheet. we are the only client
|
|
1992
|
+
// (we republish). we can just publish directly.
|
|
1993
|
+
|
|
1994
|
+
this.grid_events.Publish({ type: 'style', area });
|
|
1995
|
+
*/
|
|
1996
|
+
|
|
1997
|
+
return Area.Bleed(area);
|
|
1998
|
+
|
|
1999
|
+
/*
|
|
2000
|
+
return new Area(
|
|
2001
|
+
{
|
|
2002
|
+
row: Math.max(0, area.start.row - 1),
|
|
2003
|
+
column: Math.max(0, area.start.column - 1),
|
|
2004
|
+
}, {
|
|
2005
|
+
row: area.end.row + 1,
|
|
2006
|
+
column: area.end.column + 1,
|
|
2007
|
+
},
|
|
2008
|
+
);
|
|
2009
|
+
*/
|
|
2010
|
+
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
protected TranslateR1C1(address: ICellAddress, value: CellValue): CellValue {
|
|
2014
|
+
|
|
2015
|
+
let transformed = false;
|
|
2016
|
+
|
|
2017
|
+
const cached = this.parser.flags.r1c1;
|
|
2018
|
+
this.parser.flags.r1c1 = true; // set
|
|
2019
|
+
|
|
2020
|
+
if (typeof value === 'string' && value[0] === '=') {
|
|
2021
|
+
const result = this.parser.Parse(value);
|
|
2022
|
+
if (result.expression) {
|
|
2023
|
+
this.parser.Walk(result.expression, unit => {
|
|
2024
|
+
if (unit.type === 'address' && unit.r1c1) {
|
|
2025
|
+
transformed = true;
|
|
2026
|
+
|
|
2027
|
+
// translate...
|
|
2028
|
+
if (unit.offset_column) {
|
|
2029
|
+
unit.column = unit.column + address.column;
|
|
2030
|
+
}
|
|
2031
|
+
if (unit.offset_row) {
|
|
2032
|
+
unit.row = unit.row + address.row;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
}
|
|
2036
|
+
return true;
|
|
2037
|
+
});
|
|
2038
|
+
if (transformed) {
|
|
2039
|
+
|
|
2040
|
+
if (!this.flags.warned_r1c1) {
|
|
2041
|
+
|
|
2042
|
+
// 1-time warning
|
|
2043
|
+
|
|
2044
|
+
this.flags.warned_r1c1 = true;
|
|
2045
|
+
console.warn('NOTE: R1C1 support is experimental. the semantics may change in the future.');
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
value = '=' + this.parser.Render(result.expression, { missing: '' });
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
this.parser.flags.r1c1 = cached; // reset
|
|
2054
|
+
return value;
|
|
2055
|
+
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
protected ClearAreaInternal(area: Area) {
|
|
2059
|
+
|
|
2060
|
+
// updated to use sheet ID. not sure why this was still using
|
|
2061
|
+
// active sheet without checking ID.
|
|
2062
|
+
|
|
2063
|
+
let sheet: Sheet|undefined;
|
|
2064
|
+
|
|
2065
|
+
if (area.start.sheet_id) {
|
|
2066
|
+
sheet = this.model.sheets.Find(area.start.sheet_id);
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
sheet = this.active_sheet;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (!sheet) {
|
|
2073
|
+
console.warn(`can't resolve sheet in ClearAreaInternal`);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
let error = false;
|
|
2078
|
+
area = sheet.RealArea(area); // collapse
|
|
2079
|
+
|
|
2080
|
+
sheet.cells.Apply(area, (cell) => {
|
|
2081
|
+
if (cell.area && !area.ContainsArea(cell.area)) {
|
|
2082
|
+
// throw new Error('can\'t change part of an array');
|
|
2083
|
+
error = true;
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
// if the area completely encloses a table, delete the table
|
|
2088
|
+
const table_keys = this.model.tables.keys();
|
|
2089
|
+
for (const key of table_keys) {
|
|
2090
|
+
const table = this.model.tables.get(key);
|
|
2091
|
+
if (table && table.area.start.sheet_id === sheet.id) {
|
|
2092
|
+
const table_area = new Area(table.area.start, table.area.end);
|
|
2093
|
+
if (area.ContainsArea(table_area)) {
|
|
2094
|
+
for (let row = table_area.start.row; row <= table_area.end.row; row++) {
|
|
2095
|
+
for (let column = table_area.start.column; column <= table.area.end.column; column++) {
|
|
2096
|
+
const cell = sheet.cells.GetCell({row, column}, false);
|
|
2097
|
+
if (cell) {
|
|
2098
|
+
cell.table = undefined;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
this.model.tables.delete(key);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (error) {
|
|
2108
|
+
this.Error(ErrorCode.array); // `You can't change part of an array.`
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
sheet.ClearArea(area);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* send an error message. subscriber can figure out how to communicate it
|
|
2118
|
+
* to users.
|
|
2119
|
+
*
|
|
2120
|
+
* dropping strings, now we only allow error constants (via enum)
|
|
2121
|
+
*
|
|
2122
|
+
* @param message
|
|
2123
|
+
*/
|
|
2124
|
+
protected Error(message: ErrorCode) {
|
|
2125
|
+
|
|
2126
|
+
/*
|
|
2127
|
+
console.info('Error', message);
|
|
2128
|
+
if (typeof message === 'string') {
|
|
2129
|
+
this.grid_events.Publish({
|
|
2130
|
+
type: 'error',
|
|
2131
|
+
message,
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
this.grid_events.Publish({
|
|
2136
|
+
type: 'error',
|
|
2137
|
+
code: message,
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
*/
|
|
2141
|
+
|
|
2142
|
+
this.grid_events.Publish({
|
|
2143
|
+
type: 'error',
|
|
2144
|
+
code: message,
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* this breaks (or doesn't work) if the add_tab option is false; that's
|
|
2152
|
+
* fine, although we might want to make a distinction between UI add-tab
|
|
2153
|
+
* and API add-tab. And allow it from the API.
|
|
2154
|
+
*
|
|
2155
|
+
* @param command
|
|
2156
|
+
* @returns
|
|
2157
|
+
*/
|
|
2158
|
+
private DuplicateSheetInternal(command: DuplicateSheetCommand) {
|
|
2159
|
+
|
|
2160
|
+
if (!this.options.add_tab) {
|
|
2161
|
+
console.warn('add tab option not set or false');
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const source = this.ResolveSheet(command);
|
|
2166
|
+
const next_id = this.model.sheets.list.reduce((id, sheet) => Math.max(id, sheet.id), 0) + 1;
|
|
2167
|
+
|
|
2168
|
+
let insert_index = -1;
|
|
2169
|
+
for (let i = 0; i < this.model.sheets.length; i++) {
|
|
2170
|
+
if (this.model.sheets.list[i] === source) {
|
|
2171
|
+
insert_index = i + 1;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
if (!source || insert_index < 0) {
|
|
2176
|
+
throw new Error('source sheet not found');
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// explicit insert index
|
|
2180
|
+
|
|
2181
|
+
if (typeof command.insert_before === 'number') {
|
|
2182
|
+
insert_index = command.insert_before;
|
|
2183
|
+
}
|
|
2184
|
+
else if (typeof command.insert_before === 'string') {
|
|
2185
|
+
const lc = command.insert_before.toLowerCase();
|
|
2186
|
+
for (let i = 0; i < this.model.sheets.length; i++) {
|
|
2187
|
+
if (this.model.sheets.list[i].name.toLowerCase() === lc) {
|
|
2188
|
+
insert_index = i;
|
|
2189
|
+
break;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
const options: SerializeOptions = {
|
|
2195
|
+
rendered_values: true,
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
const clone = Sheet.FromJSON(source.toJSON(options), this.model.theme_style_properties);
|
|
2199
|
+
|
|
2200
|
+
let name = command.new_name || source.name;
|
|
2201
|
+
while (this.model.sheets.list.some((test) => test.name === name)) {
|
|
2202
|
+
const match = name.match(/^(.*?)(\d+)$/);
|
|
2203
|
+
if (match) {
|
|
2204
|
+
name = match[1] + (Number(match[2]) + 1);
|
|
2205
|
+
}
|
|
2206
|
+
else {
|
|
2207
|
+
name = name + '2';
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
clone.name = name;
|
|
2212
|
+
clone.id = next_id;
|
|
2213
|
+
|
|
2214
|
+
// console.info('CLONE', clone.id, clone);
|
|
2215
|
+
|
|
2216
|
+
this.model.sheets.Splice(insert_index, 0, clone);
|
|
2217
|
+
|
|
2218
|
+
// if (this.tab_bar) { this.tab_bar.Update(); }
|
|
2219
|
+
|
|
2220
|
+
return clone.id;
|
|
2221
|
+
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* this is the callback method for the command-log select command
|
|
2226
|
+
* (which is not widely used). it does nothing. the specialization
|
|
2227
|
+
* should do something.
|
|
2228
|
+
*
|
|
2229
|
+
* @param command
|
|
2230
|
+
*/
|
|
2231
|
+
protected SelectInternal(command: SelectCommand) {
|
|
2232
|
+
// does nothing
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
protected FreezeInternal(command: FreezeCommand) {
|
|
2236
|
+
|
|
2237
|
+
const sheet = this.FindSheet(command.sheet_id || this.active_sheet.id);
|
|
2238
|
+
|
|
2239
|
+
sheet.freeze.rows = command.rows;
|
|
2240
|
+
sheet.freeze.columns = command.columns;
|
|
2241
|
+
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
/**
|
|
2247
|
+
* FIXME: should be API method
|
|
2248
|
+
* FIXME: need to handle annotations that are address-based
|
|
2249
|
+
*
|
|
2250
|
+
* @see InsertColumns for inline comments
|
|
2251
|
+
*/
|
|
2252
|
+
protected InsertRowsInternal(command: InsertRowsCommand): {
|
|
2253
|
+
error?: boolean;
|
|
2254
|
+
update_annotations_list?: Annotation[];
|
|
2255
|
+
resize_annotations_list?: Annotation[];
|
|
2256
|
+
delete_annotations_list?: Annotation[];
|
|
2257
|
+
} {
|
|
2258
|
+
|
|
2259
|
+
const target_sheet = this.FindSheet(command.sheet_id);
|
|
2260
|
+
|
|
2261
|
+
if (command.count === Infinity) {
|
|
2262
|
+
command.count = 1; // ?
|
|
2263
|
+
}
|
|
2264
|
+
else if (command.count === -Infinity) {
|
|
2265
|
+
command.count = -target_sheet.rows; // delete all
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
if (!target_sheet.InsertRows(command.before_row, command.count)){
|
|
2269
|
+
// this.Error(`You can't change part of an array.`);
|
|
2270
|
+
this.Error(ErrorCode.array);
|
|
2271
|
+
return { error: true };
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// see InsertColumnsInternal re: tables. rows are less complicated,
|
|
2275
|
+
// except that if you delete the header row we want to remove the
|
|
2276
|
+
// table entirely.
|
|
2277
|
+
|
|
2278
|
+
const tables = Array.from(this.model.tables.values());
|
|
2279
|
+
|
|
2280
|
+
for (const table of tables) {
|
|
2281
|
+
if (table.area.start.sheet_id === command.sheet_id) {
|
|
2282
|
+
|
|
2283
|
+
if (command.count > 0) {
|
|
2284
|
+
if (command.before_row <= table.area.start.row) {
|
|
2285
|
+
// shift the table down
|
|
2286
|
+
|
|
2287
|
+
//console.info("shift table down");
|
|
2288
|
+
table.area.start.row += command.count;
|
|
2289
|
+
table.area.end.row += command.count;
|
|
2290
|
+
|
|
2291
|
+
}
|
|
2292
|
+
else if (command.before_row <= table.area.end.row) {
|
|
2293
|
+
// insert rows. we need to add references to
|
|
2294
|
+
// cells that have been inserted.
|
|
2295
|
+
|
|
2296
|
+
// console.info("insert table rows");
|
|
2297
|
+
table.area.end.row += command.count;
|
|
2298
|
+
for (let row = table.area.start.row; row <= table.area.end.row; row++) {
|
|
2299
|
+
for (let column = table.area.start.column; column <= table.area.end.column; column++) {
|
|
2300
|
+
const cell = target_sheet.CellData({row, column});
|
|
2301
|
+
cell.table = table;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
else {
|
|
2308
|
+
if (command.before_row <= table.area.start.row) {
|
|
2309
|
+
if (command.before_row - command.count <= table.area.start.row) {
|
|
2310
|
+
// shift table up
|
|
2311
|
+
|
|
2312
|
+
table.area.start.row += command.count;
|
|
2313
|
+
table.area.end.row += command.count;
|
|
2314
|
+
|
|
2315
|
+
}
|
|
2316
|
+
else if (command.before_row - command.count >= table.area.end.row) {
|
|
2317
|
+
// remove the entire table
|
|
2318
|
+
|
|
2319
|
+
this.model.tables.delete(table.name.toLowerCase());
|
|
2320
|
+
|
|
2321
|
+
}
|
|
2322
|
+
else {
|
|
2323
|
+
|
|
2324
|
+
// assuming this will remove the header row, drop the table
|
|
2325
|
+
// altogether. the alternative is to just not let you remove
|
|
2326
|
+
// this row. but that should be handled before you get here;
|
|
2327
|
+
// if you get here, and you want to delete the row, then the
|
|
2328
|
+
// table will go.
|
|
2329
|
+
|
|
2330
|
+
this.model.tables.delete(table.name.toLowerCase());
|
|
2331
|
+
|
|
2332
|
+
for (let row = command.before_row; row <= table.area.end.row; row++) {
|
|
2333
|
+
for (let column = table.area.start.column; column <= table.area.end.column; column++) {
|
|
2334
|
+
const cell = target_sheet.CellData({row, column});
|
|
2335
|
+
if (cell.table === table) {
|
|
2336
|
+
cell.table = undefined;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
else if (command.before_row <= table.area.end.row) {
|
|
2344
|
+
// remove table rows from the end. cap.
|
|
2345
|
+
// we may be removing the totals row -- in that case, update the table to reflect.
|
|
2346
|
+
|
|
2347
|
+
if (command.before_row - command.count > table.area.end.row) {
|
|
2348
|
+
table.totals_row = false;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
table.area.end.row = Math.max(0, table.area.end.row + command.count, command.before_row - 1);
|
|
2352
|
+
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
|
|
2360
|
+
this.model.named_ranges.PatchNamedRanges(target_sheet.id, 0, 0, command.before_row, command.count);
|
|
2361
|
+
|
|
2362
|
+
const target_sheet_name = target_sheet.name.toLowerCase();
|
|
2363
|
+
|
|
2364
|
+
for (const sheet of this.model.sheets.list) {
|
|
2365
|
+
const is_target = sheet === target_sheet;
|
|
2366
|
+
|
|
2367
|
+
sheet.cells.IterateAll((cell: Cell) => {
|
|
2368
|
+
if (cell.ValueIsFormula()) {
|
|
2369
|
+
const modified = this.PatchFormulasInternal(cell.value || '',
|
|
2370
|
+
command.before_row, command.count, 0, 0,
|
|
2371
|
+
target_sheet_name, is_target);
|
|
2372
|
+
if (modified) {
|
|
2373
|
+
cell.value = modified;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
for (const annotation of sheet.annotations) {
|
|
2379
|
+
if (annotation.formula) {
|
|
2380
|
+
const modified = this.PatchFormulasInternal(annotation.formula || '',
|
|
2381
|
+
command.before_row, command.count, 0, 0,
|
|
2382
|
+
target_sheet_name, is_target);
|
|
2383
|
+
if (modified) {
|
|
2384
|
+
annotation.formula = modified;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
// annotations
|
|
2393
|
+
|
|
2394
|
+
const update_annotations_list: Annotation[] = [];
|
|
2395
|
+
const resize_annotations_list: Annotation[] = [];
|
|
2396
|
+
const delete_annotations_list: Annotation[] = [];
|
|
2397
|
+
|
|
2398
|
+
if (command.count > 0) { // insert
|
|
2399
|
+
|
|
2400
|
+
const first = command.before_row;
|
|
2401
|
+
|
|
2402
|
+
for (const annotation of target_sheet.annotations) {
|
|
2403
|
+
if (annotation.layout) {
|
|
2404
|
+
const [start, end, endy] = [
|
|
2405
|
+
annotation.layout.tl.address.row,
|
|
2406
|
+
annotation.layout.br.address.row,
|
|
2407
|
+
annotation.layout.br.offset.y,
|
|
2408
|
+
];
|
|
2409
|
+
|
|
2410
|
+
if (first <= start ) {
|
|
2411
|
+
|
|
2412
|
+
// start case 1: starts above the annotation (including exactly at the top)
|
|
2413
|
+
|
|
2414
|
+
// shift
|
|
2415
|
+
annotation.layout.tl.address.row += command.count;
|
|
2416
|
+
annotation.layout.br.address.row += command.count;
|
|
2417
|
+
|
|
2418
|
+
}
|
|
2419
|
+
else if (first < end || first === end && endy > 0) {
|
|
2420
|
+
|
|
2421
|
+
// start case 2: starts in the annotation, omitting the first row
|
|
2422
|
+
|
|
2423
|
+
annotation.layout.br.address.row += command.count;
|
|
2424
|
+
|
|
2425
|
+
// size changing
|
|
2426
|
+
resize_annotations_list.push(annotation);
|
|
2427
|
+
|
|
2428
|
+
}
|
|
2429
|
+
else {
|
|
2430
|
+
|
|
2431
|
+
// do nothing
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
update_annotations_list.push(annotation);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
}
|
|
2440
|
+
else if (command.count < 0) { // delete
|
|
2441
|
+
|
|
2442
|
+
// first and last column deleted
|
|
2443
|
+
|
|
2444
|
+
const first = command.before_row;
|
|
2445
|
+
const last = command.before_row - command.count - 1;
|
|
2446
|
+
|
|
2447
|
+
for (const annotation of target_sheet.annotations) {
|
|
2448
|
+
if (annotation.layout) {
|
|
2449
|
+
|
|
2450
|
+
// start and end row of the annotation. recall that in
|
|
2451
|
+
// this layout, the annotation may extend into the (first,last)
|
|
2452
|
+
// row but not beyond it. the offset is _within_ the row.
|
|
2453
|
+
|
|
2454
|
+
const [start, end, endy] = [
|
|
2455
|
+
annotation.layout.tl.address.row,
|
|
2456
|
+
annotation.layout.br.address.row,
|
|
2457
|
+
annotation.layout.br.offset.y,
|
|
2458
|
+
];
|
|
2459
|
+
|
|
2460
|
+
if (first <= start ) {
|
|
2461
|
+
|
|
2462
|
+
// start case 1: starts above the annotation (including exactly at the top)
|
|
2463
|
+
|
|
2464
|
+
if (last < start) {
|
|
2465
|
+
|
|
2466
|
+
// end case 1: ends before the annotation
|
|
2467
|
+
|
|
2468
|
+
// shift
|
|
2469
|
+
annotation.layout.tl.address.row += command.count;
|
|
2470
|
+
annotation.layout.br.address.row += command.count;
|
|
2471
|
+
|
|
2472
|
+
}
|
|
2473
|
+
else if (last < end - 1 || (last === end -1 && endy > 0)) {
|
|
2474
|
+
|
|
2475
|
+
// end case 2: ends before the end of the annotation
|
|
2476
|
+
|
|
2477
|
+
// shift + cut
|
|
2478
|
+
annotation.layout.tl.address.row = first;
|
|
2479
|
+
annotation.layout.tl.offset.y = 0;
|
|
2480
|
+
annotation.layout.br.address.row += command.count;
|
|
2481
|
+
|
|
2482
|
+
// size changing
|
|
2483
|
+
resize_annotations_list.push(annotation);
|
|
2484
|
+
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
|
|
2488
|
+
// end case 3: ends after the annotation
|
|
2489
|
+
|
|
2490
|
+
// drop the annotation
|
|
2491
|
+
delete_annotations_list.push(annotation);
|
|
2492
|
+
continue;
|
|
2493
|
+
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
}
|
|
2497
|
+
else if (first < end || first === end && endy > 0) {
|
|
2498
|
+
|
|
2499
|
+
// start case 2: starts in the annotation, omitting the first row
|
|
2500
|
+
|
|
2501
|
+
if (last < end - 1 || (last === end -1 && endy > 0)) {
|
|
2502
|
+
|
|
2503
|
+
// end case 2: ends before the end of the annotation
|
|
2504
|
+
|
|
2505
|
+
// shorten
|
|
2506
|
+
annotation.layout.br.address.row += command.count;
|
|
2507
|
+
|
|
2508
|
+
// size changing
|
|
2509
|
+
resize_annotations_list.push(annotation);
|
|
2510
|
+
|
|
2511
|
+
}
|
|
2512
|
+
else {
|
|
2513
|
+
|
|
2514
|
+
// end case 3: ends after the annotation
|
|
2515
|
+
|
|
2516
|
+
// clip
|
|
2517
|
+
annotation.layout.br.address.row = first;
|
|
2518
|
+
annotation.layout.br.offset.y = 0;
|
|
2519
|
+
|
|
2520
|
+
// size changing
|
|
2521
|
+
resize_annotations_list.push(annotation);
|
|
2522
|
+
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
}
|
|
2526
|
+
else {
|
|
2527
|
+
|
|
2528
|
+
// start case 3: starts after the annotation
|
|
2529
|
+
|
|
2530
|
+
// do nothing
|
|
2531
|
+
|
|
2532
|
+
continue;
|
|
2533
|
+
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
update_annotations_list.push(annotation);
|
|
2537
|
+
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
for (const annotation of delete_annotations_list) {
|
|
2544
|
+
target_sheet.annotations = target_sheet.annotations.filter(test => test !== annotation);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
return {
|
|
2548
|
+
update_annotations_list,
|
|
2549
|
+
resize_annotations_list,
|
|
2550
|
+
delete_annotations_list,
|
|
2551
|
+
};
|
|
2552
|
+
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/**
|
|
2556
|
+
*
|
|
2557
|
+
*/
|
|
2558
|
+
protected InsertColumnsInternal(command: InsertColumnsCommand): {
|
|
2559
|
+
error?: boolean;
|
|
2560
|
+
update_annotations_list?: Annotation[];
|
|
2561
|
+
resize_annotations_list?: Annotation[];
|
|
2562
|
+
delete_annotations_list?: Annotation[];
|
|
2563
|
+
} {
|
|
2564
|
+
|
|
2565
|
+
const target_sheet = this.FindSheet(command.sheet_id);
|
|
2566
|
+
|
|
2567
|
+
// it seems like we never get an insert infinity. not sure why,
|
|
2568
|
+
// but the UI is blocking that. we should handle it anyway jic
|
|
2569
|
+
|
|
2570
|
+
if (command.count === Infinity) {
|
|
2571
|
+
command.count = 1; // ?
|
|
2572
|
+
}
|
|
2573
|
+
else if (command.count === -Infinity) {
|
|
2574
|
+
command.count = -target_sheet.columns; // delete all
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// FIXME: we need to get this error out earlier. before this call,
|
|
2578
|
+
// in the call that generates the insert event. otherwise if we
|
|
2579
|
+
// have remotes, everyone will see the error -- we only want the
|
|
2580
|
+
// actual actor to see the error.
|
|
2581
|
+
|
|
2582
|
+
if (!target_sheet.InsertColumns(command.before_column, command.count)) {
|
|
2583
|
+
// this.Error(`You can't change part of an array.`);
|
|
2584
|
+
this.Error(ErrorCode.array);
|
|
2585
|
+
return { error: true };
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// patch tables. we removed this from the sheet routine entirely,
|
|
2589
|
+
// we need to rebuild any affected tables now.
|
|
2590
|
+
|
|
2591
|
+
// NOTE: we may drop tables, so we can't use a live iterator. or
|
|
2592
|
+
// is the iterator precomputed? not sure. let's flatten immediately jic.
|
|
2593
|
+
|
|
2594
|
+
const tables = Array.from(this.model.tables.values());
|
|
2595
|
+
|
|
2596
|
+
for (const table of tables) {
|
|
2597
|
+
if (table.area.start.sheet_id === command.sheet_id) {
|
|
2598
|
+
|
|
2599
|
+
if (command.count > 0) {
|
|
2600
|
+
if (command.before_column <= table.area.start.column) {
|
|
2601
|
+
// shift the table to the right. update the table reference,
|
|
2602
|
+
// we can skip updating headers as the columns haven't changed.
|
|
2603
|
+
|
|
2604
|
+
// console.info("shift table right");
|
|
2605
|
+
table.area.start.column += command.count;
|
|
2606
|
+
table.area.end.column += command.count;
|
|
2607
|
+
|
|
2608
|
+
}
|
|
2609
|
+
else if (command.before_column <= table.area.end.column) {
|
|
2610
|
+
// insert columns -- we need to add references to new
|
|
2611
|
+
// cells, and update headers.
|
|
2612
|
+
|
|
2613
|
+
// console.info("insert table columns");
|
|
2614
|
+
table.area.end.column += command.count;
|
|
2615
|
+
for (let row = table.area.start.row; row <= table.area.end.row; row++) {
|
|
2616
|
+
for (let column = table.area.start.column; column <= table.area.end.column; column++) {
|
|
2617
|
+
const cell = target_sheet.CellData({row, column});
|
|
2618
|
+
cell.table = table;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
this.UpdateTableColumns(table);
|
|
2622
|
+
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
else {
|
|
2626
|
+
if (command.before_column <= table.area.start.column) {
|
|
2627
|
+
if (command.before_column - command.count <= table.area.start.column) {
|
|
2628
|
+
// shift table left. update the table reference, we can skip headers.
|
|
2629
|
+
|
|
2630
|
+
// console.info("shift table left");
|
|
2631
|
+
table.area.start.column += command.count;
|
|
2632
|
+
table.area.end.column += command.count;
|
|
2633
|
+
|
|
2634
|
+
}
|
|
2635
|
+
else if (command.before_column - command.count >= table.area.end.column ){
|
|
2636
|
+
// remove entire table. cells are already removed, we can just
|
|
2637
|
+
// drop the table from the model.
|
|
2638
|
+
|
|
2639
|
+
// console.info("remove table");
|
|
2640
|
+
this.model.tables.delete(table.name.toLowerCase());
|
|
2641
|
+
|
|
2642
|
+
}
|
|
2643
|
+
else {
|
|
2644
|
+
// shift to the left, then remove table columns. cells are
|
|
2645
|
+
// already removed, so we don't need to touch cells; just
|
|
2646
|
+
// update the reference and column headers.
|
|
2647
|
+
|
|
2648
|
+
// console.info("remove table columns (1)");
|
|
2649
|
+
table.area.start.column = command.before_column;
|
|
2650
|
+
table.area.end.column += command.count;
|
|
2651
|
+
this.UpdateTableColumns(table);
|
|
2652
|
+
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
else if (command.before_column <= table.area.end.column) {
|
|
2656
|
+
// remove table columns. as above. cap.
|
|
2657
|
+
|
|
2658
|
+
// console.info("remove table columns (2)");
|
|
2659
|
+
table.area.end.column = Math.max(0, table.area.end.column + command.count, command.before_column - 1);
|
|
2660
|
+
this.UpdateTableColumns(table);
|
|
2661
|
+
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
|
|
2669
|
+
this.model.named_ranges.PatchNamedRanges(target_sheet.id, command.before_column, command.count, 0, 0);
|
|
2670
|
+
|
|
2671
|
+
// FIXME: we need an event here?
|
|
2672
|
+
|
|
2673
|
+
// A: caller sends a "structure" event after this call. that doesn't include
|
|
2674
|
+
// affected areas, though. need to think about whether structure event
|
|
2675
|
+
// triggers a recalc (probably should). we could track whether we've made
|
|
2676
|
+
// any modifications (and maybe also whether we now have any invalid
|
|
2677
|
+
// references)
|
|
2678
|
+
|
|
2679
|
+
// patch all sheets
|
|
2680
|
+
|
|
2681
|
+
// you know we have a calculator that has backward-and-forward references.
|
|
2682
|
+
// we could theoretically ask the calculator what needs to be changed.
|
|
2683
|
+
//
|
|
2684
|
+
// for the most part, we try to maintain separation between the display
|
|
2685
|
+
// (this) and the calculator. we could ask, but this isn't terrible and
|
|
2686
|
+
// helps maintain that separation.
|
|
2687
|
+
|
|
2688
|
+
const target_sheet_name = target_sheet.name.toLowerCase();
|
|
2689
|
+
|
|
2690
|
+
for (const sheet of this.model.sheets.list) {
|
|
2691
|
+
const is_target = sheet === target_sheet;
|
|
2692
|
+
|
|
2693
|
+
sheet.cells.IterateAll((cell: Cell) => {
|
|
2694
|
+
if (cell.ValueIsFormula()) {
|
|
2695
|
+
const modified = this.PatchFormulasInternal(cell.value || '', 0, 0,
|
|
2696
|
+
command.before_column, command.count,
|
|
2697
|
+
target_sheet_name, is_target);
|
|
2698
|
+
if (modified) {
|
|
2699
|
+
cell.value = modified;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
for (const annotation of sheet.annotations) {
|
|
2705
|
+
if (annotation.formula) {
|
|
2706
|
+
const modified = this.PatchFormulasInternal(annotation.formula,
|
|
2707
|
+
0, 0, command.before_column, command.count,
|
|
2708
|
+
target_sheet_name, is_target);
|
|
2709
|
+
if (modified) {
|
|
2710
|
+
annotation.formula = modified;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// annotations
|
|
2718
|
+
|
|
2719
|
+
const update_annotations_list: Annotation[] = [];
|
|
2720
|
+
const resize_annotations_list: Annotation[] = [];
|
|
2721
|
+
const delete_annotations_list: Annotation[] = [];
|
|
2722
|
+
|
|
2723
|
+
if (command.count > 0) { // insert
|
|
2724
|
+
|
|
2725
|
+
const first = command.before_column;
|
|
2726
|
+
|
|
2727
|
+
for (const annotation of target_sheet.annotations) {
|
|
2728
|
+
if (annotation.layout) {
|
|
2729
|
+
const [start, end, endx] = [
|
|
2730
|
+
annotation.layout.tl.address.column,
|
|
2731
|
+
annotation.layout.br.address.column,
|
|
2732
|
+
annotation.layout.br.offset.x,
|
|
2733
|
+
];
|
|
2734
|
+
|
|
2735
|
+
if (first <= start ) {
|
|
2736
|
+
|
|
2737
|
+
// start case 1: starts to the left of the annotation (including exactly at the left)
|
|
2738
|
+
|
|
2739
|
+
// shift
|
|
2740
|
+
annotation.layout.tl.address.column += command.count;
|
|
2741
|
+
annotation.layout.br.address.column += command.count;
|
|
2742
|
+
|
|
2743
|
+
}
|
|
2744
|
+
else if (first < end || first === end && endx > 0) {
|
|
2745
|
+
|
|
2746
|
+
// start case 2: starts in the annotation, omitting the first column
|
|
2747
|
+
|
|
2748
|
+
annotation.layout.br.address.column += command.count;
|
|
2749
|
+
|
|
2750
|
+
// size changing
|
|
2751
|
+
resize_annotations_list.push(annotation);
|
|
2752
|
+
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
|
|
2756
|
+
// do nothing
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
update_annotations_list.push(annotation);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
}
|
|
2765
|
+
else if (command.count < 0) { // delete
|
|
2766
|
+
|
|
2767
|
+
// first and last column deleted
|
|
2768
|
+
|
|
2769
|
+
const first = command.before_column;
|
|
2770
|
+
const last = command.before_column - command.count - 1;
|
|
2771
|
+
|
|
2772
|
+
for (const annotation of target_sheet.annotations) {
|
|
2773
|
+
if (annotation.layout) {
|
|
2774
|
+
|
|
2775
|
+
// start and end column of the annotation. recall that in
|
|
2776
|
+
// this layout, the annotation may extend into the (first,last)
|
|
2777
|
+
// column but not beyond it. the offset is _within_ the column.
|
|
2778
|
+
|
|
2779
|
+
const [start, end, endx] = [
|
|
2780
|
+
annotation.layout.tl.address.column,
|
|
2781
|
+
annotation.layout.br.address.column,
|
|
2782
|
+
annotation.layout.br.offset.x,
|
|
2783
|
+
];
|
|
2784
|
+
|
|
2785
|
+
if (first <= start ) {
|
|
2786
|
+
|
|
2787
|
+
// start case 1: starts to the left of the annotation (including exactly at the left)
|
|
2788
|
+
|
|
2789
|
+
if (last < start) {
|
|
2790
|
+
|
|
2791
|
+
// end case 1: ends before the annotation
|
|
2792
|
+
|
|
2793
|
+
// shift
|
|
2794
|
+
annotation.layout.tl.address.column += command.count;
|
|
2795
|
+
annotation.layout.br.address.column += command.count;
|
|
2796
|
+
|
|
2797
|
+
}
|
|
2798
|
+
else if (last < end - 1 || (last === end -1 && endx > 0)) {
|
|
2799
|
+
|
|
2800
|
+
// end case 2: ends before the end of the annotation
|
|
2801
|
+
|
|
2802
|
+
// shift + cut
|
|
2803
|
+
annotation.layout.tl.address.column = first;
|
|
2804
|
+
annotation.layout.tl.offset.x = 0;
|
|
2805
|
+
annotation.layout.br.address.column += command.count;
|
|
2806
|
+
|
|
2807
|
+
// size changing
|
|
2808
|
+
resize_annotations_list.push(annotation);
|
|
2809
|
+
|
|
2810
|
+
}
|
|
2811
|
+
else {
|
|
2812
|
+
|
|
2813
|
+
// end case 3: ends after the annotation
|
|
2814
|
+
|
|
2815
|
+
// drop the annotation
|
|
2816
|
+
delete_annotations_list.push(annotation);
|
|
2817
|
+
continue;
|
|
2818
|
+
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
}
|
|
2822
|
+
else if (first < end || first === end && endx > 0) {
|
|
2823
|
+
|
|
2824
|
+
// start case 2: starts in the annotation, omitting the first column
|
|
2825
|
+
|
|
2826
|
+
if (last < end - 1 || (last === end -1 && endx > 0)) {
|
|
2827
|
+
|
|
2828
|
+
// end case 2: ends before the end of the annotation
|
|
2829
|
+
|
|
2830
|
+
// shorten
|
|
2831
|
+
annotation.layout.br.address.column += command.count;
|
|
2832
|
+
|
|
2833
|
+
// size changing
|
|
2834
|
+
resize_annotations_list.push(annotation);
|
|
2835
|
+
|
|
2836
|
+
}
|
|
2837
|
+
else {
|
|
2838
|
+
|
|
2839
|
+
// end case 3: ends after the annotation
|
|
2840
|
+
|
|
2841
|
+
// clip
|
|
2842
|
+
annotation.layout.br.address.column = first;
|
|
2843
|
+
annotation.layout.br.offset.x = 0;
|
|
2844
|
+
|
|
2845
|
+
// size changing
|
|
2846
|
+
resize_annotations_list.push(annotation);
|
|
2847
|
+
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
|
|
2853
|
+
// start case 3: starts after the annotation
|
|
2854
|
+
|
|
2855
|
+
// do nothing
|
|
2856
|
+
|
|
2857
|
+
continue;
|
|
2858
|
+
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
update_annotations_list.push(annotation);
|
|
2862
|
+
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
for (const annotation of delete_annotations_list) {
|
|
2869
|
+
target_sheet.annotations = target_sheet.annotations.filter(test => test !== annotation);
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
return {
|
|
2873
|
+
update_annotations_list,
|
|
2874
|
+
resize_annotations_list,
|
|
2875
|
+
delete_annotations_list,
|
|
2876
|
+
};
|
|
2877
|
+
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
|
|
2881
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
2882
|
+
|
|
2883
|
+
/**
|
|
2884
|
+
* pass all data/style/structure operations through a command mechanism.
|
|
2885
|
+
* this method should optimally act as a dispatcher, so try to minimize
|
|
2886
|
+
* inline code in favor of method calls.
|
|
2887
|
+
*
|
|
2888
|
+
* [NOTE: don't go crazy with that, some simple operations can be inlined]
|
|
2889
|
+
*
|
|
2890
|
+
* NOTE: working on coediting. we will need to handle different sheets.
|
|
2891
|
+
* going to work one command at a time...
|
|
2892
|
+
*
|
|
2893
|
+
* @param queue -- push on the command log. this is default true so it
|
|
2894
|
+
* doesn't change existing behavior, but you can turn it off if the message
|
|
2895
|
+
* comes from a remote queue.
|
|
2896
|
+
*
|
|
2897
|
+
*/
|
|
2898
|
+
public ExecCommand(commands: Command | Command[], queue = true): UpdateFlags {
|
|
2899
|
+
|
|
2900
|
+
// FIXME: support ephemeral commands (...)
|
|
2901
|
+
|
|
2902
|
+
// data and style events were triggered by the areas being set.
|
|
2903
|
+
// we are not necessarily setting them for offsheet changes, so
|
|
2904
|
+
// we need an explicit flag. this should be logically OR'ed with
|
|
2905
|
+
// the area existing (for purposes of sending an event).
|
|
2906
|
+
|
|
2907
|
+
// all flags/areas moved to this struct
|
|
2908
|
+
|
|
2909
|
+
const flags: UpdateFlags = {
|
|
2910
|
+
pending: [],
|
|
2911
|
+
};
|
|
2912
|
+
|
|
2913
|
+
const events: GridEvent[] = [];
|
|
2914
|
+
|
|
2915
|
+
// should we normalize always, or only if we're queueing?
|
|
2916
|
+
// it seems like it's useful here, then we can be a little
|
|
2917
|
+
// sloppier in the actual handlers. after normalizing, any
|
|
2918
|
+
// command that has an address/area (or sheet ID parameter)
|
|
2919
|
+
// will have an explicit sheet ID.
|
|
2920
|
+
|
|
2921
|
+
commands = this.NormalizeCommands(commands);
|
|
2922
|
+
|
|
2923
|
+
// FIXME: we should queue later, so we can remove any commands
|
|
2924
|
+
// that fail... throw errors, and so on
|
|
2925
|
+
|
|
2926
|
+
if (queue) {
|
|
2927
|
+
this.command_log.Publish({ command: commands, timestamp: new Date().getTime() });
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
for (const command of commands) {
|
|
2931
|
+
|
|
2932
|
+
// console.log(CommandKey[command.key], JSON.stringify(command));
|
|
2933
|
+
|
|
2934
|
+
switch (command.key) {
|
|
2935
|
+
case CommandKey.Reset:
|
|
2936
|
+
|
|
2937
|
+
// not sure how well this fits in with the command queue. it
|
|
2938
|
+
// doesn't look like it sends any events, so what's the point?
|
|
2939
|
+
// just to get a command log event?
|
|
2940
|
+
|
|
2941
|
+
// the problem is that load doesn't run through the queue, so
|
|
2942
|
+
// even if you did a reset -> load we'd just get the reset part.
|
|
2943
|
+
|
|
2944
|
+
// ...
|
|
2945
|
+
|
|
2946
|
+
// OK, actually this is used in the CSV import routine. we need
|
|
2947
|
+
// to support it until we get rid of that (it needs to move).
|
|
2948
|
+
|
|
2949
|
+
this.ResetInternal();
|
|
2950
|
+
break;
|
|
2951
|
+
|
|
2952
|
+
case CommandKey.Clear:
|
|
2953
|
+
if (command.area) {
|
|
2954
|
+
const area = new Area(command.area.start, command.area.end);
|
|
2955
|
+
this.ClearAreaInternal(area);
|
|
2956
|
+
flags.data_area = Area.Join(area, flags.data_area);
|
|
2957
|
+
flags.formula = true;
|
|
2958
|
+
}
|
|
2959
|
+
break;
|
|
2960
|
+
|
|
2961
|
+
case CommandKey.Select:
|
|
2962
|
+
|
|
2963
|
+
// nobody (except one routine) is using commands for selection.
|
|
2964
|
+
// not sure why or why not, or if that's a problem. (it's definitely
|
|
2965
|
+
// a problem if we are recording the log for playback)
|
|
2966
|
+
|
|
2967
|
+
// ATM the base class is just going to do nothing.
|
|
2968
|
+
|
|
2969
|
+
this.SelectInternal(command);
|
|
2970
|
+
|
|
2971
|
+
break;
|
|
2972
|
+
|
|
2973
|
+
case CommandKey.Freeze:
|
|
2974
|
+
|
|
2975
|
+
// COEDITING: ok
|
|
2976
|
+
|
|
2977
|
+
this.FreezeInternal(command);
|
|
2978
|
+
|
|
2979
|
+
// is the event necessary here? not sure. we were sending it as a
|
|
2980
|
+
// side effect, so it was added here in case there was some reason
|
|
2981
|
+
// it was necessary. at a minimum, it should not require a rebuild
|
|
2982
|
+
// because no addresses change. (although we leave it in case someone
|
|
2983
|
+
// else sets it).)
|
|
2984
|
+
|
|
2985
|
+
flags.structure_event = true;
|
|
2986
|
+
|
|
2987
|
+
break;
|
|
2988
|
+
|
|
2989
|
+
case CommandKey.InsertTable:
|
|
2990
|
+
|
|
2991
|
+
// the most important thing here is validating that we can
|
|
2992
|
+
// create the table in the target area.
|
|
2993
|
+
|
|
2994
|
+
{
|
|
2995
|
+
const sheet = this.FindSheet(command.area);
|
|
2996
|
+
const area = new Area(command.area.start, command.area.end);
|
|
2997
|
+
|
|
2998
|
+
// validate first
|
|
2999
|
+
|
|
3000
|
+
let valid = true;
|
|
3001
|
+
|
|
3002
|
+
validation_loop:
|
|
3003
|
+
for (let row = area.start.row; row <= area.end.row; row++) {
|
|
3004
|
+
for (let column = area.start.column; column <= area.end.column; column++) {
|
|
3005
|
+
const cell = sheet.cells.GetCell({row, column}, false);
|
|
3006
|
+
if (cell && (cell.area || cell.merge_area || cell.table)) {
|
|
3007
|
+
valid = false;
|
|
3008
|
+
break validation_loop;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
if (valid) {
|
|
3014
|
+
|
|
3015
|
+
// we need a name for the table. needs to be unique.
|
|
3016
|
+
|
|
3017
|
+
let index = this.model.tables.size + 1;
|
|
3018
|
+
let name = '';
|
|
3019
|
+
|
|
3020
|
+
for (;;) {
|
|
3021
|
+
name = `Table${index++}`;
|
|
3022
|
+
if (!this.model.tables.has(name.toLowerCase())) {
|
|
3023
|
+
break;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
const table: Table = {
|
|
3028
|
+
area: command.area,
|
|
3029
|
+
name,
|
|
3030
|
+
sortable: command.sortable, // defaults to true if !present
|
|
3031
|
+
theme: command.theme,
|
|
3032
|
+
};
|
|
3033
|
+
|
|
3034
|
+
if (command.totals) {
|
|
3035
|
+
table.totals_row = true;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
this.model.tables.set(name.toLowerCase(), table);
|
|
3039
|
+
|
|
3040
|
+
for (let row = area.start.row; row <= area.end.row; row++) {
|
|
3041
|
+
for (let column = area.start.column; column <= area.end.column; column++) {
|
|
3042
|
+
const cell = sheet.cells.GetCell({row, column}, true);
|
|
3043
|
+
cell.table = table;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
this.UpdateTableColumns(table);
|
|
3048
|
+
|
|
3049
|
+
// force rerendering, we don't need to flush the values
|
|
3050
|
+
|
|
3051
|
+
sheet.Invalidate(new Area(table.area.start, table.area.end));
|
|
3052
|
+
|
|
3053
|
+
if (sheet === this.active_sheet) {
|
|
3054
|
+
flags.style_area = Area.Join(area, flags.style_area);
|
|
3055
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3056
|
+
|
|
3057
|
+
}
|
|
3058
|
+
else {
|
|
3059
|
+
flags.style_event = true;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
break;
|
|
3067
|
+
|
|
3068
|
+
|
|
3069
|
+
case CommandKey.RemoveTable:
|
|
3070
|
+
|
|
3071
|
+
// this is pretty easy, we can do it inline
|
|
3072
|
+
|
|
3073
|
+
{
|
|
3074
|
+
const sheet = this.FindSheet(command.table.area);
|
|
3075
|
+
const area = new Area(command.table.area.start, command.table.area.end);
|
|
3076
|
+
|
|
3077
|
+
for (let row = area.start.row; row <= area.end.row; row++) {
|
|
3078
|
+
for (let column = area.start.column; column <= area.end.column; column++) {
|
|
3079
|
+
const cell = sheet.cells.GetCell({row, column}, false);
|
|
3080
|
+
if (cell) {
|
|
3081
|
+
cell.table = undefined;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// drop from model
|
|
3087
|
+
|
|
3088
|
+
// console.info('deleting...', command.table.name);
|
|
3089
|
+
this.model.tables.delete(command.table.name.toLowerCase());
|
|
3090
|
+
|
|
3091
|
+
// tables use nonstandard styling, we need to invalidate the sheet.
|
|
3092
|
+
// for edges invalidate an extra cell around the table
|
|
3093
|
+
|
|
3094
|
+
const invalid = sheet.RealArea(area.Clone().Shift(-1, -1).Resize(area.rows + 2, area.columns + 2));
|
|
3095
|
+
sheet.Invalidate(invalid);
|
|
3096
|
+
|
|
3097
|
+
if (sheet === this.active_sheet) {
|
|
3098
|
+
flags.style_area = Area.Join(area, flags.style_area);
|
|
3099
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3100
|
+
}
|
|
3101
|
+
else {
|
|
3102
|
+
flags.style_event = true;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
break;
|
|
3108
|
+
|
|
3109
|
+
case CommandKey.MergeCells:
|
|
3110
|
+
{
|
|
3111
|
+
// COEDITING: ok
|
|
3112
|
+
|
|
3113
|
+
const sheet = this.FindSheet(command.area);
|
|
3114
|
+
|
|
3115
|
+
sheet.MergeCells(
|
|
3116
|
+
new Area(command.area.start, command.area.end));
|
|
3117
|
+
|
|
3118
|
+
// sheet publishes a data event here, too. probably a good
|
|
3119
|
+
// idea because references to the secondary (non-head) merge
|
|
3120
|
+
// cells will break.
|
|
3121
|
+
|
|
3122
|
+
flags.structure_event = true;
|
|
3123
|
+
flags.structure_rebuild_required = true;
|
|
3124
|
+
|
|
3125
|
+
if (sheet === this.active_sheet) {
|
|
3126
|
+
flags.data_area = Area.Join(command.area, flags.data_area);
|
|
3127
|
+
flags.render_area = Area.Join(command.area, flags.render_area);
|
|
3128
|
+
}
|
|
3129
|
+
else {
|
|
3130
|
+
flags.data_event = true;
|
|
3131
|
+
// this.pending_layout_update.add(sheet.id);
|
|
3132
|
+
if (!flags.pending) {
|
|
3133
|
+
flags.pending = [];
|
|
3134
|
+
}
|
|
3135
|
+
flags.pending.push(sheet.id);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
break;
|
|
3140
|
+
|
|
3141
|
+
case CommandKey.UnmergeCells:
|
|
3142
|
+
{
|
|
3143
|
+
// COEDITING: ok
|
|
3144
|
+
|
|
3145
|
+
// the sheet unmerge routine requires a single, contiguous merge area.
|
|
3146
|
+
// we want to support multiple unmerges at the same time, though,
|
|
3147
|
+
// so let's check for multiple. create a list.
|
|
3148
|
+
|
|
3149
|
+
// FIXME: use a set
|
|
3150
|
+
|
|
3151
|
+
const sheet = this.FindSheet(command.area);
|
|
3152
|
+
const list: Record<string, Area> = {};
|
|
3153
|
+
const area = new Area(command.area.start, command.area.end);
|
|
3154
|
+
|
|
3155
|
+
sheet.cells.Apply(area, (cell: Cell) => {
|
|
3156
|
+
if (cell.merge_area) {
|
|
3157
|
+
const label = Area.CellAddressToLabel(cell.merge_area.start) + ':'
|
|
3158
|
+
+ Area.CellAddressToLabel(cell.merge_area.end);
|
|
3159
|
+
list[label] = cell.merge_area;
|
|
3160
|
+
}
|
|
3161
|
+
}, false);
|
|
3162
|
+
|
|
3163
|
+
const keys = Object.keys(list);
|
|
3164
|
+
|
|
3165
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3166
|
+
sheet.UnmergeCells(list[keys[i]]);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
// see above
|
|
3170
|
+
|
|
3171
|
+
if (sheet === this.active_sheet) {
|
|
3172
|
+
flags.render_area = Area.Join(command.area, flags.render_area);
|
|
3173
|
+
flags.data_area = Area.Join(command.area, flags.data_area);
|
|
3174
|
+
}
|
|
3175
|
+
else {
|
|
3176
|
+
flags.data_event = true;
|
|
3177
|
+
// this.pending_layout_update.add(sheet.id);
|
|
3178
|
+
if (!flags.pending) {
|
|
3179
|
+
flags.pending = [];
|
|
3180
|
+
}
|
|
3181
|
+
flags.pending.push(sheet.id);
|
|
3182
|
+
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
flags.structure_event = true;
|
|
3186
|
+
flags.structure_rebuild_required = true;
|
|
3187
|
+
}
|
|
3188
|
+
break;
|
|
3189
|
+
|
|
3190
|
+
case CommandKey.UpdateStyle:
|
|
3191
|
+
{
|
|
3192
|
+
// COEDITING: handles sheet ID properly
|
|
3193
|
+
|
|
3194
|
+
// to account for our background bleeding up/left, when applying
|
|
3195
|
+
// style changes we may need to render one additional row/column.
|
|
3196
|
+
|
|
3197
|
+
let area: Area|undefined;
|
|
3198
|
+
const sheet = this.FindSheet(command.area);
|
|
3199
|
+
|
|
3200
|
+
if (IsCellAddress(command.area)) {
|
|
3201
|
+
area = new Area(command.area);
|
|
3202
|
+
sheet.UpdateCellStyle(command.area, command.style, !!command.delta);
|
|
3203
|
+
}
|
|
3204
|
+
else {
|
|
3205
|
+
area = new Area(command.area.start, command.area.end);
|
|
3206
|
+
sheet.UpdateAreaStyle(area, command.style, !!command.delta);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
if (sheet === this.active_sheet) {
|
|
3210
|
+
flags.style_area = Area.Join(area, flags.style_area);
|
|
3211
|
+
|
|
3212
|
+
// we can limit bleed handling to cases where it's necessary...
|
|
3213
|
+
// if we really wanted to optimize we could call invalidate on .left, .top, &c
|
|
3214
|
+
|
|
3215
|
+
if (!command.delta
|
|
3216
|
+
|| command.style.fill
|
|
3217
|
+
|| command.style.border_top
|
|
3218
|
+
|| command.style.border_left
|
|
3219
|
+
|| command.style.border_right
|
|
3220
|
+
|| command.style.border_bottom) {
|
|
3221
|
+
|
|
3222
|
+
area = Area.Bleed(area); // bleed by 1 to account for borders/background
|
|
3223
|
+
this.active_sheet.Invalidate(area);
|
|
3224
|
+
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3228
|
+
|
|
3229
|
+
}
|
|
3230
|
+
else {
|
|
3231
|
+
flags.style_event = true;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
break;
|
|
3237
|
+
|
|
3238
|
+
case CommandKey.DataValidation:
|
|
3239
|
+
|
|
3240
|
+
// COEDITING: ok
|
|
3241
|
+
|
|
3242
|
+
this.SetValidationInternal(command);
|
|
3243
|
+
if (!command.area.sheet_id || command.area.sheet_id === this.active_sheet.id) {
|
|
3244
|
+
flags.render_area = Area.Join(new Area(command.area), flags.render_area);
|
|
3245
|
+
}
|
|
3246
|
+
break;
|
|
3247
|
+
|
|
3248
|
+
case CommandKey.SetName:
|
|
3249
|
+
|
|
3250
|
+
// it seems like we're allowing overwriting names if those
|
|
3251
|
+
// names exist as expressions or named ranges. however we
|
|
3252
|
+
// should not allow overriding a built-in function name (or
|
|
3253
|
+
// a macro function name?)
|
|
3254
|
+
|
|
3255
|
+
// FOR THE TIME BEING we're going to add that restriction to
|
|
3256
|
+
// the calling function, which (atm) is the only way to get here.
|
|
3257
|
+
|
|
3258
|
+
if (command.area) {
|
|
3259
|
+
|
|
3260
|
+
//if (this.model.named_expressions[command.name]) {
|
|
3261
|
+
// delete this.model.named_expressions[command.name];
|
|
3262
|
+
//}
|
|
3263
|
+
this.model.named_expressions.delete(command.name);
|
|
3264
|
+
|
|
3265
|
+
this.model.named_ranges.SetName(command.name,
|
|
3266
|
+
new Area(command.area.start, command.area.end));
|
|
3267
|
+
this.autocomplete_matcher.AddFunctions({
|
|
3268
|
+
type: DescriptorType.Token,
|
|
3269
|
+
name: command.name,
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
else if (command.expression) {
|
|
3273
|
+
this.model.named_ranges.ClearName(command.name);
|
|
3274
|
+
this.model.named_expressions.set(command.name, command.expression);
|
|
3275
|
+
this.autocomplete_matcher.AddFunctions({
|
|
3276
|
+
type: DescriptorType.Token,
|
|
3277
|
+
name: command.name,
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
else {
|
|
3281
|
+
this.model.named_ranges.ClearName(command.name);
|
|
3282
|
+
//if (this.model.named_expressions[command.name]) {
|
|
3283
|
+
// delete this.model.named_expressions[command.name];
|
|
3284
|
+
//}
|
|
3285
|
+
this.model.named_expressions.delete(command.name);
|
|
3286
|
+
|
|
3287
|
+
this.autocomplete_matcher.RemoveFunctions({
|
|
3288
|
+
type: DescriptorType.Token,
|
|
3289
|
+
name: command.name,
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
flags.structure_event = true;
|
|
3293
|
+
flags.structure_rebuild_required = true;
|
|
3294
|
+
break;
|
|
3295
|
+
|
|
3296
|
+
case CommandKey.UpdateBorders:
|
|
3297
|
+
{
|
|
3298
|
+
// COEDITING: ok
|
|
3299
|
+
|
|
3300
|
+
// UPDATE: actually had a problem with Area.Bleed dropping the
|
|
3301
|
+
// sheet ID. fixed.
|
|
3302
|
+
|
|
3303
|
+
const area = this.ApplyBordersInternal(command);
|
|
3304
|
+
|
|
3305
|
+
if (area.start.sheet_id === this.active_sheet.id) {
|
|
3306
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3307
|
+
flags.style_area = Area.Join(area, flags.style_area);
|
|
3308
|
+
}
|
|
3309
|
+
else {
|
|
3310
|
+
flags.style_event = true;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
}
|
|
3314
|
+
break;
|
|
3315
|
+
|
|
3316
|
+
case CommandKey.ShowSheet:
|
|
3317
|
+
|
|
3318
|
+
// COEDITING: we probably don't want this to pass through
|
|
3319
|
+
// when coediting, but it won't break anything. you can filter.
|
|
3320
|
+
|
|
3321
|
+
this.ShowSheetInternal(command);
|
|
3322
|
+
flags.sheets = true; // repaint tab bar
|
|
3323
|
+
flags.structure_event = true;
|
|
3324
|
+
break;
|
|
3325
|
+
|
|
3326
|
+
case CommandKey.ReorderSheet:
|
|
3327
|
+
{
|
|
3328
|
+
// COEDITING: seems OK, irrespective of active sheet
|
|
3329
|
+
|
|
3330
|
+
const sheets: Sheet[] = [];
|
|
3331
|
+
const target = this.model.sheets.list[command.index];
|
|
3332
|
+
|
|
3333
|
+
for (let i = 0; i < this.model.sheets.length; i++) {
|
|
3334
|
+
if (i !== command.index) {
|
|
3335
|
+
if (i === command.move_before) {
|
|
3336
|
+
sheets.push(target);
|
|
3337
|
+
}
|
|
3338
|
+
sheets.push(this.model.sheets.list[i]);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
if (command.move_before >= this.model.sheets.length) {
|
|
3343
|
+
sheets.push(target);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// this.model.sheets = sheets;
|
|
3347
|
+
this.model.sheets.Assign(sheets);
|
|
3348
|
+
|
|
3349
|
+
flags.sheets = true;
|
|
3350
|
+
flags.structure_event = true;
|
|
3351
|
+
|
|
3352
|
+
}
|
|
3353
|
+
break;
|
|
3354
|
+
|
|
3355
|
+
case CommandKey.RenameSheet:
|
|
3356
|
+
{
|
|
3357
|
+
// COEDITING: seems OK, irrespective of active sheet
|
|
3358
|
+
|
|
3359
|
+
const sheet = this.ResolveSheet(command);
|
|
3360
|
+
if (sheet) {
|
|
3361
|
+
this.RenameSheetInternal(sheet, command.new_name);
|
|
3362
|
+
flags.sheets = true;
|
|
3363
|
+
flags.structure_event = true;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
break;
|
|
3367
|
+
|
|
3368
|
+
case CommandKey.ResizeRows:
|
|
3369
|
+
|
|
3370
|
+
// moving this to a method so we can specialize: non-UI grid
|
|
3371
|
+
// should not support autosize (it can't)
|
|
3372
|
+
|
|
3373
|
+
// this may impact the SUBTOTAL function. which is dumb, but
|
|
3374
|
+
// there you go. so treat this as a data event for rows that
|
|
3375
|
+
// change visibility one way or the other.
|
|
3376
|
+
|
|
3377
|
+
// COEDITING: ok
|
|
3378
|
+
|
|
3379
|
+
{
|
|
3380
|
+
const area = this.ResizeRowsInternal(command);
|
|
3381
|
+
if (area) {
|
|
3382
|
+
if (area.start.sheet_id === this.active_sheet.id) {
|
|
3383
|
+
const real_area = this.active_sheet.RealArea(new Area(area.start, area.end));
|
|
3384
|
+
flags.render_area = Area.Join(real_area, flags.render_area);
|
|
3385
|
+
flags.data_area = Area.Join(real_area, flags.data_area);
|
|
3386
|
+
flags.data_event = true;
|
|
3387
|
+
}
|
|
3388
|
+
else {
|
|
3389
|
+
flags.data_event = true;
|
|
3390
|
+
if (!flags.pending) {
|
|
3391
|
+
flags.pending = [];
|
|
3392
|
+
}
|
|
3393
|
+
if (area.start.sheet_id) {
|
|
3394
|
+
flags.pending.push(area.start.sheet_id);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
flags.structure_event = true;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
break;
|
|
3402
|
+
|
|
3403
|
+
case CommandKey.ResizeColumns:
|
|
3404
|
+
|
|
3405
|
+
this.ResizeColumnsInternal(command);
|
|
3406
|
+
flags.structure_event = true;
|
|
3407
|
+
break;
|
|
3408
|
+
|
|
3409
|
+
case CommandKey.ShowHeaders:
|
|
3410
|
+
|
|
3411
|
+
// FIXME: now that we don't support 2-level headers (or anything
|
|
3412
|
+
// other than 1-level headers), headers should be managed by/move into
|
|
3413
|
+
// the grid class.
|
|
3414
|
+
|
|
3415
|
+
this.active_sheet.SetHeaderSize(command.show ? undefined : 1, command.show ? undefined : 1);
|
|
3416
|
+
this.flags.layout = true;
|
|
3417
|
+
this.flags.repaint = true;
|
|
3418
|
+
break;
|
|
3419
|
+
|
|
3420
|
+
case CommandKey.InsertRows:
|
|
3421
|
+
|
|
3422
|
+
// COEDITING: annotations are broken
|
|
3423
|
+
|
|
3424
|
+
this.InsertRowsInternal(command);
|
|
3425
|
+
flags.structure_event = true;
|
|
3426
|
+
flags.structure_rebuild_required = true;
|
|
3427
|
+
break;
|
|
3428
|
+
|
|
3429
|
+
case CommandKey.InsertColumns:
|
|
3430
|
+
|
|
3431
|
+
// COEDITING: annotations are broken
|
|
3432
|
+
|
|
3433
|
+
this.InsertColumnsInternal(command);
|
|
3434
|
+
flags.structure_event = true;
|
|
3435
|
+
flags.structure_rebuild_required = true;
|
|
3436
|
+
break;
|
|
3437
|
+
|
|
3438
|
+
case CommandKey.SetLink:
|
|
3439
|
+
case CommandKey.SetNote:
|
|
3440
|
+
{
|
|
3441
|
+
// COEDITING: ok
|
|
3442
|
+
|
|
3443
|
+
// note and link are basically the same, although there's a
|
|
3444
|
+
// method for setting note (not sure why)
|
|
3445
|
+
|
|
3446
|
+
const sheet = this.FindSheet(command.area);
|
|
3447
|
+
|
|
3448
|
+
let cell = sheet.cells.GetCell(command.area, true);
|
|
3449
|
+
if (cell) {
|
|
3450
|
+
|
|
3451
|
+
let area: Area;
|
|
3452
|
+
if (cell.merge_area) {
|
|
3453
|
+
area = new Area(cell.merge_area.start);
|
|
3454
|
+
cell = sheet.cells.GetCell(cell.merge_area.start, true);
|
|
3455
|
+
}
|
|
3456
|
+
else {
|
|
3457
|
+
area = new Area(command.area);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
if (command.key === CommandKey.SetNote) {
|
|
3461
|
+
cell.SetNote(command.note);
|
|
3462
|
+
}
|
|
3463
|
+
else {
|
|
3464
|
+
cell.hyperlink = command.reference || undefined;
|
|
3465
|
+
cell.render_clean = [];
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
if (sheet === this.active_sheet) {
|
|
3469
|
+
|
|
3470
|
+
// this isn't necessary because it's what the render area does
|
|
3471
|
+
// this.DelayedRender(false, area);
|
|
3472
|
+
|
|
3473
|
+
// treat this as style, because it affects painting but
|
|
3474
|
+
// does not require calculation.
|
|
3475
|
+
|
|
3476
|
+
flags.style_area = Area.Join(area, flags.style_area);
|
|
3477
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3478
|
+
|
|
3479
|
+
}
|
|
3480
|
+
else {
|
|
3481
|
+
flags.style_event = true;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
break;
|
|
3487
|
+
|
|
3488
|
+
case CommandKey.SortTable:
|
|
3489
|
+
{
|
|
3490
|
+
// console.info(command.table.area.spreadsheet_label);
|
|
3491
|
+
const area = this.SortTableInternal(command);
|
|
3492
|
+
|
|
3493
|
+
if (area && area.start.sheet_id === this.active_sheet.id) {
|
|
3494
|
+
|
|
3495
|
+
flags.data_area = Area.Join(area, flags.data_area);
|
|
3496
|
+
|
|
3497
|
+
// normally we don't paint, we wait for the calculator to resolve
|
|
3498
|
+
|
|
3499
|
+
if (this.options.repaint_on_cell_change) {
|
|
3500
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
}
|
|
3504
|
+
else {
|
|
3505
|
+
flags.data_event = true;
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
break;
|
|
3509
|
+
|
|
3510
|
+
|
|
3511
|
+
case CommandKey.SetRange:
|
|
3512
|
+
{
|
|
3513
|
+
// COEDITING: handles sheet ID properly
|
|
3514
|
+
// FIXME: areas should check sheet
|
|
3515
|
+
|
|
3516
|
+
// area could be undefined if there's an error
|
|
3517
|
+
// (try to change part of an array)
|
|
3518
|
+
|
|
3519
|
+
const area = this.SetRangeInternal(command, flags);
|
|
3520
|
+
|
|
3521
|
+
if (area) {
|
|
3522
|
+
const sheet = this.model.sheets.Find(area.start.sheet_id || this.active_sheet.id);
|
|
3523
|
+
const tables = sheet?.TablesFromArea(area, true) || [];
|
|
3524
|
+
for (const table of tables) {
|
|
3525
|
+
this.UpdateTableColumns(table);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
if (area && area.start.sheet_id === this.active_sheet.id) {
|
|
3530
|
+
|
|
3531
|
+
flags.data_area = Area.Join(area, flags.data_area);
|
|
3532
|
+
|
|
3533
|
+
// normally we don't paint, we wait for the calculator to resolve
|
|
3534
|
+
|
|
3535
|
+
if (this.options.repaint_on_cell_change) {
|
|
3536
|
+
flags.render_area = Area.Join(area, flags.render_area);
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
}
|
|
3540
|
+
else {
|
|
3541
|
+
flags.data_event = true;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
}
|
|
3545
|
+
break;
|
|
3546
|
+
|
|
3547
|
+
case CommandKey.DeleteSheet:
|
|
3548
|
+
|
|
3549
|
+
// COEDITING: looks fine
|
|
3550
|
+
|
|
3551
|
+
this.DeleteSheetInternal(command);
|
|
3552
|
+
flags.sheets = true;
|
|
3553
|
+
flags.structure_event = true;
|
|
3554
|
+
flags.structure_rebuild_required = true;
|
|
3555
|
+
break;
|
|
3556
|
+
|
|
3557
|
+
case CommandKey.DuplicateSheet:
|
|
3558
|
+
|
|
3559
|
+
// FIXME: what happens to named ranges? we don't have sheet-local names...
|
|
3560
|
+
|
|
3561
|
+
this.DuplicateSheetInternal(command);
|
|
3562
|
+
|
|
3563
|
+
flags.sheets = true;
|
|
3564
|
+
flags.structure_event = true;
|
|
3565
|
+
flags.structure_rebuild_required = true;
|
|
3566
|
+
break;
|
|
3567
|
+
|
|
3568
|
+
case CommandKey.AddSheet:
|
|
3569
|
+
|
|
3570
|
+
// COEDITING: this won't break, but it shouldn't change the
|
|
3571
|
+
// active sheet if this is a remote command. is there a way
|
|
3572
|
+
// to know? we can guess implicitly from the queue parameter,
|
|
3573
|
+
// but it would be better to be explicit.
|
|
3574
|
+
|
|
3575
|
+
{
|
|
3576
|
+
const id = this.AddSheetInternal(command.name, command.insert_index); // default name
|
|
3577
|
+
if (typeof id === 'number' && command.show) {
|
|
3578
|
+
this.ActivateSheetInternal({
|
|
3579
|
+
key: CommandKey.ActivateSheet,
|
|
3580
|
+
id,
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
flags.structure_event = true;
|
|
3584
|
+
flags.sheets = true;
|
|
3585
|
+
flags.structure = true;
|
|
3586
|
+
|
|
3587
|
+
}
|
|
3588
|
+
break;
|
|
3589
|
+
|
|
3590
|
+
case CommandKey.ActivateSheet:
|
|
3591
|
+
this.ActivateSheetInternal(command);
|
|
3592
|
+
break;
|
|
3593
|
+
|
|
3594
|
+
default:
|
|
3595
|
+
console.warn(`unhandled command: ${CommandKey[command.key]} (${command.key})`);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// consolidate events and merge areas
|
|
3600
|
+
|
|
3601
|
+
if (flags.data_area) {
|
|
3602
|
+
if (!flags.data_area.start.sheet_id) {
|
|
3603
|
+
flags.data_area.SetSheetID(this.active_sheet.id);
|
|
3604
|
+
}
|
|
3605
|
+
events.push({ type: 'data', area: flags.data_area });
|
|
3606
|
+
}
|
|
3607
|
+
else if (flags.data_event) {
|
|
3608
|
+
events.push({ type: 'data' });
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
if (flags.style_area) {
|
|
3612
|
+
if (!flags.style_area.start.sheet_id) {
|
|
3613
|
+
flags.style_area.SetSheetID(this.active_sheet.id);
|
|
3614
|
+
}
|
|
3615
|
+
events.push({ type: 'style', area: flags.style_area });
|
|
3616
|
+
}
|
|
3617
|
+
else if (flags.style_event) {
|
|
3618
|
+
events.push({ type: 'style' });
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
if (flags.structure_event) {
|
|
3622
|
+
events.push({
|
|
3623
|
+
type: 'structure',
|
|
3624
|
+
rebuild_required: flags.structure_rebuild_required,
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
if (this.batch) {
|
|
3629
|
+
this.batch_events.push(...events);
|
|
3630
|
+
}
|
|
3631
|
+
else {
|
|
3632
|
+
this.grid_events.Publish(events);
|
|
3633
|
+
//if (flags.render_area) {
|
|
3634
|
+
// this.DelayedRender(false, flags.render_area);
|
|
3635
|
+
//}
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
return flags;
|
|
3639
|
+
|
|
3640
|
+
|
|
3641
|
+
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
}
|