@trebco/treb 27.5.3 → 27.9.0

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 (48) hide show
  1. package/dist/treb-spreadsheet.mjs +14 -14
  2. package/dist/treb.d.ts +37 -23
  3. package/notes/conditional-fomratring.md +29 -0
  4. package/package.json +3 -3
  5. package/treb-base-types/src/area.ts +181 -0
  6. package/treb-base-types/src/evaluate-options.ts +21 -0
  7. package/treb-base-types/src/gradient.ts +97 -0
  8. package/treb-base-types/src/import.ts +2 -1
  9. package/treb-base-types/src/index.ts +2 -0
  10. package/treb-calculator/src/calculator.ts +205 -132
  11. package/treb-calculator/src/dag/calculation_leaf_vertex.ts +97 -0
  12. package/treb-calculator/src/dag/graph.ts +10 -22
  13. package/treb-calculator/src/dag/{leaf_vertex.ts → state_leaf_vertex.ts} +3 -3
  14. package/treb-calculator/src/descriptors.ts +10 -3
  15. package/treb-calculator/src/expression-calculator.ts +1 -1
  16. package/treb-calculator/src/function-library.ts +25 -22
  17. package/treb-calculator/src/functions/base-functions.ts +166 -5
  18. package/treb-calculator/src/index.ts +6 -6
  19. package/treb-calculator/src/notifier-types.ts +1 -1
  20. package/treb-calculator/src/utilities.ts +2 -2
  21. package/treb-charts/src/util.ts +2 -2
  22. package/treb-embed/src/embedded-spreadsheet.ts +382 -71
  23. package/treb-embed/style/formula-bar.scss +2 -0
  24. package/treb-embed/style/theme-defaults.scss +46 -15
  25. package/treb-export/src/export-worker/export-worker.ts +0 -13
  26. package/treb-export/src/export2.ts +187 -2
  27. package/treb-export/src/import2.ts +169 -4
  28. package/treb-export/src/workbook-style2.ts +56 -8
  29. package/treb-export/src/workbook2.ts +10 -1
  30. package/treb-grid/src/editors/editor.ts +1276 -0
  31. package/treb-grid/src/editors/external_editor.ts +113 -0
  32. package/treb-grid/src/editors/formula_bar.ts +450 -474
  33. package/treb-grid/src/editors/overlay_editor.ts +437 -512
  34. package/treb-grid/src/index.ts +2 -1
  35. package/treb-grid/src/layout/base_layout.ts +24 -16
  36. package/treb-grid/src/render/tile_renderer.ts +2 -1
  37. package/treb-grid/src/types/conditional_format.ts +168 -0
  38. package/treb-grid/src/types/data_model.ts +130 -3
  39. package/treb-grid/src/types/external_editor_config.ts +47 -0
  40. package/treb-grid/src/types/grid.ts +96 -45
  41. package/treb-grid/src/types/grid_base.ts +187 -35
  42. package/treb-grid/src/types/scale-control.ts +1 -1
  43. package/treb-grid/src/types/sheet.ts +330 -26
  44. package/treb-grid/src/types/sheet_types.ts +4 -0
  45. package/treb-grid/src/util/dom_utilities.ts +58 -25
  46. package/treb-grid/src/editors/formula_editor_base.ts +0 -912
  47. package/treb-grid/src/types/external_editor.ts +0 -27
  48. /package/{README-shadow-DOM.md → notes/shadow-DOM.md} +0 -0
