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