@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.
- package/dist/treb-spreadsheet.mjs +14 -14
- package/dist/treb.d.ts +37 -23
- package/notes/conditional-fomratring.md +29 -0
- package/package.json +3 -3
- package/treb-base-types/src/area.ts +181 -0
- package/treb-base-types/src/evaluate-options.ts +21 -0
- package/treb-base-types/src/gradient.ts +97 -0
- package/treb-base-types/src/import.ts +2 -1
- package/treb-base-types/src/index.ts +2 -0
- package/treb-calculator/src/calculator.ts +205 -132
- package/treb-calculator/src/dag/calculation_leaf_vertex.ts +97 -0
- package/treb-calculator/src/dag/graph.ts +10 -22
- package/treb-calculator/src/dag/{leaf_vertex.ts → state_leaf_vertex.ts} +3 -3
- package/treb-calculator/src/descriptors.ts +10 -3
- package/treb-calculator/src/expression-calculator.ts +1 -1
- package/treb-calculator/src/function-library.ts +25 -22
- package/treb-calculator/src/functions/base-functions.ts +166 -5
- package/treb-calculator/src/index.ts +6 -6
- package/treb-calculator/src/notifier-types.ts +1 -1
- package/treb-calculator/src/utilities.ts +2 -2
- package/treb-charts/src/util.ts +2 -2
- package/treb-embed/src/embedded-spreadsheet.ts +382 -71
- package/treb-embed/style/formula-bar.scss +2 -0
- package/treb-embed/style/theme-defaults.scss +46 -15
- package/treb-export/src/export-worker/export-worker.ts +0 -13
- package/treb-export/src/export2.ts +187 -2
- package/treb-export/src/import2.ts +169 -4
- package/treb-export/src/workbook-style2.ts +56 -8
- package/treb-export/src/workbook2.ts +10 -1
- package/treb-grid/src/editors/editor.ts +1276 -0
- package/treb-grid/src/editors/external_editor.ts +113 -0
- package/treb-grid/src/editors/formula_bar.ts +450 -474
- package/treb-grid/src/editors/overlay_editor.ts +437 -512
- package/treb-grid/src/index.ts +2 -1
- package/treb-grid/src/layout/base_layout.ts +24 -16
- package/treb-grid/src/render/tile_renderer.ts +2 -1
- package/treb-grid/src/types/conditional_format.ts +168 -0
- package/treb-grid/src/types/data_model.ts +130 -3
- package/treb-grid/src/types/external_editor_config.ts +47 -0
- package/treb-grid/src/types/grid.ts +96 -45
- package/treb-grid/src/types/grid_base.ts +187 -35
- package/treb-grid/src/types/scale-control.ts +1 -1
- package/treb-grid/src/types/sheet.ts +330 -26
- package/treb-grid/src/types/sheet_types.ts +4 -0
- package/treb-grid/src/util/dom_utilities.ts +58 -25
- package/treb-grid/src/editors/formula_editor_base.ts +0 -912
- package/treb-grid/src/types/external_editor.ts +0 -27
- /package/{README-shadow-DOM.md → notes/shadow-DOM.md} +0 -0
|
@@ -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
|
-
}
|