@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.
Files changed (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +285 -269
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +320 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,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
+ }