@trebco/treb 23.6.2 → 25.0.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +293 -299
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +320 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,910 @@
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
+ import { Area, Cell, Theme, Rectangle, Localization, ICellAddress } from 'treb-base-types';
23
+ import { Yield, EventSource } from 'treb-utils';
24
+ import type { Parser, UnitRange, UnitAddress, ParseResult, ExpressionUnit } from 'treb-parser';
25
+
26
+ import type { GridSelection } from '../types/grid_selection';
27
+ import type { Autocomplete, AutocompleteResult } from './autocomplete';
28
+ import { AutocompleteExecResult, AutocompleteMatcher, DescriptorType } from './autocomplete_matcher';
29
+
30
+ import type { DataModel, ViewModel } from '../types/data_model';
31
+ import { UA } from '../util/ua';
32
+
33
+ /** event on commit, either enter or tab */
34
+ export interface FormulaEditorCommitEvent {
35
+ type: 'commit';
36
+ selection?: GridSelection;
37
+ value?: string;
38
+
39
+ /**
40
+ * true if commiting an array. note that if the cell _is_ an array,
41
+ * and you commit as !array, that should be an error.
42
+ */
43
+ array?: boolean;
44
+
45
+ /**
46
+ * for the formula editor, the event won't bubble so we can't handle
47
+ * it with the normal event handler -- so use the passed event to
48
+ */
49
+ event?: KeyboardEvent;
50
+ }
51
+
52
+ /** event on discard -- escape */
53
+ export interface FormulaEditorDiscardEvent {
54
+ type: 'discard';
55
+ }
56
+
57
+ /** event on end select state, reset selection */
58
+ export interface FormulaEditorEndSelectionEvent {
59
+ type: 'end-selection';
60
+ }
61
+
62
+ /** event on text update: need to update sheet dependencies */
63
+ export interface FormulaEditorUpdateEvent {
64
+ type: 'update';
65
+ text?: string;
66
+ cell?: Cell;
67
+ dependencies?: Area[];
68
+ }
69
+
70
+ // export interface FormulaEditorAutocompleteEvent {
71
+ // type: 'autocomplete';
72
+ // text?: string;
73
+ // cursor?: number;
74
+ // }
75
+
76
+ /*
77
+ export interface RetainFocusEvent {
78
+ type: 'retain-focus';
79
+ focus: boolean;
80
+ }
81
+ */
82
+
83
+ export interface StartEditingEvent {
84
+ type: 'start-editing';
85
+ editor?: string;
86
+ }
87
+
88
+ export interface StopEditingEvent {
89
+ type: 'stop-editing';
90
+ editor?: string;
91
+ }
92
+
93
+ /** discriminated union */
94
+ export type FormulaEditorEvent
95
+ = // RetainFocusEvent
96
+ | StopEditingEvent
97
+ | StartEditingEvent
98
+ | FormulaEditorUpdateEvent
99
+ | FormulaEditorCommitEvent
100
+ | FormulaEditorDiscardEvent
101
+ | FormulaEditorEndSelectionEvent
102
+ ;
103
+
104
+ /**
105
+ * this class implements some common functionality for the formula
106
+ * bar editor and the in-cell editor, in an effort to reduce duplication
107
+ * and normalize behavior.
108
+ *
109
+ * finally figured out how to use a polymorphic discriminated union.
110
+ * not sure what would happen if the implementing type violated the
111
+ * type rule... not an issue atm, but worth a look. maybe enforce somehow,
112
+ * via interface?
113
+ */
114
+ export abstract class FormulaEditorBase<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorEvent> {
115
+
116
+ protected static readonly FormulaChars = ('$^&*(-+={[<>/~%' + Localization.argument_separator).split(''); // FIXME: i18n
117
+
118
+ /**
119
+ * the current edit cell. in the event we're editing a merged or
120
+ * array cell, this might be different than the actual target address.
121
+ */
122
+ public active_cell?: Cell;
123
+
124
+ /**
125
+ * address of cell we're editing
126
+ * why did this get removed? it would be helpful
127
+ */
128
+ public target_address?: ICellAddress;
129
+
130
+ /** area we're editing, for potential arrays */
131
+ // public area: Area;
132
+
133
+ /** matcher. passed in by owner. should move to constructor arguments */
134
+ public autocomplete_matcher?: AutocompleteMatcher;
135
+
136
+ /**
137
+ * non-document node for text munging
138
+ *
139
+ * FIXME: this could be static? is there a case where we are editing
140
+ * two things at once? (...)
141
+ */
142
+ protected measurement_node: HTMLDivElement;
143
+
144
+ // tslint:disable-next-line:variable-name
145
+ protected selecting_ = false;
146
+
147
+ /** node for inserting cell address, when selecting */
148
+ protected editor_insert_node?: HTMLSpanElement;
149
+
150
+ /** the edit node, which is a contenteditable div */
151
+ protected editor_node?: HTMLDivElement;
152
+
153
+ /** the containing node, used for layout */
154
+ protected container_node?: HTMLDivElement;
155
+
156
+ /** ac instance */
157
+ // protected autocomplete!: Autocomplete; // = new Autocomplete();
158
+
159
+ /** this never fucking ends */
160
+ //protected trident = ((typeof navigator !== 'undefined') &&
161
+ // navigator.userAgent && /trident/i.test(navigator.userAgent));
162
+
163
+ // ...
164
+ protected last_parse_string = '';
165
+ protected last_parse_result?: ParseResult;
166
+
167
+ // protected dependency_list?: DependencyList;
168
+ protected reference_list?: Array<UnitRange|UnitAddress>;
169
+ protected dependency_list: Area[] = [];
170
+ protected reference_index_map: number[] = [];
171
+
172
+ protected last_reconstructed_text = '';
173
+
174
+ private enable_reconstruct = true; // false;
175
+
176
+ /**
177
+ * accessor for editor selecting cells. if this is set, a click on the
178
+ * sheet (or arrow navigation) should be interpreted as selecting a
179
+ * cell as an argument
180
+ */
181
+ public get selecting() { return this.selecting_; }
182
+
183
+ /**
184
+ * selection being edited. note that this is private rather than protected
185
+ * in an effort to prevent subclasses from accidentally using shallow copies
186
+ */
187
+ // tslint:disable-next-line:variable-name
188
+ private selection_: GridSelection = {
189
+ target: { row: 0, column: 0 },
190
+ area: new Area({ row: 0, column: 0 }),
191
+ };
192
+
193
+ /** accessor for selection */
194
+ public get selection(){ return this.selection_; }
195
+
196
+ /** set selection, deep copy */
197
+ public set selection(rhs: GridSelection){
198
+ if (!rhs){
199
+ const zero = {row: 0, column: 0};
200
+ this.selection_ = {target: zero, area: new Area(zero)};
201
+ }
202
+ else {
203
+ const target = rhs.target || rhs.area.start;
204
+ this.selection_ = {
205
+ target: {row: target.row, column: target.column},
206
+ area: new Area(rhs.area.start, rhs.area.end),
207
+ };
208
+ }
209
+ }
210
+
211
+ constructor(
212
+ protected readonly parser: Parser,
213
+ protected readonly theme: Theme,
214
+ protected readonly model: DataModel,
215
+ protected readonly view: ViewModel,
216
+ protected readonly autocomplete: Autocomplete){
217
+
218
+ super();
219
+
220
+ // not added to dom
221
+ this.measurement_node = document.createElement('div');
222
+ }
223
+
224
+ public UpdateTheme(scale: number) {
225
+ // ...
226
+ }
227
+
228
+ public InsertReference(reference: string, id: any){
229
+
230
+ if (!this.editor_node) return;
231
+
232
+ // FIXME: x/browser?
233
+
234
+ if (!this.editor_insert_node){
235
+ const selection = window.getSelection();
236
+ if (selection) {
237
+ const range = selection.getRangeAt(0);
238
+ this.editor_insert_node = document.createElement('span');
239
+ range.insertNode(this.editor_insert_node);
240
+ selection.collapseToEnd();
241
+ }
242
+ }
243
+ if (this.editor_insert_node) {
244
+
245
+ this.editor_insert_node.innerText = reference;
246
+
247
+ // edge handles this differently than chrome/ffx. in edge, the
248
+ // cursor does not move to the end of the selection, which is
249
+ // what we want. so we need to fix that for edge:
250
+
251
+ // FIXME: limit to edge (causing problems in chrome? ...)
252
+
253
+ if (reference.length) {
254
+ const selection = window.getSelection();
255
+ if (selection) {
256
+ const range = document.createRange();
257
+ range.selectNodeContents(this.editor_insert_node);
258
+ selection.removeAllRanges();
259
+ selection.addRange(range);
260
+ selection.collapseToEnd();
261
+ }
262
+ }
263
+
264
+ }
265
+
266
+ const dependencies = this.ListDependencies();
267
+
268
+ this.Publish({type: 'update', text: this.editor_node.textContent || undefined, dependencies});
269
+
270
+ }
271
+
272
+ /** called when there's AC data to display (or tooltip) */
273
+ public Autocomplete(data: AutocompleteExecResult, target_node?: Node): void {
274
+
275
+ if (!this.container_node) {
276
+ return;
277
+ }
278
+
279
+ let client_rect: DOMRect;
280
+ if (target_node?.nodeType === Node.ELEMENT_NODE) {
281
+ client_rect = (target_node as Element).getBoundingClientRect();
282
+ }
283
+ else {
284
+ client_rect = this.container_node.getBoundingClientRect();
285
+ }
286
+
287
+ const rect = new Rectangle(
288
+ Math.round(client_rect.left),
289
+ Math.round(client_rect.top),
290
+ client_rect.width, client_rect.height);
291
+
292
+ this.autocomplete.Show(this.AcceptAutocomplete.bind(this), data, rect);
293
+
294
+ }
295
+
296
+ /** flush insert reference, so the next insert uses a new element */
297
+ protected FlushReference(): void {
298
+ this.editor_insert_node = undefined;
299
+ }
300
+
301
+ /**
302
+ * get text substring to caret position, irrespective of node structure
303
+ */
304
+ protected SubstringToCaret(node: HTMLDivElement): string {
305
+
306
+ // FIXME: x/browser
307
+
308
+ // not sure about x/browser with this... only for electron atm
309
+ // seems to be ok in chrome (natch), ffx, [ie/edge? saf? test]
310
+
311
+ const selection = window.getSelection();
312
+ if (!selection) {
313
+ throw new Error('error getting selection');
314
+ }
315
+
316
+ if (selection.rangeCount === 0) {
317
+ console.warn('range count is 0');
318
+ return '';
319
+ }
320
+
321
+ const range = selection.getRangeAt(0);
322
+ const preCaretRange = range.cloneRange();
323
+
324
+ preCaretRange.selectNodeContents(node);
325
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
326
+
327
+ this.measurement_node.textContent = '';
328
+ this.measurement_node.appendChild(preCaretRange.cloneContents());
329
+
330
+ return this.measurement_node.textContent;
331
+ }
332
+
333
+ /**
334
+ * @param flush flush existing selection even if state does not
335
+ * change -- this is used in the case where there's a single keypress
336
+ * between two selections, otherwise we keep the initial block
337
+ */
338
+ protected UpdateSelectState(flush = false): void {
339
+
340
+ let selecting = false;
341
+ let formula = false;
342
+
343
+ if (!this.editor_node) return;
344
+
345
+ const text = this.editor_node.textContent || '';
346
+
347
+ // if (text.trim().startsWith('=')){
348
+ if (text.trim()[0] === '='){
349
+ formula = true;
350
+ const sub = this.SubstringToCaret(this.editor_node).trim();
351
+
352
+ if (sub.length){
353
+ const char = sub[sub.length - 1];
354
+ if (FormulaEditorBase.FormulaChars.some((a) => char === a)) selecting = true;
355
+
356
+ // this.Publish({
357
+ // type: 'autocomplete',
358
+ // text, cursor: sub.length,
359
+ // });
360
+ // bind instance so we know it exists. this is unecessary, but it's
361
+ // more correct and ts will stop complaining
362
+
363
+ const matcher = this.autocomplete_matcher;
364
+
365
+ if (matcher) {
366
+ Yield().then(() => {
367
+ const exec_result = matcher.Exec({ text, cursor: sub.length });
368
+ const node =
369
+ this.NodeAtIndex(exec_result.completions ?
370
+ (exec_result.position || 0) :
371
+ (exec_result.function_position || 0));
372
+ this.Autocomplete(exec_result, node);
373
+ });
374
+ }
375
+
376
+ }
377
+ }
378
+
379
+ if (selecting !== this.selecting_){
380
+ this.selecting_ = selecting;
381
+ if (!selecting) {
382
+ this.Reconstruct(); // because we skipped the last one (should just switch order?)
383
+ }
384
+ if (flush || !selecting) {
385
+ this.Publish({type: 'end-selection'});
386
+ }
387
+ }
388
+
389
+ // special case
390
+ else if (selecting && flush) this.Publish({type: 'end-selection'});
391
+
392
+ const dependencies = formula ? this.ListDependencies() : undefined;
393
+
394
+ this.Publish({ type: 'update', text, dependencies });
395
+
396
+ }
397
+
398
+ protected NodeAtIndex(index: number): Node|undefined {
399
+ const children = this.editor_node?.childNodes || [];
400
+ for (let i = 0; i < children.length; i++) {
401
+ const len = children[i].textContent?.length || 0;
402
+ if (len > index) {
403
+ return children[i];
404
+ }
405
+ index -= len;
406
+ }
407
+ return undefined;
408
+ }
409
+
410
+ /*
411
+ protected HighlightColor(index: number, overlay = false) {
412
+ if (overlay) {
413
+ if (Array.isArray(this.theme.additional_selection_overlay_color)) {
414
+ index = index % this.theme.additional_selection_overlay_color.length;
415
+ return this.theme.additional_selection_overlay_color[index] || '';
416
+ }
417
+ return this.theme.additional_selection_overlay_color || '';
418
+ }
419
+ else {
420
+ if (Array.isArray(this.theme.additional_selection_text_color)) {
421
+ index = index % this.theme.additional_selection_text_color.length;
422
+ return this.theme.additional_selection_text_color[index] || '';
423
+ }
424
+ return this.theme.additional_selection_text_color || '';
425
+ }
426
+ }
427
+ */
428
+
429
+ /**
430
+ * replace text with node structure for highlighting.
431
+ *
432
+ * lots of cross-browser issues. chrome is generally ok. firefox drops
433
+ * spaces at the end of the text. IE11 breaks, but it's not clear why.
434
+ *
435
+ * UPDATE: this breaks when entering hanzi, probably true of all
436
+ * multibyte unicode characters
437
+ *
438
+ * removing unused parameter
439
+ */
440
+ protected Reconstruct(): void {
441
+
442
+ if (!this.enable_reconstruct) {
443
+ return; // disabled
444
+ }
445
+
446
+ if (!this.editor_node) {
447
+ return;
448
+ }
449
+
450
+ this.ParseDependencies();
451
+
452
+ // ---
453
+
454
+ // this was originally here and wasn't doing what it was supposed to
455
+ // do, because the reference list could be empty but still !false. however
456
+ // we're actually adding nodes for other things (calls) so we should leave
457
+ // it as is for now
458
+
459
+ if (!this.reference_list) {
460
+ return;
461
+ }
462
+
463
+ // my attempted fix
464
+ // if (!this.reference_list || !this.reference_list.length) {
465
+ // return;
466
+ // }
467
+
468
+ // ---
469
+
470
+ // here we would normally set spellcheck to true for strings,
471
+ // but that seems to break IME (at least in chrome). what we
472
+ // should do is have spellcheck default to true and then turn
473
+ // it off for functions. also we should only do this on parse,
474
+ // because that only happens when text changes.
475
+
476
+ const text = this.editor_node.textContent || '';
477
+
478
+ if (text.trim()[0] !== '=') {
479
+ // this.editor_node.setAttribute('spellcheck', 'true');
480
+ return;
481
+ }
482
+
483
+ this.editor_node.spellcheck = false;
484
+
485
+ // we might not have to do this, if the text hasn't changed
486
+ // (or the text has only changed slightly...) this might actually
487
+ // save us from the firefox issue (issue: firefox drops trailing spaces)
488
+
489
+ // just make sure you flush appropriately
490
+
491
+ // we can also skip when selecting (in fact if we don't, it will break
492
+ // the selecting routine by dumping the target span)
493
+
494
+ if (this.selecting) return;
495
+
496
+ // why do we parse dependencies (above) if the text hasn't changed? (...)
497
+ // actually that routine will also short-circuit, although it would presumably
498
+ // be better to not call it
499
+
500
+ if (text.trim() === this.last_reconstructed_text.trim()) {
501
+ return;
502
+ }
503
+
504
+ this.last_reconstructed_text = text;
505
+
506
+ const subtext = this.SubstringToCaret(this.editor_node);
507
+ const caret = subtext.length;
508
+
509
+ // why are we using a document fragment? something to do with x-browser?
510
+ // (...)
511
+ // actually I think it's so we can construct like a regular document, but
512
+ // do it off screen (double-buffered), not sure if it makes that much of
513
+ // a difference. I suppose you could use a container node instead... ?
514
+
515
+ const fragment = document.createDocumentFragment();
516
+
517
+ // this is the node that will contain the caret/cursor
518
+ let selection_target_node: Node|undefined;
519
+
520
+ // this is the caret/cursor offset within that node
521
+ let selection_offset = 0;
522
+
523
+ let last_node: Node|undefined;
524
+ let last_text = '';
525
+
526
+ if (this.last_parse_result) {
527
+
528
+ // somewhat unfortunate but we drop the = from the original text when
529
+ // parsing, so all of the offsets are off by 1.
530
+
531
+ let base = 0;
532
+ let label = '';
533
+ let reference_index = 0;
534
+
535
+ const append_node = (start: number, text: string, type: string, unit?: ExpressionUnit) => {
536
+ const text_node = document.createTextNode(text);
537
+ if (type === 'text') {
538
+ fragment.appendChild(text_node);
539
+ }
540
+ else {
541
+ const span = document.createElement('span');
542
+ span.appendChild(text_node);
543
+ span.dataset.position = start.toString();
544
+ span.dataset.type = type;
545
+
546
+ if (type === 'address' || type === 'range') {
547
+ span.classList.add(`highlight-${(this.reference_index_map[reference_index++] % 5) + 1}`);
548
+ }
549
+ else if (type === 'structured-reference') {
550
+ if (this.target_address && unit?.type === 'structured-reference') {
551
+ const reference = this.model.ResolveStructuredReference(unit, this.target_address);
552
+ if (reference) {
553
+ span.classList.add(`highlight-${(this.reference_index_map[reference_index++] % 5) + 1}`);
554
+ }
555
+ }
556
+ }
557
+ else if (type === 'identifier') {
558
+ if (this.model.named_ranges.Get(text)) {
559
+ span.classList.add(`highlight-${(this.reference_index_map[reference_index++] % 5) + 1}`);
560
+ }
561
+ }
562
+
563
+ fragment.appendChild(span);
564
+ }
565
+
566
+ if (caret >= start && caret < start + text.length) {
567
+ // console.info('caret is in this one:', text);
568
+ selection_target_node = text_node;
569
+ selection_offset = caret - start;
570
+ }
571
+
572
+ return text_node;
573
+ };
574
+
575
+ if (this.last_parse_result.expression) {
576
+
577
+ // console.info({expr: this.last_parse_result.expression});
578
+
579
+ this.parser.Walk(this.last_parse_result.expression, (unit: ExpressionUnit) => {
580
+
581
+ switch (unit.type) {
582
+ case 'address':
583
+ case 'range':
584
+ case 'call':
585
+ case 'identifier':
586
+ case 'structured-reference':
587
+
588
+ // any leading text we have skipped, create a text node
589
+ if (unit.position !== base - 1) {
590
+ append_node(base, text.substring(base, unit.position + 1), 'text');
591
+ }
592
+
593
+ // let's get the raw text, and not the "label" -- that's causing
594
+ // text to toggle as we type, which is generally OK except when
595
+ // it's not, but when it's not it's really annoying.
596
+
597
+ if (unit.type === 'call' || unit.type === 'identifier') { label = unit.name; }
598
+ else {
599
+
600
+ // use the raw text. FIXME: parser could save raw
601
+ // text here, so we don't have to substring.
602
+
603
+ label = this.last_parse_string.substring(unit.position + 1, unit.position + unit.label.length + 1);
604
+
605
+ }
606
+
607
+ // label = (unit.type === 'call' || unit.type === 'identifier') ? unit.name : unit.label;
608
+
609
+ append_node(unit.position + 1, label, unit.type, unit);
610
+
611
+ base = unit.position + label.length + 1;
612
+ break;
613
+ }
614
+
615
+ // range is unusual because we don't recurse (return false)
616
+ return unit.type !== 'range';
617
+
618
+ });
619
+ }
620
+
621
+ // balance, create another text node. hang on to this one.
622
+ last_text = text.substring(base) || '';
623
+ last_node = append_node(base, last_text, 'text');
624
+
625
+ }
626
+
627
+ if (!selection_target_node) {
628
+ if (text.length === caret) {
629
+ const selection_span = document.createElement('span');
630
+ fragment.appendChild(selection_span);
631
+ selection_target_node = selection_span;
632
+ selection_offset = 0;
633
+ }
634
+ else {
635
+ selection_target_node = last_node; // remainder_node;
636
+ selection_offset = Math.max(0, last_text.length - (text.length - caret));
637
+ // console.info("FIXME!", text.length - caret);
638
+ }
639
+ }
640
+
641
+ // fragment is not a node, so once we append this we have more than
642
+ // one child. we might wrap it in something... ?
643
+
644
+ this.editor_node.textContent = '';
645
+ this.editor_node.appendChild(fragment);
646
+
647
+ // console.info("STC", selection_target_node, selection_offset);
648
+
649
+ if (selection_target_node) {
650
+ const range = document.createRange();
651
+ const selection = window.getSelection();
652
+ if (selection) {
653
+ range.setStart(selection_target_node, selection_offset);
654
+ range.setEnd(selection_target_node, selection_offset);
655
+ range.collapse(true);
656
+ selection.removeAllRanges();
657
+ selection.addRange(range);
658
+ }
659
+ }
660
+
661
+ // return fragment;
662
+ }
663
+
664
+ protected ParseDependencies(): void {
665
+
666
+ if (!this.editor_node) {
667
+ return;
668
+ }
669
+
670
+ const text = this.editor_node.textContent || '';
671
+
672
+ // this is pretty rare (parsing the same string twice), we only do this
673
+ // text on changes. still, we want to keep the dep list around, so we
674
+ // might as well check.
675
+
676
+ // far more common are minor (like 1-char) changes; it would be nice if
677
+ // we could do incremental updates. probably a lot of work on the parser
678
+ // side, though.
679
+
680
+ if (text !== this.last_parse_string || !this.reference_list) {
681
+
682
+ const sheet_name_map: {[index: string]: number} = {};
683
+ for (const sheet of this.model.sheets.list) {
684
+ sheet_name_map[sheet.name.toLowerCase()] = sheet.id;
685
+ }
686
+
687
+ this.dependency_list = [];
688
+ this.reference_index_map = [];
689
+
690
+ if (text) {
691
+ const parse_result = this.parser.Parse(text);
692
+ this.last_parse_string = text;
693
+ this.last_parse_result = parse_result;
694
+
695
+ // console.info("SA?", self); (self as any).LPR = this.last_parse_result;
696
+
697
+ this.reference_list = []; // parse_result.full_reference_list;
698
+
699
+ if (parse_result.full_reference_list) {
700
+ for (const unit of parse_result.full_reference_list) {
701
+ if (unit.type === 'address' || unit.type === 'range') {
702
+
703
+ // if there's a sheet name, map to an ID. FIXME: make a map
704
+ const start = (unit.type === 'address') ? unit : unit.start;
705
+
706
+ if (!start.sheet_id) {
707
+ if (start.sheet) {
708
+ start.sheet_id = sheet_name_map[start.sheet.toLowerCase()] || 0;
709
+ }
710
+ else {
711
+ start.sheet_id = this.view.active_sheet.id;
712
+ }
713
+ }
714
+ this.reference_list.push(unit);
715
+
716
+ }
717
+ else if (unit.type === 'structured-reference') {
718
+
719
+ if (this.target_address) {
720
+ const reference = this.model.ResolveStructuredReference(unit, this.target_address);
721
+ if (reference) {
722
+ this.reference_list.push(reference);
723
+ }
724
+ }
725
+ else {
726
+ console.info('target address not set');
727
+ }
728
+
729
+ }
730
+ else {
731
+ const named_range = this.model.named_ranges.Get(unit.name);
732
+ if (named_range) {
733
+ if (named_range.count === 1) {
734
+ this.reference_list.push({
735
+ type: 'address',
736
+ ...named_range.start,
737
+ label: unit.name,
738
+ position: unit.position,
739
+ id: unit.id,
740
+ });
741
+ }
742
+ else {
743
+ this.reference_list.push({
744
+ type: 'range',
745
+ start: {
746
+ type: 'address',
747
+ position: unit.position,
748
+ id: unit.id,
749
+ label: unit.name,
750
+ ...named_range.start,
751
+ },
752
+ end: {
753
+ type: 'address',
754
+ position: unit.position,
755
+ label: unit.name,
756
+ id: unit.id,
757
+ ...named_range.end,
758
+ },
759
+ label: unit.name,
760
+ position: unit.position,
761
+ id: unit.id,
762
+ });
763
+ }
764
+ }
765
+ }
766
+ }
767
+ }
768
+
769
+ if (this.reference_list) {
770
+
771
+ this.reference_list.sort((a, b) => a.position - b.position);
772
+
773
+ for (const reference of this.reference_list) {
774
+ let area: Area;
775
+
776
+ if (reference.type === 'address') {
777
+ area = new Area({
778
+ row: reference.row, column: reference.column, sheet_id: reference.sheet_id}); // note dropping absolute
779
+ }
780
+ else {
781
+ area = new Area(
782
+ {row: reference.start.row, column: reference.start.column,
783
+ sheet_id: reference.start.sheet_id}, // note dropping absolute
784
+ {row: reference.end.row, column: reference.end.column});
785
+ }
786
+
787
+ const label = area.spreadsheet_label;
788
+ if (!this.dependency_list.some((test, index) => {
789
+ if (test.spreadsheet_label === label && test.start.sheet_id === area.start.sheet_id) {
790
+ this.reference_index_map.push(index);
791
+ return true;
792
+ }
793
+ return false;
794
+ })) {
795
+ this.reference_index_map.push(this.dependency_list.length);
796
+ this.dependency_list.push(area);
797
+ }
798
+ }
799
+ }
800
+
801
+ }
802
+ else {
803
+ this.reference_list = undefined;
804
+ }
805
+ }
806
+
807
+ }
808
+
809
+ /**
810
+ * moving dependency parser into this class (from grid), so we can do
811
+ * some highlighting in the editor (at least in the formula bar).
812
+ *
813
+ * this method returns a consolidated list of dependencies, addresses
814
+ * and ranges, as Area[]. we may have duplicates where one is absolute
815
+ * and the other is not; for the purposes of this method, those are the
816
+ * same.
817
+ */
818
+ protected ListDependencies(): Area[] {
819
+
820
+ this.ParseDependencies();
821
+ return this.dependency_list || [];
822
+
823
+ /*
824
+ if (this.reference_list) {
825
+
826
+ for (const reference of this.reference_list) {
827
+ let area: Area;
828
+ if (reference.type === 'address') {
829
+ area = new Area({row: reference.row, column: reference.column}); // note dropping absolute
830
+ }
831
+ else {
832
+ area = new Area(
833
+ {row: reference.start.row, column: reference.start.column}, // note dropping absolute
834
+ {row: reference.end.row, column: reference.end.column});
835
+ }
836
+ const label = area.spreadsheet_label;
837
+ if (!results.some((test) => test.spreadsheet_label === label)) {
838
+ results.push(area);
839
+ }
840
+ }
841
+
842
+ }
843
+ return results;
844
+ */
845
+
846
+ }
847
+
848
+ protected AcceptAutocomplete(ac_result: AutocompleteResult): void {
849
+
850
+ if (!this.editor_node) return;
851
+ let selection = window.getSelection();
852
+
853
+ let type = DescriptorType.Function;
854
+ if (ac_result.data && ac_result.data.completions) {
855
+ for (const completion of ac_result.data.completions) {
856
+ if (completion.name.toLowerCase() === ac_result.value?.toLowerCase()) {
857
+ type = completion.type || DescriptorType.Function;
858
+ break;
859
+ }
860
+ }
861
+ }
862
+
863
+ if (!selection) throw new Error('error getting selection');
864
+
865
+ let range = selection.getRangeAt(0);
866
+ const preCaretRange = range.cloneRange();
867
+ const tmp = document.createElement('div');
868
+
869
+ preCaretRange.selectNodeContents(this.editor_node);
870
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
871
+ tmp.appendChild(preCaretRange.cloneContents());
872
+
873
+ const str = (tmp.textContent || '').substr(0, ac_result.data ? ac_result.data.position : 0) + ac_result.value;
874
+ //const insert = (type === DescriptorType.Token) ? str + ' ' : str + '(';
875
+ const insert = (type === DescriptorType.Token) ? str : str + '(';
876
+
877
+ // this is destroying nodes, we should be setting html here
878
+
879
+ this.editor_node.textContent = insert;
880
+ this.autocomplete.Hide();
881
+
882
+ // we have to reconstruct because we destroyed nodes, although
883
+ // we do need to call this for new nodes (on a defined name)
884
+
885
+ // firefox has problems... essentially if we do reconstruct, then
886
+ // try to place the cursor at the end, it ends up in a garbage position.
887
+ // (debugging...)
888
+
889
+ if (!UA.is_firefox) {
890
+ this.Reconstruct();
891
+ }
892
+
893
+ selection = window.getSelection();
894
+ range = document.createRange();
895
+ if (this.editor_node?.lastChild) {
896
+ range.setStartAfter(this.editor_node.lastChild);
897
+ }
898
+ range.collapse(true);
899
+ selection?.removeAllRanges();
900
+ selection?.addRange(range);
901
+
902
+ this.selecting_ = true;
903
+
904
+ if (ac_result.click){
905
+ this.UpdateSelectState();
906
+ }
907
+
908
+ }
909
+
910
+ }