@@ -1,474 +1,450 @@
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 { Yield } from 'treb-utils';
23
-
24
- import { DOMUtilities } from '../util/dom_utilities';
25
- import type { Theme } from 'treb-base-types';
26
- import type { FormulaEditorEvent } from './formula_editor_base';
27
- import { FormulaEditorBase } from './formula_editor_base';
28
- import type { GridOptions } from '../types/grid_options';
29
- import type { Autocomplete } from './autocomplete';
30
- import type { DataModel, ViewModel } from '../types/data_model';
31
- import type { Parser } from 'treb-parser';
32
-
33
- export interface FormulaBarResizeEvent {
34
- type: 'formula-bar-resize';
35
- }
36
-
37
- export interface FormulaButtonEvent {
38
- type: 'formula-button';
39
- formula?: string;
40
- cursor_position?: number;
41
- }
42
-
43
- export interface AddressLabelEvent {
44
- type: 'address-label-event';
45
- text?: string;
46
- }
47
-
48
- export type FormulaBar2Event
49
- = FormulaEditorEvent
50
- | FormulaButtonEvent
51
- | FormulaBarResizeEvent
52
- | AddressLabelEvent
53
- ;
54
-
55
- export class FormulaBar extends FormulaEditorBase<FormulaBar2Event> {
56
-
57
- /** is the _editor_ currently focused */
58
- // tslint:disable-next-line:variable-name
59
- public focused_ = false;
60
-
61
- /** accessor for focused field */
62
- public get focused(): boolean { return this.focused_; }
63
-
64
- /** address label (may also show other things... ?) */
65
- private address_label_container: HTMLDivElement;
66
-
67
- /** address label (may also show other things... ?) */
68
- private address_label: HTMLDivElement;
69
-
70
- /** the function button (optional?) */
71
- private button!: HTMLButtonElement;
72
-
73
- /** */
74
- private expand_button!: HTMLButtonElement;
75
-
76
- /** corner for resizing formula editor */
77
- private drag_corner!: HTMLDivElement;
78
-
79
- /** for math */
80
- private lines = 1;
81
-
82
- private last_formula = '';
83
-
84
- private label_update_timer = 0;
85
-
86
- /** get formula text */
87
- public get formula(): string {
88
- return this.editor_node ? this.editor_node.textContent || '' : '';
89
- }
90
-
91
- /** set formula text */
92
- public set formula(text: string) {
93
- if (this.editor_node) {
94
- this.editor_node.textContent = text;
95
- this.last_reconstructed_text = '';
96
- }
97
- this.last_formula = text;
98
- }
99
-
100
- /** get address label text */
101
- public get label(): string {
102
- return this.address_label?.textContent || '';
103
- }
104
-
105
- /**
106
- * set address label text. if the label is too long for the box,
107
- * add a title attribute for a tooltip.
108
- */
109
- public set label(text: string) {
110
- if (!text.trim().length) {
111
- this.address_label.innerHTML = '&nbsp;';
112
- this.address_label.removeAttribute('title');
113
- }
114
- else {
115
- this.address_label.textContent = text;
116
-
117
- if (!this.label_update_timer) {
118
- this.label_update_timer = requestAnimationFrame(() => {
119
- this.label_update_timer = 0;
120
-
121
- // should this be in a Yield callback? need to check IE11...
122
- // yes
123
-
124
- if (this.address_label.scrollWidth > this.address_label.offsetWidth) {
125
- this.address_label.setAttribute('title', text);
126
- }
127
- else {
128
- this.address_label.removeAttribute('title');
129
- }
130
-
131
- });
132
- }
133
-
134
- }
135
- }
136
-
137
- /** toggle editable property: supports locked cells */
138
- public set editable(editable: boolean) {
139
- if (!this.editor_node || !this.container_node) return;
140
-
141
- if (editable) {
142
- this.editor_node.setAttribute('contenteditable', 'true'); // is that required?
143
- this.container_node.removeAttribute('locked');
144
- }
145
- else {
146
- this.editor_node.removeAttribute('contenteditable');
147
- this.container_node.setAttribute('locked', '');
148
- }
149
-
150
- }
151
-
152
- constructor(
153
- private container: HTMLElement,
154
- parser: Parser,
155
- theme: Theme,
156
- model: DataModel,
157
- view: ViewModel,
158
- private options: GridOptions,
159
- autocomplete: Autocomplete,
160
- ) {
161
-
162
- super(parser, theme, model, view, autocomplete);
163
-
164
- const inner_node = container.querySelector('.treb-formula-bar') as HTMLElement;
165
- inner_node.removeAttribute('hidden');
166
-
167
- this.address_label_container = inner_node.querySelector('.treb-address-label') as HTMLDivElement;
168
- this.address_label = this.address_label_container.firstElementChild as HTMLDivElement;
169
-
170
- this.InitAddressLabel();
171
-
172
- if (this.options.insert_function_button) {
173
- this.button = DOMUtilities.Create<HTMLButtonElement>('button', 'formula-button', inner_node);
174
- this.button.addEventListener('click', () => {
175
- const formula: string = this.editor_node ? this.editor_node.textContent || '' : '';
176
- this.Publish({ type: 'formula-button', formula });
177
- });
178
- }
179
-
180
- this.container_node = container.querySelector('.treb-editor-container') as HTMLDivElement;
181
- this.editor_node = this.container_node.firstElementChild as HTMLDivElement;
182
-
183
- //
184
- // change the default back. this was changed when we were trying to figure
185
- // out what was happening with IME, but it had nothing to do with spellcheck.
186
- //
187
- this.editor_node.spellcheck = false; // change the default back
188
-
189
- this.editor_node.addEventListener('focusin', () => {
190
-
191
- // can't happen
192
- if (!this.editor_node) { return; }
193
-
194
- // console.info('focus in');
195
-
196
- let text = this.editor_node.textContent || '';
197
-
198
- if (text[0] === '{' && text[text.length - 1] === '}') {
199
- text = text.substr(1, text.length - 2);
200
- this.editor_node.textContent = text;
201
- this.last_reconstructed_text = '';
202
- }
203
-
204
- this.editor_node.spellcheck = (text[0] !== '='); // true except for functions
205
-
206
- this.autocomplete.ResetBlock();
207
-
208
- /*
209
- const fragment = this.Reconstruct();
210
- if (fragment && this.editor_node) {
211
- this.editor_node.textContent = '';
212
- this.editor_node.appendChild(fragment);
213
- }
214
- */
215
- Yield().then(() => {
216
- // this.Reconstruct(true);
217
- this.Reconstruct();
218
- });
219
-
220
- const dependencies = this.ListDependencies();
221
-
222
- this.Publish([
223
- { type: 'start-editing', editor: 'formula-bar' },
224
- { type: 'update', text, cell: this.active_cell, dependencies },
225
- // { type: 'retain-focus', focus: true },
226
- ]);
227
-
228
- this.focused_ = true;
229
-
230
- });
231
-
232
- this.editor_node.addEventListener('focusout', () => {
233
-
234
- if (this.selecting) {
235
- console.info('focusout, but selecting...');
236
- }
237
-
238
- // console.info('focus out');
239
-
240
- this.autocomplete.Hide();
241
- this.Publish([
242
- { type: 'stop-editing' },
243
- // { type: 'retain-focus', focus: false },
244
- ]);
245
- this.focused_ = false;
246
-
247
- if (this.editor_node) {
248
- this.editor_node.spellcheck = false; // for firefox
249
- }
250
-
251
- });
252
-
253
- this.editor_node.addEventListener('keydown', (event) => this.FormulaKeyDown(event));
254
- this.editor_node.addEventListener('keyup', (event) => this.FormulaKeyUp(event));
255
-
256
- this.editor_node.addEventListener('input', (event: Event) => {
257
-
258
- if (event instanceof InputEvent && event.isComposing) {
259
- return;
260
- }
261
-
262
- this.Reconstruct();
263
- this.UpdateSelectState();
264
-
265
- });
266
-
267
- if (this.options.expand_formula_button) {
268
- this.expand_button = DOMUtilities.Create<HTMLButtonElement>('button', 'expand-button', inner_node);
269
- this.expand_button.addEventListener('click', (event: MouseEvent) => {
270
- event.stopPropagation();
271
- event.preventDefault();
272
- if (this.editor_node) {
273
- this.editor_node.scrollTop = 0;
274
- }
275
- // inner_node.classList.toggle('expanded');
276
- if (inner_node.hasAttribute('expanded')) {
277
- inner_node.removeAttribute('expanded');
278
- }
279
- else {
280
- inner_node.setAttribute('expanded', '');
281
- }
282
- });
283
- }
284
-
285
- }
286
-
287
- public IsElement(element: HTMLElement): boolean {
288
- return element === this.editor_node;
289
- }
290
-
291
- public InitAddressLabel() {
292
-
293
- this.address_label.contentEditable = 'true';
294
- this.address_label.spellcheck = false;
295
-
296
- // on focus, select all
297
- // Q: do we do this in other places? we should consolidate
298
- // A: I don't think we do just this, usually there's additional logic for % and such
299
-
300
- this.address_label.addEventListener('focusin', (event) => {
301
-
302
- // FIXME: close any open editors? (...)
303
-
304
- // we're now doing this async for all browsers... it's only really
305
- // necessary for IE11 and safari, but doesn't hurt
306
-
307
- requestAnimationFrame(() => {
308
- if ((document.body as any).createTextRange) {
309
- const range = (document.body as any).createTextRange();
310
- range.moveToElementText(this.address_label);
311
- range.select();
312
- }
313
- else {
314
- const selection = window.getSelection();
315
- const range = document.createRange();
316
- range.selectNodeContents(this.address_label);
317
- selection?.removeAllRanges();
318
- selection?.addRange(range);
319
- }
320
- });
321
-
322
- });
323
-
324
- this.address_label.addEventListener('keydown', (event) => {
325
- switch (event.key) {
326
-
327
- case 'Enter':
328
- event.stopPropagation();
329
- event.preventDefault();
330
- this.Publish({
331
- type: 'address-label-event',
332
- text: this.address_label.textContent || undefined,
333
- });
334
- break;
335
-
336
- case 'Esc':
337
- case 'Escape':
338
- event.stopPropagation();
339
- event.preventDefault();
340
- this.Publish({ type: 'address-label-event' });
341
- break;
342
- }
343
- });
344
-
345
- }
346
-
347
- /**
348
- * focuses the formula editor. this is intended to be called after a
349
- * range selection, so we can continue editing.
350
- */
351
- public FocusEditor(): void {
352
- if (this.editor_node) {
353
- this.editor_node.focus();
354
- }
355
- }
356
-
357
- /*
358
- public UpdateTheme(): void {
359
-
360
- let font_size = this.theme.formula_bar_font_size || null;
361
-
362
- if (typeof font_size === 'number') {
363
- font_size = `${font_size}pt`;
364
- }
365
-
366
- // all these are applied to the container; font is then inherited.
367
-
368
- this.address_label_container.style.fontFamily = this.theme.formula_bar_font_face || '';
369
- this.address_label_container.style.fontSize = font_size || '';
370
- this.address_label_container.style.fontWeight = '400'; // FIXME
371
-
372
- this.address_label_container.style.backgroundColor = this.theme.formula_bar_background_color || '';
373
- this.address_label_container.style.color = this.theme.formula_bar_color || '';
374
-
375
- if (this.container_node) {
376
- this.container_node.style.fontFamily = this.theme.formula_bar_font_face || '';
377
- this.container_node.style.fontSize = font_size || '';
378
- this.container_node.style.fontWeight = '400'; // FIXME
379
- this.container_node.style.backgroundColor = this.theme.formula_bar_background_color || '';
380
- this.container_node.style.color = this.theme.formula_bar_color || '';
381
- }
382
-
383
- if (this.autocomplete) {
384
- this.autocomplete.UpdateTheme();
385
- }
386
-
387
- }
388
- */
389
-
390
- private GetTextContent(node: Node) {
391
-
392
- const children = node.childNodes;
393
- const buffer: string[] = [];
394
- for (let i = 0; i < children.length; i++) {
395
- const child = children[i];
396
- switch (child.nodeType) {
397
- case Node.ELEMENT_NODE:
398
- buffer.push(...this.GetTextContent(child));
399
- if (child instanceof Element && child.tagName === 'DIV') {
400
- buffer.push('\n');
401
- }
402
- break;
403
-
404
- case Node.TEXT_NODE:
405
- if (child.nodeValue) { buffer.push(child.nodeValue); }
406
- break;
407
- }
408
- }
409
- return buffer;
410
-
411
- }
412
-
413
- private FormulaKeyDown(event: KeyboardEvent){
414
-
415
- const ac_result = this.autocomplete.HandleKey('keydown', event);
416
- if (ac_result.accept) this.AcceptAutocomplete(ac_result);
417
- if (ac_result.handled) return;
418
-
419
- switch (event.key){
420
- case 'Enter':
421
- case 'Tab':
422
- {
423
- this.selecting_ = false;
424
- const array = (event.key === 'Enter' && event.ctrlKey && event.shiftKey);
425
-
426
- const text = (this.editor_node ?
427
- this.GetTextContent(this.editor_node).join('') : '').trim();
428
-
429
- this.Publish({
430
- type: 'commit',
431
- selection: this.selection,
432
- value: text,
433
- event,
434
- array,
435
- });
436
- this.FlushReference();
437
- }
438
- break;
439
-
440
- case 'Escape':
441
- case 'Esc':
442
- this.selecting_ = false;
443
- this.Publish({ type: 'discard' });
444
- this.FlushReference();
445
- break;
446
-
447
- default:
448
- return;
449
- }
450
-
451
- event.stopPropagation();
452
- event.preventDefault();
453
-
454
- }
455
-
456
- private FormulaKeyUp(event: KeyboardEvent){
457
-
458
- const ac_result = this.autocomplete.HandleKey('keyup', event);
459
- if (ac_result.handled) return;
460
- this.FlushReference();
461
-
462
- // because there are no input events, we have to try this one -- note
463
- // we still won't capture pastes, FIXME (add handlers?)
464
-
465
- //if (this.trident) {
466
- // this.UpdateSelectState();
467
- // this.Reconstruct();
468
- //}
469
-
470
- }
471
-
472
-
473
- }
474
-
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 type { Area, Cell, Theme } from 'treb-base-types';
23
+ import { Editor, type NodeDescriptor, type FormulaEditorEvent } from './editor';
24
+ import { Parser } from 'treb-parser';
25
+ import type { DataModel, ViewModel } from '../types/data_model';
26
+ import type { GridOptions } from '../types/grid_options';
27
+ import { Autocomplete } from './autocomplete';
28
+ import { DOMUtilities } from '../util/dom_utilities';
29
+
30
+ // --- from formula_bar ---
31
+
32
+ export interface FormulaBarResizeEvent {
33
+ type: 'formula-bar-resize';
34
+ }
35
+
36
+ export interface FormulaButtonEvent {
37
+ type: 'formula-button';
38
+ formula?: string;
39
+ cursor_position?: number;
40
+ }
41
+
42
+ export interface AddressLabelEvent {
43
+ type: 'address-label-event';
44
+ text?: string;
45
+ }
46
+
47
+ export type FormulaBar2Event
48
+ = FormulaButtonEvent
49
+ | FormulaBarResizeEvent
50
+ | AddressLabelEvent
51
+ ;
52
+
53
+ // ---
54
+
55
+ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
56
+
57
+
58
+ /** is the _editor_ currently focused */
59
+ // tslint:disable-next-line:variable-name
60
+ public focused_ = false;
61
+
62
+ /** accessor for focused field */
63
+ public get focused(): boolean { return this.focused_; }
64
+
65
+ /** address label (may also show other things... ?) */
66
+ private address_label_container: HTMLDivElement;
67
+
68
+ /** address label (may also show other things... ?) */
69
+ private address_label: HTMLDivElement;
70
+
71
+ /** the function button (optional?) */
72
+ private button!: HTMLButtonElement;
73
+
74
+ /** */
75
+ private expand_button!: HTMLButtonElement;
76
+
77
+ /* * corner for resizing formula editor * /
78
+ private drag_corner!: HTMLDivElement;
79
+
80
+ / * * for math * /
81
+ private lines = 1;
82
+
83
+ private last_formula = '';
84
+ */
85
+
86
+ private label_update_timer = 0;
87
+
88
+ /** get formula text */
89
+ public get formula(): string {
90
+ return this.active_editor ? this.active_editor.node.textContent || '' : '';
91
+ }
92
+
93
+ /** set formula text */
94
+ public set formula(text: string) {
95
+ if (this.active_editor) {
96
+ this.active_editor.node.textContent = text;
97
+ this.active_editor.formatted_text = undefined;
98
+ }
99
+ // this.last_formula = text;
100
+ }
101
+
102
+ /** get address label text */
103
+ public get label(): string {
104
+ return this.address_label?.textContent || '';
105
+ }
106
+
107
+ /**
108
+ * set address label text. if the label is too long for the box,
109
+ * add a title attribute for a tooltip.
110
+ */
111
+ public set label(text: string) {
112
+ if (!text.trim().length) {
113
+ this.address_label.innerHTML = '&nbsp;';
114
+ this.address_label.removeAttribute('title');
115
+ }
116
+ else {
117
+ this.address_label.textContent = text;
118
+
119
+ if (!this.label_update_timer) {
120
+ this.label_update_timer = requestAnimationFrame(() => {
121
+ this.label_update_timer = 0;
122
+
123
+ // should this be in a Yield callback? need to check IE11...
124
+ // yes
125
+
126
+ if (this.address_label.scrollWidth > this.address_label.offsetWidth) {
127
+ this.address_label.setAttribute('title', text);
128
+ }
129
+ else {
130
+ this.address_label.removeAttribute('title');
131
+ }
132
+
133
+ });
134
+ }
135
+
136
+ }
137
+ }
138
+
139
+ /** toggle editable property: supports locked cells */
140
+ public set editable(editable: boolean) {
141
+ if (!this.active_editor || !this.container_node) return;
142
+
143
+ if (editable) {
144
+ this.active_editor.node.setAttribute('contenteditable', 'true'); // is that required?
145
+ this.container_node.removeAttribute('locked');
146
+ }
147
+ else {
148
+ this.active_editor.node.removeAttribute('contenteditable');
149
+ this.container_node.setAttribute('locked', '');
150
+ }
151
+
152
+ }
153
+
154
+ constructor(
155
+ container: HTMLElement,
156
+ // parser: Parser,
157
+ // theme: Theme,
158
+ model: DataModel,
159
+ view: ViewModel,
160
+ private options: GridOptions,
161
+ autocomplete: Autocomplete,
162
+ ) {
163
+
164
+ super(model, view, autocomplete);
165
+
166
+ const inner_node = container.querySelector('.treb-formula-bar') as HTMLElement;
167
+ inner_node.removeAttribute('hidden');
168
+
169
+ this.address_label_container = inner_node.querySelector('.treb-address-label') as HTMLDivElement;
170
+ this.address_label = this.address_label_container.firstElementChild as HTMLDivElement;
171
+
172
+ this.InitAddressLabel();
173
+
174
+ if (this.options.insert_function_button) {
175
+ this.button = DOMUtilities.Create('button', 'formula-button', inner_node);
176
+ this.button.addEventListener('click', () => {
177
+ const formula: string = this.active_editor ? this.active_editor.node.textContent || '' : '';
178
+ this.Publish({ type: 'formula-button', formula });
179
+ });
180
+ }
181
+
182
+ this.container_node = container.querySelector('.treb-editor-container') as HTMLDivElement;
183
+ const target = this.container_node.firstElementChild as HTMLDivElement;
184
+ const descriptor: NodeDescriptor = {
185
+ node: target,
186
+ };
187
+
188
+ this.active_editor = descriptor;
189
+ this.nodes = [ descriptor ];
190
+
191
+ // ------------------
192
+
193
+ if (target) {
194
+ this.RegisterListener(descriptor, 'input', (event: Event) => {
195
+
196
+ // we send an extra event when we insert a reference.
197
+ // so filter that out. this might cause problems for other
198
+ // callers -- could we use a different filter?
199
+
200
+ if (event.isTrusted) {
201
+ this.UpdateText(descriptor);
202
+ this.UpdateColors(); // will send a local event
203
+ }
204
+
205
+ });
206
+ }
207
+
208
+ // ------------------
209
+
210
+ //
211
+ // change the default back. this was changed when we were trying to figure
212
+ // out what was happening with IME, but it had nothing to do with spellcheck.
213
+ //
214
+ this.active_editor.node.spellcheck = false; // change the default back
215
+
216
+ this.RegisterListener(descriptor, 'focusin', () => {
217
+
218
+ // this.editor_node.addEventListener('focusin', () => {
219
+
220
+ // can't happen
221
+ if (!this.active_editor) { return; }
222
+
223
+ // console.info('focus in');
224
+
225
+ let text = this.active_editor.node.textContent || '';
226
+
227
+ if (text[0] === '{' && text[text.length - 1] === '}') {
228
+ text = text.substring(1, text.length - 1);
229
+ this.active_editor.node.textContent = text;
230
+ this.active_editor.formatted_text = undefined; // why do we clear this here?
231
+ }
232
+
233
+ // not here // this.editor_node.spellcheck = (text[0] !== '='); // true except for functions
234
+ this.autocomplete?.ResetBlock();
235
+
236
+ this.UpdateText(this.active_editor);
237
+ this.UpdateColors();
238
+
239
+ this.Publish([
240
+ { type: 'start-editing', editor: 'formula-bar' },
241
+ { type: 'update', text, cell: this.active_cell, dependencies: this.composite_dependencies },
242
+ ]);
243
+
244
+ this.focused_ = true;
245
+
246
+ });
247
+
248
+ this.RegisterListener(descriptor, 'focusout', (event: FocusEvent) => {
249
+
250
+ if (this.selecting) {
251
+ console.info('focusout, but selecting...');
252
+ }
253
+
254
+ // console.info('focus out');
255
+
256
+ this.autocomplete?.Hide();
257
+ this.Publish([
258
+ { type: 'stop-editing' },
259
+ ]);
260
+
261
+ this.focused_ = false;
262
+
263
+ if (this.active_editor) {
264
+ this.active_editor.node.spellcheck = false; // for firefox
265
+ }
266
+
267
+ });
268
+
269
+ this.RegisterListener(descriptor, 'keydown', this.FormulaKeyDown.bind(this));
270
+ this.RegisterListener(descriptor, 'keyup', this.FormulaKeyUp.bind(this));
271
+
272
+ if (this.options.expand_formula_button) {
273
+ this.expand_button = DOMUtilities.Create('button', 'expand-button', inner_node);
274
+ this.expand_button.addEventListener('click', (event: MouseEvent) => {
275
+ event.stopPropagation();
276
+ event.preventDefault();
277
+ if (this.active_editor) {
278
+ this.active_editor.node.scrollTop = 0;
279
+ }
280
+ if (inner_node.hasAttribute('expanded')) {
281
+ inner_node.removeAttribute('expanded');
282
+ }
283
+ else {
284
+ inner_node.setAttribute('expanded', '');
285
+ }
286
+ });
287
+ }
288
+
289
+ }
290
+
291
+ public IsElement(element: HTMLElement): boolean {
292
+ return element === this.active_editor?.node;
293
+ }
294
+
295
+ public InitAddressLabel() {
296
+
297
+ this.address_label.contentEditable = 'true';
298
+ this.address_label.spellcheck = false;
299
+
300
+ // on focus, select all
301
+ // Q: do we do this in other places? we should consolidate
302
+ // A: I don't think we do just this, usually there's additional logic for % and such
303
+
304
+ this.address_label.addEventListener('focusin', (event) => {
305
+
306
+ // FIXME: close any open editors? (...)
307
+
308
+ // we're now doing this async for all browsers... it's only really
309
+ // necessary for IE11 and safari, but doesn't hurt
310
+
311
+ requestAnimationFrame(() => {
312
+ if ((document.body as any).createTextRange) {
313
+ const range = (document.body as any).createTextRange();
314
+ range.moveToElementText(this.address_label);
315
+ range.select();
316
+ }
317
+ else {
318
+ const selection = window.getSelection();
319
+ const range = document.createRange();
320
+ range.selectNodeContents(this.address_label);
321
+ selection?.removeAllRanges();
322
+ selection?.addRange(range);
323
+ }
324
+ });
325
+
326
+ });
327
+
328
+ this.address_label.addEventListener('keydown', (event) => {
329
+
330
+ if (event.isComposing) {
331
+ return;
332
+ }
333
+
334
+ switch (event.key) {
335
+
336
+ case 'Enter':
337
+ event.stopPropagation();
338
+ event.preventDefault();
339
+ this.Publish({
340
+ type: 'address-label-event',
341
+ text: this.address_label.textContent || undefined,
342
+ });
343
+ break;
344
+
345
+ case 'Esc':
346
+ case 'Escape':
347
+ event.stopPropagation();
348
+ event.preventDefault();
349
+ this.Publish({ type: 'address-label-event' });
350
+ break;
351
+ }
352
+ });
353
+
354
+ }
355
+
356
+ private GetTextContent(node: Node) {
357
+
358
+ const children = node.childNodes;
359
+ const buffer: string[] = [];
360
+ for (let i = 0; i < children.length; i++) {
361
+ const child = children[i];
362
+ switch (child.nodeType) {
363
+ case Node.ELEMENT_NODE:
364
+ buffer.push(...this.GetTextContent(child));
365
+ if (child instanceof Element && child.tagName === 'DIV') {
366
+ buffer.push('\n');
367
+ }
368
+ break;
369
+
370
+ case Node.TEXT_NODE:
371
+ if (child.nodeValue) { buffer.push(child.nodeValue); }
372
+ break;
373
+ }
374
+ }
375
+ return buffer;
376
+
377
+ }
378
+
379
+
380
+
381
+ private FormulaKeyUp(event: KeyboardEvent){
382
+
383
+ if (event.isComposing) {
384
+ return;
385
+ }
386
+
387
+ if (this.autocomplete) {
388
+ const ac_result = this.autocomplete.HandleKey('keyup', event);
389
+ if (ac_result.handled) {
390
+ return;
391
+ }
392
+ // this.FlushReference();
393
+ }
394
+
395
+ }
396
+
397
+ private FormulaKeyDown(event: KeyboardEvent){
398
+
399
+ if (event.isComposing) {
400
+ return;
401
+ }
402
+
403
+ if (this.autocomplete) {
404
+ const ac_result = this.autocomplete.HandleKey('keydown', event);
405
+ if (ac_result.accept) this.AcceptAutocomplete(ac_result);
406
+ if (ac_result.handled) return;
407
+ }
408
+
409
+ switch (event.key){
410
+ case 'Enter':
411
+ case 'Tab':
412
+ {
413
+ // this.selecting_ = false;
414
+ const array = (event.key === 'Enter' && event.ctrlKey && event.shiftKey);
415
+
416
+ // I think we use this nontstandard routine so that we preserve
417
+ // newlines? not sure. would like to see the motivation for it.
418
+
419
+ const text = (this.active_editor ?
420
+ this.GetTextContent(this.active_editor.node).join('') : '').trim();
421
+
422
+ this.Publish({
423
+ type: 'commit',
424
+ // selection: this.selection,
425
+ value: text,
426
+ event,
427
+ array,
428
+ });
429
+
430
+ // this.FlushReference();
431
+ }
432
+ break;
433
+
434
+ case 'Escape':
435
+ case 'Esc':
436
+ // this.selecting_ = false;
437
+ this.Publish({ type: 'discard' });
438
+ // this.FlushReference();
439
+ break;
440
+
441
+ default:
442
+ return;
443
+ }
444
+
445
+ event.stopPropagation();
446
+ event.preventDefault();
447
+
448
+ }
449
+
450
+ }