@trebco/treb 27.5.3 → 27.7.6

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.
@@ -1,512 +1,437 @@
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
- * this is a new version of the ICE that doubles as a key handler
24
- * for the grid; the aim is to support IME in ICE, which did not work
25
- * in our old scheme.
26
- *
27
- * update: trying to clean up the node structure, remove one node,
28
- * and get layout working properly.
29
- */
30
-
31
- import type { Theme, CellValue, Rectangle, Cell } from 'treb-base-types';
32
- import { Style, ThemeColor2, type CellStyle } from 'treb-base-types';
33
- import { Yield } from 'treb-utils';
34
- import type { Parser } from 'treb-parser';
35
- import type { GridSelection } from '../types/grid_selection';
36
- import { FormulaEditorBase } from './formula_editor_base';
37
- import type { Autocomplete } from './autocomplete';
38
- import type { DataModel, ViewModel } from '../types/data_model';
39
- import { UA } from '../util/ua';
40
-
41
- /**
42
- * new return type for key event handler, has some additional state
43
- */
44
- export enum OverlayEditorResult {
45
- not_handled = 0,
46
- handled = 1,
47
- commit = 2,
48
- discard = 3,
49
- }
50
-
51
- /** legacy */
52
- // const support_cloned_events = (typeof KeyboardEvent === 'function');
53
-
54
- /** legacy */
55
- // const use_create_text_range = (typeof ((document?.body as any)?.createTextRange) === 'function');
56
-
57
- export class OverlayEditor extends FormulaEditorBase {
58
-
59
- // we could add these back, always construct them, and then
60
- // just assign, that would get us around all the conditionals
61
-
62
- public edit_node: HTMLElement & ElementContentEditable;
63
- public edit_container: HTMLElement;
64
-
65
- public edit_inset: HTMLElement;
66
-
67
- public scale = 1; // this should go into theme, since it tends to follow it
68
-
69
- /** FIXME: shouldn't this be a lint error? did we drop that rule? */
70
- private _editing = false;
71
-
72
- public get editing(): boolean {
73
- return this._editing;
74
- }
75
-
76
- public set editing(state: boolean) {
77
- if (this._editing !== state) {
78
- this._editing = state;
79
- if (state) {
80
- this.edit_container.style.opacity = '1';
81
- this.edit_container.style.pointerEvents = 'initial';
82
- }
83
- else {
84
- this.edit_container.style.opacity = '0';
85
- this.edit_container.style.pointerEvents = 'none';
86
- }
87
- }
88
- }
89
-
90
- constructor(private container: HTMLElement, parser: Parser, theme: Theme, model: DataModel, view: ViewModel, autocomplete: Autocomplete) {
91
-
92
- super(parser, theme, model, view, autocomplete);
93
-
94
- this.edit_container = container.querySelector('.treb-overlay-container') as HTMLElement;
95
- this.edit_node = this.edit_container.querySelector('.treb-overlay-editor') as HTMLElement & ElementContentEditable;
96
-
97
- if (UA.is_firefox) {
98
- this.edit_node.classList.add('firefox');
99
- }
100
-
101
- // attempting to cancel "auto" keyboard on ios
102
- this.edit_node.inputMode = 'none';
103
-
104
- //
105
- // so clearly I am doing this when rendering, not sure how it
106
- // happens, but we're offsetting by this much. checked on mac
107
- // (dpr = 2) and windows (dpr = 1, dpr = 1.5). 1 is the default.
108
- //
109
- // NOTE that there's a `resolution` media query, not implemented in safari
110
- //
111
- // https://bugs.webkit.org/show_bug.cgi?id=78087
112
- //
113
- // and another nonstandard -webkit-max-device-pixel-ratio, which seems
114
- // to be in all modern browsers (possibly with -moz prefix in ffx). OTOH
115
- // this is not complicated and only called on construct, so probably fine,
116
- // if somewhat obscure.
117
- //
118
-
119
- // NOTE: it's not that simple (see linux). it has something to do with
120
- // measuring the font. we probably need to figure out exactly what we are
121
- // doing in the renderer, and do that. which also means it might be cell
122
- // specific if there are font face/size changes. also we can get it to
123
- // offset incorrectly on windows with some fonts (I'm looking at you, comic
124
- // sans)
125
-
126
- // although it probably still has something to do with dpr, maybe that's
127
- // a factor...
128
-
129
- //if (self.devicePixelRatio && self.devicePixelRatio > 1) {
130
- // this.edit_node.style.paddingBottom = `${self.devicePixelRatio}px`;
131
- //}
132
-
133
-
134
-
135
- this.edit_node.addEventListener('input', (event: Event) => {
136
-
137
- if (event instanceof InputEvent && event.isComposing) {
138
- return;
139
- }
140
-
141
- // this is a new thing that popped up in chrome (actually edge).
142
- // not sure what's happening but this seems to clean it up.
143
- // we technically could allow a newline here, but... call that a TODO
144
-
145
- const first_child = this.edit_node.firstChild as HTMLElement;
146
- if (first_child && first_child.tagName === 'BR') {
147
- this.edit_node.removeChild(first_child);
148
- }
149
-
150
- // should we dynamically add this when editing? (...)
151
- if (!this.editing) { return; }
152
-
153
- this.Reconstruct();
154
- this.UpdateSelectState();
155
- });
156
-
157
- this.edit_node.addEventListener('keyup', (event: KeyboardEvent) => {
158
-
159
- if (event.isComposing) {
160
- return;
161
- }
162
-
163
- // should we dynamically add this when editing? (...)
164
- if (!this.editing) { return; }
165
-
166
- const ac = this.autocomplete.HandleKey('keyup', event);
167
- if (ac.handled) {
168
- return;
169
- }
170
-
171
- if (this.selecting_){
172
- switch (event.key){
173
- case 'ArrowUp':
174
- case 'ArrowDown':
175
- case 'ArrowLeft':
176
- case 'ArrowRight':
177
- case 'Shift': // also selection modifiers
178
- case 'Control': // ...
179
- return;
180
- }
181
- }
182
-
183
- // clear node. new ones will be created as necessary.
184
- this.FlushReference();
185
- this.UpdateSelectState(true);
186
-
187
- });
188
-
189
- this.edit_inset = this.edit_container.querySelector('.treb-overlay-inset') as HTMLElement;
190
-
191
- // this.edit_inset = document.createElement('div');
192
- // this.edit_inset.classList.add('treb-overlay-inset');
193
- // this.edit_container.appendChild(this.edit_node);
194
- // this.edit_container.appendChild(this.edit_inset);
195
- // this.edit_inset.appendChild(this.edit_node);
196
- // this.edit_container.appendChild(this.edit_node); // dropping inset
197
- // container.appendChild(this.edit_container);
198
- // this.edit_container.style.opacity = '0';
199
-
200
- this.editor_node = this.edit_node as HTMLDivElement; // wtf is this?
201
- this.container_node = this.edit_container as HTMLDivElement; // wtf is this?
202
-
203
- this.ClearContents();
204
-
205
- }
206
-
207
- /* * this is here only for compatibility with the old ICE; not sure if we need it * /
208
- public HandleMouseEvent(event: MouseEvent): boolean {
209
-
210
- return false;
211
- }
212
- */
213
-
214
- public UpdateCaption(text = ''): void {
215
- this.edit_node.setAttribute('aria-label', text);
216
- }
217
-
218
- public Focus(text = ''): void {
219
-
220
- // we get unexpected scroll behavior if we focus on the overlay editor
221
- // when it is not already focused, and the grid is scrolled. that's because
222
- // by default the editor is at (0, 0), so we need to move it before we
223
- // focus on it (but only in this case).
224
-
225
- if (this.edit_node !== document.activeElement) {
226
-
227
- // this was not correct, but should we add those 2 pixels back?
228
-
229
- // this.edit_container.style.top = `${this.container.scrollTop + 2}px`;
230
- // this.edit_container.style.left = `${this.container.scrollLeft + 2}px`;
231
-
232
- this.edit_container.style.top = `${this.container.scrollTop + this.view.active_sheet.header_offset.y}px`;
233
- this.edit_container.style.left = `${this.container.scrollLeft + this.view.active_sheet.header_offset.x}px`;
234
-
235
- }
236
-
237
- this.edit_node.focus();
238
- this.UpdateCaption(text);
239
-
240
- }
241
-
242
- /* TEMP (should be Hide() ?) */
243
- public CloseEditor(): void {
244
- this.editing = false;
245
-
246
- // this (all) should go into the set visible accessor? (...)
247
-
248
- this.ClearContents();
249
- this.edit_node.spellcheck = true; // default
250
- this.autocomplete.Hide();
251
-
252
- this.active_cell = undefined;
253
-
254
- }
255
-
256
- /**
257
- * remove contents, plus add mozilla junk node
258
- */
259
- public ClearContents(): void {
260
-
261
- // UA doesn't change, so this should be mapped directly
262
- // (meaning function pointer and no test)
263
-
264
- // ...maybe overoptimizing
265
-
266
- if (UA.is_firefox) {
267
-
268
- // in firefox if the node is empty when you focus on it the
269
- // cursor shifts up like 1/2 em or something, no idea why
270
- // (TODO: check bugs)
271
-
272
- this.edit_node.innerHTML = '<span></span>';
273
-
274
- }
275
- else {
276
- this.edit_node.textContent = '';
277
- }
278
-
279
- }
280
-
281
- public Edit(gridselection: GridSelection, rect: Rectangle, cell: Cell, value?: CellValue, event?: Event): void {
282
-
283
- this.Publish({
284
- type: 'start-editing',
285
- editor: 'ice',
286
- });
287
-
288
- this.active_cell = cell;
289
- this.target_address = {...gridselection.target};
290
-
291
- const style: CellStyle = cell.style || {};
292
-
293
- this.edit_node.style.font = Style.Font(style, this.scale);
294
- this.edit_node.style.color = ThemeColor2(this.theme, style.text, 1);
295
-
296
- this.edit_inset.style.backgroundColor = ThemeColor2(this.theme, style.fill, 0);
297
- // this.edit_container.style.backgroundColor = ThemeColor2(this.theme, style.fill, 0);
298
-
299
- // NOTE: now that we dropped support for IE11, we can probably
300
- // remove more than one class at the same time.
301
-
302
- // (but apparently firefox didn't support multiple classes either,
303
- // until v[x]? I think that may have been years ago...)
304
-
305
- switch (style.horizontal_align) {
306
- case 'right': // Style.HorizontalAlign.Right:
307
- this.edit_container.classList.remove('align-center', 'align-left');
308
- this.edit_container.classList.add('align-right');
309
- break;
310
- case 'center': // Style.HorizontalAlign.Center:
311
- this.edit_container.classList.remove('align-right', 'align-left');
312
- this.edit_container.classList.add('align-center');
313
- break;
314
- default:
315
- this.edit_container.classList.remove('align-right', 'align-center');
316
- this.edit_container.classList.add('align-left');
317
- break;
318
- }
319
-
320
- this.edit_node.style.paddingBottom = `${ Math.max(0, (self.devicePixelRatio||1) - 1)}px`;
321
-
322
- // console.info('pb', this.edit_node.style.paddingBottom);
323
-
324
- // TODO: alignment, underline (strike?)
325
- // bold/italic already work because those are font properties.
326
-
327
- const value_string = value?.toString() || '';
328
-
329
- // do this only if there's existing text, in which case we're not
330
- // typing... or it could be a %, which is OK because the key is a number
331
-
332
- if (value_string && value_string[0] === '=') {
333
- this.edit_node.spellcheck = false;
334
- }
335
-
336
- this.autocomplete.ResetBlock();
337
- this.FlushReference();
338
- this.selection = gridselection;
339
-
340
- if (typeof value !== 'undefined') {
341
-
342
- const percent = value_string[0] !== '=' && value_string[value_string.length - 1] === '%';
343
- const value_length = value_string.length;
344
- this.edit_node.textContent = value_string;
345
-
346
- // legacy
347
-
348
- /*
349
- if (use_create_text_range) {
350
-
351
- Yield().then(() => {
352
- const r = (document.body as any).createTextRange();
353
- r.moveToElementText(this.editor_node);
354
-
355
- // the weird logic here is as follows: move to the end, unless
356
- // it's a percent; in which case move to just before the % sign;
357
- // unless, in the special case of overtyping a %, don't do anything.
358
- // it works (the last case) because this is called via a yield. IE
359
- // will somehow end up doing the right thing in this case.
360
-
361
- if (percent) {
362
- if (value_length > 1) {
363
- r.moveStart('character', value_length);
364
- r.move('character', -1);
365
- r.select();
366
- }
367
- }
368
- else {
369
- r.moveStart('character', value_length);
370
- r.select();
371
- }
372
-
373
- });
374
- }
375
- else
376
- */
377
- {
378
-
379
- const range = document.createRange();
380
- const selection = window.getSelection();
381
-
382
- if (!selection) throw new Error('invalid selection object');
383
-
384
- if (this.edit_node.lastChild){
385
- if (percent) {
386
- range.setStart(this.edit_node.lastChild, value_length - 1);
387
- range.setEnd(this.edit_node.lastChild, value_length - 1);
388
- }
389
- else {
390
- range.setStartAfter(this.edit_node.lastChild);
391
- }
392
- }
393
-
394
- range.collapse(true);
395
- selection.removeAllRanges();
396
- selection.addRange(range);
397
-
398
- }
399
-
400
- if (!event) {
401
- const dependencies = this.ListDependencies();
402
- this.Publish({type: 'update', text: value.toString(), dependencies});
403
- }
404
-
405
- }
406
- else {
407
-
408
- // FIXME: mozilla junk? check old ICE
409
-
410
- }
411
-
412
- rect.ApplyStyle(this.edit_container);
413
- this.editing = true;
414
-
415
- // I'm not sure we need to do this...
416
-
417
- Yield().then(() => {
418
-
419
- // we probably do need to do this, but maybe not the next one
420
- this.last_reconstructed_text = '';
421
- this.Reconstruct();
422
-
423
- });
424
-
425
- }
426
-
427
- /**
428
- * we probably need more state in the return value to move stuff from
429
- * the async handler to directly in the sync handler -- we no longer need
430
- * to redispatch events, because we're in the same event stream
431
- *
432
- * @param event
433
- * @returns
434
- */
435
- public HandleKeyDown(event: KeyboardEvent): OverlayEditorResult {
436
-
437
- if (!this.editing) {
438
- return OverlayEditorResult.not_handled;
439
- }
440
-
441
- // pass through to autocomplete
442
-
443
- const ac = this.autocomplete.HandleKey('keydown', event);
444
-
445
- if (ac.accept){
446
- this.AcceptAutocomplete(ac);
447
- }
448
- if (ac.handled) {
449
- return OverlayEditorResult.handled;
450
- }
451
-
452
- switch (event.key) {
453
-
454
- case 'Enter':
455
- case 'Tab':
456
- {
457
- /*
458
- // we're going to trap this event, and then re-send it, as we do with
459
- // the formula bar editor. this is so that the grid can send the data
460
- // event before the selection event, to better support undo.
461
-
462
- const value = this.edit_node.textContent || undefined;
463
- const array = (event.key === 'Enter' && event.ctrlKey && event.shiftKey);
464
- this.Publish({type: 'commit', value, selection: this.selection, array, event});
465
- */
466
-
467
- this.selecting_ = false;
468
-
469
- // do this so we don't tab-switch-focus
470
- // event.stopPropagation();
471
- // event.preventDefault();
472
-
473
- return OverlayEditorResult.commit;
474
- }
475
-
476
- case 'Escape':
477
- case 'Esc':
478
-
479
- // this.Publish({type: 'discard'});
480
- this.selecting_ = false;
481
- return OverlayEditorResult.discard;
482
-
483
- case 'ArrowUp':
484
- case 'ArrowDown':
485
- case 'ArrowLeft':
486
- case 'ArrowRight':
487
- case 'Up':
488
- case 'Down':
489
- case 'Left':
490
- case 'Right':
491
- return this.selecting_ ? OverlayEditorResult.not_handled : OverlayEditorResult.handled;
492
-
493
- }
494
-
495
- // for all other keys, we consume the key if we're in edit mode; otherwise
496
- // return false and let the calling routine (in grid) handle the key
497
-
498
- // return this.editing;
499
-
500
- return OverlayEditorResult.handled; // always true because we test at the top
501
-
502
- }
503
-
504
- // --- from old ICE ----------------------------------------------------------
505
-
506
- public UpdateTheme(scale: number): void {
507
- this.scale = scale;
508
- }
509
-
510
- }
511
-
512
-
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 { Editor, type NodeDescriptor } from './editor';
23
+ import { Area, Cell, type CellStyle, type CellValue, Rectangle, Style, type Theme, ThemeColor2 } from 'treb-base-types';
24
+ import { DataModel, type ViewModel } from '../types/data_model';
25
+ import { Autocomplete } from './autocomplete';
26
+ import { UA } from '../util/ua';
27
+ import type { GridSelection } from '../types/grid_selection';
28
+
29
+ export type OverlayEditorResult = 'handled' | 'commit' | 'discard';
30
+
31
+ /**
32
+ * but when to send it?
33
+ */
34
+ export interface ResetSelectionEvent {
35
+ type: 'reset-selection';
36
+ }
37
+
38
+ export class OverlayEditor extends Editor<ResetSelectionEvent> {
39
+
40
+ // --- do we actually need this? ---------------------------------------------
41
+
42
+ /**
43
+ * selection being edited. note that this is private rather than protected
44
+ * in an effort to prevent subclasses from accidentally using shallow copies
45
+ */
46
+ private internal_selection: GridSelection = {
47
+ target: { row: 0, column: 0 },
48
+ area: new Area({ row: 0, column: 0 }),
49
+ };
50
+
51
+ /** accessor for selection */
52
+ public get selection(){ return this.internal_selection; }
53
+
54
+ /** set selection, deep copy */
55
+ public set selection(rhs: GridSelection){
56
+ if (!rhs){
57
+ const zero = {row: 0, column: 0};
58
+ this.internal_selection = {target: zero, area: new Area(zero)};
59
+ }
60
+ else {
61
+ const target = rhs.target || rhs.area.start;
62
+ this.internal_selection = {
63
+ target: {row: target.row, column: target.column},
64
+ area: new Area(rhs.area.start, rhs.area.end),
65
+ };
66
+ }
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * this is a flag used to indicate when we need to reset the selection.
73
+ * the issue has to do with selecting cells via arrow keys; if you do
74
+ * that twice, the second time the selection starts on the cell you
75
+ * selected the first time. so we want to fix that.
76
+ *
77
+ * I guess that used to work with an 'end-selection' event (although it
78
+ * didn't change the sheet) but that doesn't happen anymore because
79
+ * selecting state is determined dynamically now.
80
+ */
81
+ public reset_selection = false;
82
+
83
+ /** we could use the descriptor reference */
84
+ public edit_node: HTMLElement & ElementContentEditable;
85
+
86
+ /** narrowing from superclass */
87
+ public container_node: HTMLElement;
88
+
89
+ /** special node for ICE */
90
+ public edit_inset: HTMLElement;
91
+
92
+ public scale = 1; // this should go into theme, since it tends to follow it
93
+
94
+ /** shadow property */
95
+ private internal_editing = false;
96
+
97
+ /** accessor */
98
+ public get editing(): boolean {
99
+ return this.internal_editing;
100
+ }
101
+
102
+ /**
103
+ * this is only set one time for each state, so it would be more
104
+ * efficient to inline it unless that's going to change
105
+ */
106
+ protected set editing(state: boolean) {
107
+ if (this.internal_editing !== state) {
108
+ this.internal_editing = state;
109
+ if (state) {
110
+ this.container_node.style.opacity = '1';
111
+ this.container_node.style.pointerEvents = 'initial';
112
+ }
113
+ else {
114
+ this.container_node.style.opacity = '0';
115
+ this.container_node.style.pointerEvents = 'none';
116
+ }
117
+ }
118
+ }
119
+
120
+ constructor(
121
+ private container: HTMLElement,
122
+ private theme: Theme,
123
+ model: DataModel,
124
+ view: ViewModel,
125
+ autocomplete: Autocomplete) {
126
+
127
+ super(model, view, autocomplete);
128
+
129
+ this.container_node = container.querySelector('.treb-overlay-container') as HTMLElement;
130
+ this.edit_node = this.container_node.querySelector('.treb-overlay-editor') as HTMLElement & ElementContentEditable;
131
+
132
+ if (UA.is_firefox) {
133
+ this.edit_node.classList.add('firefox');
134
+ }
135
+
136
+ // attempting to cancel "auto" keyboard on ios
137
+ this.edit_node.inputMode = 'none';
138
+
139
+ ////
140
+
141
+ const descriptor: NodeDescriptor = { node: this.edit_node };
142
+ this.nodes = [ descriptor ];
143
+ this.active_editor = descriptor;
144
+
145
+ this.RegisterListener(descriptor, 'input', (event: Event) => {
146
+
147
+ if (event instanceof InputEvent && event.isComposing) {
148
+ return;
149
+ }
150
+
151
+ if (!event.isTrusted) {
152
+ this.reset_selection = true; // this is a hack, and unreliable (but works for now)
153
+ return;
154
+ }
155
+
156
+ if (this.reset_selection) {
157
+ this.Publish({
158
+ type: 'reset-selection',
159
+ });
160
+ }
161
+
162
+ // this is a new thing that popped up in chrome (actually edge).
163
+ // not sure what's happening but this seems to clean it up.
164
+ // we technically could allow a newline here, but... call that a TODO
165
+
166
+ const first_child = this.edit_node.firstChild as HTMLElement;
167
+ if (first_child && first_child.tagName === 'BR') {
168
+ this.edit_node.removeChild(first_child);
169
+ }
170
+
171
+ if (!this.editing) {
172
+ return;
173
+ }
174
+
175
+ this.UpdateText(descriptor);
176
+ this.UpdateColors();
177
+
178
+ });
179
+
180
+ this.RegisterListener(descriptor, 'keyup', (event: KeyboardEvent) => {
181
+
182
+ if (event.isComposing || !this.editing) {
183
+ return;
184
+ }
185
+
186
+ // we're not doing anything with the result? (...)
187
+
188
+ if (this.autocomplete && this.autocomplete.HandleKey('keyup', event).handled) {
189
+ return;
190
+ }
191
+
192
+ });
193
+
194
+ this.edit_inset = this.container_node.querySelector('.treb-overlay-inset') as HTMLElement;
195
+ // this.container_node = this.container_node ; // wtf is this?
196
+
197
+ this.ClearContents();
198
+
199
+ }
200
+
201
+ public UpdateCaption(text = ''): void {
202
+ this.edit_node.setAttribute('aria-label', text);
203
+ }
204
+
205
+ public Focus(text = ''): void {
206
+
207
+ // we get unexpected scroll behavior if we focus on the overlay editor
208
+ // when it is not already focused, and the grid is scrolled. that's because
209
+ // by default the editor is at (0, 0), so we need to move it before we
210
+ // focus on it (but only in this case).
211
+
212
+ if (this.edit_node !== document.activeElement) {
213
+
214
+ // this was not correct, but should we add those 2 pixels back?
215
+
216
+ // this.edit_container.style.top = `${this.container.scrollTop + 2}px`;
217
+ // this.edit_container.style.left = `${this.container.scrollLeft + 2}px`;
218
+
219
+ this.container_node.style.top = `${this.container.scrollTop + this.view.active_sheet.header_offset.y}px`;
220
+ this.container_node.style.left = `${this.container.scrollLeft + this.view.active_sheet.header_offset.x}px`;
221
+
222
+ }
223
+
224
+ this.edit_node.focus();
225
+ this.UpdateCaption(text);
226
+
227
+ }
228
+
229
+ /* TEMP (should be Hide() ?) */
230
+ public CloseEditor(): void {
231
+ this.editing = false;
232
+ this.reset_selection = false;
233
+
234
+ // this (all) should go into the set visible accessor? (...)
235
+
236
+ this.ClearContents();
237
+ this.edit_node.spellcheck = true; // default
238
+ this.autocomplete?.Hide();
239
+
240
+ this.active_cell = undefined;
241
+
242
+ }
243
+
244
+ /**
245
+ * remove contents, plus add mozilla junk node
246
+ */
247
+ public ClearContents(): void {
248
+
249
+ // UA doesn't change, so this should be mapped directly
250
+ // (meaning function pointer and no test)
251
+
252
+ // ...maybe overoptimizing
253
+
254
+ if (UA.is_firefox) {
255
+
256
+ // in firefox if the node is empty when you focus on it the
257
+ // cursor shifts up like 1/2 em or something, no idea why
258
+ // (TODO: check bugs)
259
+
260
+ this.edit_node.innerHTML = '<span></span>';
261
+
262
+ }
263
+ else {
264
+ this.edit_node.textContent = '';
265
+ }
266
+
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /**
272
+ * start editing. I'm not sure why we're passing the selection around,
273
+ * but I don't want to take it out until I can answer that question.
274
+ *
275
+ * something to do with keyboard selection? (which needs to be fixed)?
276
+ */
277
+ public Edit(gridselection: GridSelection, rect: Rectangle, cell: Cell, value?: CellValue, event?: Event): void {
278
+
279
+ this.Publish({
280
+ type: 'start-editing',
281
+ editor: 'ice',
282
+ });
283
+
284
+ this.active_cell = cell;
285
+ this.target_address = {...gridselection.target};
286
+ this.reset_selection = false;
287
+
288
+ const style: CellStyle = cell.style || {};
289
+
290
+ this.edit_node.style.font = Style.Font(style, this.scale);
291
+ this.edit_node.style.color = ThemeColor2(this.theme, style.text, 1);
292
+ this.edit_inset.style.backgroundColor = ThemeColor2(this.theme, style.fill, 0);
293
+
294
+ // NOTE: now that we dropped support for IE11, we can probably
295
+ // remove more than one class at the same time.
296
+
297
+ // (but apparently firefox didn't support multiple classes either,
298
+ // until v[x]? I think that may have been years ago...)
299
+
300
+ switch (style.horizontal_align) {
301
+ case 'right': // Style.HorizontalAlign.Right:
302
+ this.container_node.classList.remove('align-center', 'align-left');
303
+ this.container_node.classList.add('align-right');
304
+ break;
305
+ case 'center': // Style.HorizontalAlign.Center:
306
+ this.container_node.classList.remove('align-right', 'align-left');
307
+ this.container_node.classList.add('align-center');
308
+ break;
309
+ default:
310
+ this.container_node.classList.remove('align-right', 'align-center');
311
+ this.container_node.classList.add('align-left');
312
+ break;
313
+ }
314
+
315
+ this.edit_node.style.paddingBottom = `${ Math.max(0, (self.devicePixelRatio||1) - 1)}px`;
316
+
317
+ // console.info('pb', this.edit_node.style.paddingBottom);
318
+
319
+ // TODO: alignment, underline (strike?)
320
+ // bold/italic already work because those are font properties.
321
+
322
+ const value_string = value?.toString() || '';
323
+
324
+ // do this only if there's existing text, in which case we're not
325
+ // typing... or it could be a %, which is OK because the key is a number
326
+
327
+ if (value_string && value_string[0] === '=') {
328
+ this.edit_node.spellcheck = false;
329
+ }
330
+
331
+ this.autocomplete?.ResetBlock();
332
+ this.selection = gridselection;
333
+
334
+ if (typeof value !== 'undefined') {
335
+
336
+ const percent = value_string[0] !== '=' && value_string[value_string.length - 1] === '%';
337
+ const value_length = value_string.length;
338
+ this.edit_node.textContent = value_string;
339
+
340
+ this.SetCaret({ node: this.edit_node, offset: value_length - (percent ? 1 : 0) })
341
+
342
+ // event moved below
343
+
344
+ }
345
+
346
+ rect.ApplyStyle(this.container_node);
347
+ this.editing = true;
348
+
349
+ Promise.resolve().then(() => {
350
+
351
+ if (this.active_editor) {
352
+ this.active_editor.formatted_text = undefined; // necessary? (...)
353
+ this.UpdateText(this.active_editor);
354
+ this.UpdateColors();
355
+ }
356
+
357
+ // not sure about these two tests, they're from the old version
358
+
359
+ if (!event && value !== undefined) {
360
+ this.Publish({type: 'update', text: value.toString(), dependencies: this.composite_dependencies});
361
+ }
362
+
363
+ });
364
+
365
+ }
366
+
367
+ /**
368
+ * check if we want to handle this key. we have some special cases (tab,
369
+ * enter, escape) where we do take some action but we also let the
370
+ * spreadsheet handle the key. for those we have some additional return
371
+ * values.
372
+ *
373
+ * NOTE this is *not* added as an event handler -- it's called by the grid
374
+ *
375
+ * @param event
376
+ * @returns
377
+ */
378
+ public HandleKeyDown(event: KeyboardEvent): OverlayEditorResult|undefined {
379
+
380
+ // skip keys if we're not editing
381
+
382
+ if (!this.editing) {
383
+ return undefined; // not handled
384
+ }
385
+
386
+ // pass through to autocomplete
387
+
388
+ if (this.autocomplete) {
389
+ const ac = this.autocomplete.HandleKey('keydown', event);
390
+
391
+ if (ac.accept){
392
+ this.AcceptAutocomplete(ac);
393
+ }
394
+
395
+ if (ac.handled) {
396
+ return 'handled';
397
+ }
398
+ }
399
+
400
+ switch (event.key) {
401
+
402
+ case 'Enter':
403
+ case 'Tab':
404
+ return 'commit';
405
+
406
+ case 'Escape':
407
+ case 'Esc':
408
+ return 'discard';
409
+
410
+ case 'ArrowUp':
411
+ case 'ArrowDown':
412
+ case 'ArrowLeft':
413
+ case 'ArrowRight':
414
+ case 'Up':
415
+ case 'Down':
416
+ case 'Left':
417
+ case 'Right':
418
+ return this.selecting ? undefined : 'handled';
419
+
420
+ }
421
+
422
+ return 'handled'; // we will consume
423
+
424
+ }
425
+
426
+ public UpdateScale(scale: number): void {
427
+
428
+ // we're not changing in place, so this won't affect any open editors...
429
+ // I think there's a case where you change scale without focusing (using
430
+ // the mouse wheel) which might result in incorrect rendering... TODO/FIXME
431
+
432
+ this.scale = scale;
433
+
434
+ }
435
+
436
+
437
+ }