@trebco/treb 27.12.2 → 28.2.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/README.md +6 -0
- package/dist/treb-spreadsheet-light.mjs +16 -0
- package/dist/treb-spreadsheet.mjs +13 -11
- package/dist/treb.d.ts +33 -5
- package/esbuild-custom-element.mjs +3 -1
- package/package.json +8 -6
- package/treb-base-types/src/dom-utilities.ts +157 -19
- package/treb-base-types/src/import.ts +1 -0
- package/treb-base-types/src/theme.ts +5 -4
- package/treb-charts/src/renderer.ts +4 -58
- package/treb-embed/markup/layout.html +4 -0
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +131 -87
- package/treb-embed/src/embedded-spreadsheet.ts +153 -140
- package/treb-embed/src/options.ts +9 -0
- package/treb-embed/src/spinner.ts +5 -3
- package/treb-embed/src/toolbar-message.ts +5 -0
- package/treb-embed/style/layout.scss +65 -1
- package/treb-export/src/export-worker/export-worker.ts +7 -12
- package/treb-export/src/export2.ts +57 -33
- package/treb-export/src/import2.ts +61 -21
- package/treb-export/src/workbook2.ts +69 -24
- package/treb-export/src/zip-wrapper.ts +96 -0
- package/treb-grid/src/editors/autocomplete.ts +24 -13
- package/treb-grid/src/editors/editor.ts +43 -139
- package/treb-grid/src/editors/external_editor.ts +1 -1
- package/treb-grid/src/editors/formula_bar.ts +24 -24
- package/treb-grid/src/editors/overlay_editor.ts +1 -1
- package/treb-grid/src/layout/base_layout.ts +34 -25
- package/treb-grid/src/layout/grid_layout.ts +20 -20
- package/treb-grid/src/render/selection-renderer.ts +3 -3
- package/treb-grid/src/render/svg_header_overlay.ts +6 -4
- package/treb-grid/src/render/svg_selection_block.ts +10 -7
- package/treb-grid/src/types/annotation.ts +2 -2
- package/treb-grid/src/types/grid.ts +80 -81
- package/treb-grid/src/types/scale-control.ts +69 -81
- package/treb-grid/src/types/sheet.ts +3 -52
- package/treb-grid/src/types/tab_bar.ts +27 -13
- package/treb-grid/src/util/fontmetrics2.ts +24 -21
- package/treb-utils/src/event_source.ts +23 -23
- package/treb-utils/src/index.ts +2 -2
- package/treb-utils/src/measurement.ts +24 -24
- package/treb-utils/src/serialize_html.ts +25 -21
- package/treb-utils/src/dispatch.ts +0 -57
- package/treb-utils/src/resizable.ts +0 -159
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
import UZip from 'uzip';
|
|
3
|
+
import Base64JS from 'base64-js';
|
|
4
|
+
|
|
5
|
+
export class ZipWrapper {
|
|
6
|
+
|
|
7
|
+
public records: UZip.UZIPFiles;
|
|
8
|
+
public text: Map<string, string> = new Map();
|
|
9
|
+
|
|
10
|
+
public constructor(buffer: ArrayBuffer) {
|
|
11
|
+
this.records = UZip.parse(buffer);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* check if entry exists
|
|
16
|
+
*/
|
|
17
|
+
public Has(path: string) {
|
|
18
|
+
return this.text.has(path) || !!this.records?.[path];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* nondestructive
|
|
23
|
+
*/
|
|
24
|
+
public ArrayBuffer() {
|
|
25
|
+
|
|
26
|
+
const records: Record<string, Uint8Array> = {};
|
|
27
|
+
if (this.records) {
|
|
28
|
+
for (const [key, value] of Object.entries(this.records)) {
|
|
29
|
+
records[key] = new Uint8Array(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const encoder = new TextEncoder();
|
|
34
|
+
for (const [key, value] of this.text.entries()) {
|
|
35
|
+
records[key] = encoder.encode(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return UZip.encode(records);
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* set a binary file. set this directly in the records table,
|
|
44
|
+
* instead of the text table.
|
|
45
|
+
*/
|
|
46
|
+
public SetBinary(path: string, data: string, encoding?: 'base64'|'binary') {
|
|
47
|
+
|
|
48
|
+
if (encoding === 'base64') {
|
|
49
|
+
const bytes = Base64JS.toByteArray(data);
|
|
50
|
+
this.records[path] = bytes;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw new Error('unsupported encoding: ' + encoding);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public Set(path: string, text: string) {
|
|
59
|
+
|
|
60
|
+
this.text.set(path, text);
|
|
61
|
+
if (!!this.records[path]) {
|
|
62
|
+
delete this.records[path];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public GetBinary(path: string) {
|
|
67
|
+
const data = this.records[path];
|
|
68
|
+
if (data) {
|
|
69
|
+
return new Uint8Array(data);
|
|
70
|
+
}
|
|
71
|
+
throw new Error('path not in records: ' + path);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public Get(path: string) {
|
|
75
|
+
|
|
76
|
+
let text = this.text.get(path);
|
|
77
|
+
|
|
78
|
+
if (text) {
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = this.records[path];
|
|
83
|
+
if (data) {
|
|
84
|
+
text = new TextDecoder().decode(data);
|
|
85
|
+
this.text.set(path, text);
|
|
86
|
+
delete this.records[path];
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.info(this);
|
|
91
|
+
|
|
92
|
+
throw new Error('path not in zip file: ' + path);
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { DOMContext } from 'treb-base-types';
|
|
23
23
|
import type { Theme, Rectangle } from 'treb-base-types';
|
|
24
24
|
import type { AutocompleteExecResult, DescriptorType } from './autocomplete_matcher';
|
|
25
25
|
|
|
@@ -72,26 +72,37 @@ export class Autocomplete {
|
|
|
72
72
|
|
|
73
73
|
private active_element?: HTMLElement;
|
|
74
74
|
|
|
75
|
+
private DOM: DOMContext;
|
|
76
|
+
|
|
75
77
|
constructor(private options: AutocompleteOptions = {}){
|
|
76
78
|
|
|
79
|
+
this.DOM = DOMContext.GetInstance(options.container?.ownerDocument);
|
|
80
|
+
if (!this.DOM.doc) {
|
|
81
|
+
throw new Error('invalid context');
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// this.scope = 'AC' + Math.round(Math.random() * Math.pow(10, 10)).toString(16);
|
|
78
85
|
|
|
79
|
-
this.completion_list =
|
|
86
|
+
this.completion_list = this.DOM.Div(
|
|
80
87
|
'treb-cell-editor-ac-list treb-autocomplete',
|
|
81
|
-
options.container ||
|
|
82
|
-
|
|
88
|
+
options.container || this.DOM.doc.body,
|
|
89
|
+
{
|
|
90
|
+
events: {
|
|
91
|
+
mousedown: (event) => this.ListMouseDown(event),
|
|
92
|
+
mousemove: (event) => this.ListMouseMove(event)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
83
96
|
|
|
84
|
-
this.completion_list.addEventListener('mousedown', (event) => this.ListMouseDown(event));
|
|
97
|
+
// this.completion_list.addEventListener('mousedown', (event) => this.ListMouseDown(event));
|
|
85
98
|
|
|
86
99
|
// FIXME: should we add/remove listener based on visibility? (...)
|
|
87
100
|
|
|
88
|
-
this.completion_list.addEventListener('mousemove', (event) => this.ListMouseMove(event));
|
|
89
|
-
|
|
90
|
-
this.tooltip = DOMUtilities.Div('treb-cell-editor-ac-tooltip treb-autocomplete-tooltip',
|
|
91
|
-
options.container || document.body,
|
|
92
|
-
); // this.scope);
|
|
101
|
+
// this.completion_list.addEventListener('mousemove', (event) => this.ListMouseMove(event));
|
|
93
102
|
|
|
94
|
-
|
|
103
|
+
this.tooltip = this.DOM.Div(
|
|
104
|
+
'treb-cell-editor-ac-tooltip treb-autocomplete-tooltip',
|
|
105
|
+
options.container || this.DOM.doc.body);
|
|
95
106
|
|
|
96
107
|
}
|
|
97
108
|
|
|
@@ -360,8 +371,8 @@ export class Autocomplete {
|
|
|
360
371
|
// compiler thinks this is possibly undefined, but vs code does
|
|
361
372
|
// not -- I thought vs code used the same tsc we use to compile?
|
|
362
373
|
|
|
363
|
-
if (
|
|
364
|
-
const viewport_height = Math.max(
|
|
374
|
+
if (this.DOM.doc?.documentElement) {
|
|
375
|
+
const viewport_height = Math.max(this.DOM.doc.documentElement.clientHeight, this.DOM.view?.innerHeight || 0);
|
|
365
376
|
if (viewport_height - position.bottom < 200 ){
|
|
366
377
|
layout_top = true;
|
|
367
378
|
}
|
|
@@ -32,9 +32,15 @@
|
|
|
32
32
|
*
|
|
33
33
|
* subclasses or callers can handle those.
|
|
34
34
|
*
|
|
35
|
+
* ---
|
|
36
|
+
*
|
|
37
|
+
* NOTE: external editors might run in a different realm (in the js meaning
|
|
38
|
+
* of that term). so we don't necessarily want to use the spreadsheet's context
|
|
39
|
+
* for everything. this is going to be extremely confusing.
|
|
40
|
+
*
|
|
35
41
|
*/
|
|
36
42
|
|
|
37
|
-
import { Area, type IArea, type ICellAddress, IsCellAddress, Localization, type Theme, Rectangle, type Cell,
|
|
43
|
+
import { Area, type IArea, type ICellAddress, IsCellAddress, Localization, type Theme, Rectangle, type Cell, DOMContext } from 'treb-base-types';
|
|
38
44
|
import type { ExpressionUnit, ParseResult, UnitAddress, UnitRange } from 'treb-parser';
|
|
39
45
|
import { Parser, QuotedSheetNameRegex } from 'treb-parser';
|
|
40
46
|
import type { DataModel, ViewModel } from '../types/data_model';
|
|
@@ -221,15 +227,16 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
221
227
|
// handles that but this will return false. the test is "is the
|
|
222
228
|
// cursor in or at the end of a reference?"
|
|
223
229
|
|
|
224
|
-
if (this.active_editor && this.active_editor.node ===
|
|
230
|
+
if (this.active_editor && this.active_editor.node === this.active_editor.node.ownerDocument.activeElement) {
|
|
225
231
|
|
|
226
|
-
const
|
|
232
|
+
const view = this.active_editor.node.ownerDocument.defaultView as (Window & typeof globalThis);
|
|
233
|
+
const selection = view.getSelection();
|
|
227
234
|
const count = selection?.rangeCount;
|
|
228
235
|
|
|
229
236
|
if (count) {
|
|
230
237
|
|
|
231
238
|
const range = selection?.getRangeAt(0);
|
|
232
|
-
const element = range?.endContainer instanceof HTMLElement ? range.endContainer :
|
|
239
|
+
const element = range?.endContainer instanceof view.HTMLElement ? range.endContainer :
|
|
233
240
|
range.endContainer?.parentElement;
|
|
234
241
|
|
|
235
242
|
// this is a reference, assume we're selecting (we will replace)
|
|
@@ -249,7 +256,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
249
256
|
|
|
250
257
|
// start, not end
|
|
251
258
|
|
|
252
|
-
if (range?.startContainer instanceof Text) {
|
|
259
|
+
if (range?.startContainer instanceof view.Text) {
|
|
253
260
|
const str = (range.startContainer.textContent?.substring(0, range.startOffset) || '').trim();
|
|
254
261
|
if (str.length && Editor.FormulaChars.includes(str[str.length - 1])) {
|
|
255
262
|
return true;
|
|
@@ -315,8 +322,9 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
protected SelectAll(node: HTMLElement) {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
325
|
+
const view = node.ownerDocument.defaultView as (Window & typeof globalThis);
|
|
326
|
+
const selection = view.getSelection();
|
|
327
|
+
const range = node.ownerDocument.createRange();
|
|
320
328
|
range.selectNode(node);
|
|
321
329
|
selection?.removeAllRanges();
|
|
322
330
|
selection?.addRange(range);
|
|
@@ -326,12 +334,15 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
326
334
|
start: { node: ChildNode, offset: number },
|
|
327
335
|
end?: { node: ChildNode, offset: number }) {
|
|
328
336
|
|
|
329
|
-
const
|
|
330
|
-
const
|
|
337
|
+
const doc = start.node.ownerDocument;
|
|
338
|
+
const view = doc?.defaultView as (Window & typeof globalThis);
|
|
339
|
+
|
|
340
|
+
const selection = view.getSelection();
|
|
341
|
+
const range = doc?.createRange();
|
|
331
342
|
|
|
332
343
|
const FirstTextNode = (node: ChildNode) => {
|
|
333
344
|
let target: Node = node;
|
|
334
|
-
while (target && !(target instanceof Text) && !!target.firstChild) {
|
|
345
|
+
while (target && !(target instanceof view.Text) && !!target.firstChild) {
|
|
335
346
|
target = target.firstChild;
|
|
336
347
|
}
|
|
337
348
|
return target;
|
|
@@ -369,7 +380,9 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
369
380
|
return;
|
|
370
381
|
}
|
|
371
382
|
|
|
372
|
-
const
|
|
383
|
+
const view = this.active_editor.node.ownerDocument.defaultView as (Window & typeof globalThis);
|
|
384
|
+
|
|
385
|
+
const selection = view.getSelection();
|
|
373
386
|
if (!selection) {
|
|
374
387
|
throw new Error('error getting selection');
|
|
375
388
|
}
|
|
@@ -401,7 +414,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
401
414
|
// might include _more_ than an existing reference, and we want to
|
|
402
415
|
// replace the entire range selection.
|
|
403
416
|
|
|
404
|
-
if (range.startContainer instanceof Text) {
|
|
417
|
+
if (range.startContainer instanceof view.Text) {
|
|
405
418
|
|
|
406
419
|
// first case: range selected
|
|
407
420
|
if (!range.collapsed && range.startOffset < range.endOffset) {
|
|
@@ -436,7 +449,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
436
449
|
// check if we're in a reference node; if so, replace
|
|
437
450
|
|
|
438
451
|
const parent = range.startContainer.parentElement;
|
|
439
|
-
if (parent instanceof HTMLElement && parent.dataset.reference) {
|
|
452
|
+
if (parent instanceof view.HTMLElement && parent.dataset.reference) {
|
|
440
453
|
|
|
441
454
|
// console.info('case 2');
|
|
442
455
|
|
|
@@ -508,7 +521,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
508
521
|
// the text. we'll want to create a node, and we'll want to set the
|
|
509
522
|
// cursor at the end.
|
|
510
523
|
|
|
511
|
-
if (range.startContainer instanceof HTMLElement) {
|
|
524
|
+
if (range.startContainer instanceof view.HTMLElement) {
|
|
512
525
|
range.startContainer.textContent = reference;
|
|
513
526
|
this.SetCaret({
|
|
514
527
|
node: range.startContainer,
|
|
@@ -556,6 +569,8 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
556
569
|
*/
|
|
557
570
|
protected UpdateColors(force_event = false) {
|
|
558
571
|
|
|
572
|
+
const view = this.active_editor?.node.ownerDocument.defaultView as (Window & typeof globalThis);
|
|
573
|
+
|
|
559
574
|
// create a map of canonical label -> area
|
|
560
575
|
|
|
561
576
|
const map: Map<string, Area> = new Map();
|
|
@@ -582,7 +597,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
582
597
|
|
|
583
598
|
for (const entry of this.nodes) {
|
|
584
599
|
for (const node of Array.from(entry.node.childNodes)) {
|
|
585
|
-
if (node instanceof HTMLElement && node.dataset.reference) {
|
|
600
|
+
if (node instanceof view.HTMLElement && node.dataset.reference) {
|
|
586
601
|
const index = indexes.get(node.dataset.reference);
|
|
587
602
|
node.dataset.highlightIndex = (typeof index === 'number') ? (index % 5 + 1).toString() : '?';
|
|
588
603
|
}
|
|
@@ -765,6 +780,8 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
765
780
|
const node = descriptor.node;
|
|
766
781
|
const text = node.textContent || '';
|
|
767
782
|
|
|
783
|
+
const DOM = DOMContext.GetInstance(node.ownerDocument);
|
|
784
|
+
|
|
768
785
|
// set this flag so we can use it in `get selected()`
|
|
769
786
|
|
|
770
787
|
this.text_formula = text[0] === '=';
|
|
@@ -843,11 +860,11 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
843
860
|
let text_index = 0;
|
|
844
861
|
let last_text_node: Text|undefined;
|
|
845
862
|
|
|
846
|
-
const fragment =
|
|
863
|
+
const fragment = DOM.Fragment();
|
|
847
864
|
|
|
848
865
|
const AddNode = (text: string, type = 'text', reference = '', force_selection = false) => {
|
|
849
866
|
|
|
850
|
-
const text_node =
|
|
867
|
+
const text_node = DOM.Text(text);
|
|
851
868
|
|
|
852
869
|
if (force_selection || ((caret_start > text_index || (caret_start === 0 && text_index === 0)) && caret_start <= text_index + text.length)) {
|
|
853
870
|
selection_start = {
|
|
@@ -865,7 +882,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
865
882
|
|
|
866
883
|
if (type !== 'text') {
|
|
867
884
|
|
|
868
|
-
const span =
|
|
885
|
+
const span = DOM.Create('span', type);
|
|
869
886
|
|
|
870
887
|
if (reference) {
|
|
871
888
|
span.dataset.reference = reference;
|
|
@@ -1101,10 +1118,13 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
1101
1118
|
|
|
1102
1119
|
const result: [string, string] = ['', ''];
|
|
1103
1120
|
|
|
1104
|
-
if (node !==
|
|
1121
|
+
if (node !== node.ownerDocument.activeElement || node !== this.active_editor?.node) {
|
|
1105
1122
|
return result;
|
|
1106
1123
|
}
|
|
1107
|
-
|
|
1124
|
+
|
|
1125
|
+
const doc = node.ownerDocument;
|
|
1126
|
+
const view = doc.defaultView as (Window & typeof globalThis);
|
|
1127
|
+
|
|
1108
1128
|
// is there a way to do this without recursing? (...)
|
|
1109
1129
|
// how about string concat instead of array join, it's faster in
|
|
1110
1130
|
// chrome (!)
|
|
@@ -1125,7 +1145,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
1125
1145
|
// not sure what the offsets are even referring to, since we get
|
|
1126
1146
|
// offsets > the number of child nodes. is this a bug in firefox?
|
|
1127
1147
|
|
|
1128
|
-
if (element === range.startContainer && element === range.endContainer && !(element instanceof Text)) {
|
|
1148
|
+
if (element === range.startContainer && element === range.endContainer && !(element instanceof view.Text)) {
|
|
1129
1149
|
|
|
1130
1150
|
/*
|
|
1131
1151
|
if (range.startOffset !== 0 || range.endOffset !== 0) {
|
|
@@ -1155,7 +1175,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
1155
1175
|
return;
|
|
1156
1176
|
}
|
|
1157
1177
|
|
|
1158
|
-
if (element instanceof Text) {
|
|
1178
|
+
if (element instanceof view.Text) {
|
|
1159
1179
|
const text = element.textContent || '';
|
|
1160
1180
|
if (!complete[0]) {
|
|
1161
1181
|
result[0] += text;
|
|
@@ -1174,7 +1194,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
1174
1194
|
}
|
|
1175
1195
|
};
|
|
1176
1196
|
|
|
1177
|
-
const selection =
|
|
1197
|
+
const selection = view.getSelection();
|
|
1178
1198
|
if (selection?.rangeCount ?? 0 > 0) {
|
|
1179
1199
|
const range = selection?.getRangeAt(0);
|
|
1180
1200
|
if (range) {
|
|
@@ -1185,120 +1205,4 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
|
|
|
1185
1205
|
return result;
|
|
1186
1206
|
}
|
|
1187
1207
|
|
|
1188
|
-
/* *
|
|
1189
|
-
* get text up to the selection. optionally use the start of the
|
|
1190
|
-
* selection. new version does not require cloning, we can drop the
|
|
1191
|
-
* measurement node.
|
|
1192
|
-
*
|
|
1193
|
-
* @param node
|
|
1194
|
-
* @param start
|
|
1195
|
-
* @returns
|
|
1196
|
-
* /
|
|
1197
|
-
protected SubstringToCaret(node: HTMLElement, start = false): string {
|
|
1198
|
-
|
|
1199
|
-
if (node !== document.activeElement) {
|
|
1200
|
-
return '';
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// is there a way to do this without recursing? (...)
|
|
1204
|
-
// how about string concat instead of array join, it's faster in chrome (!)
|
|
1205
|
-
|
|
1206
|
-
const Consume = (element: Node, target: Node, offset: number, parts: string[]): boolean => {
|
|
1207
|
-
if (element === target) {
|
|
1208
|
-
parts.push((element.textContent || '').substring(0, offset));
|
|
1209
|
-
return false;
|
|
1210
|
-
}
|
|
1211
|
-
else if (element instanceof Text) {
|
|
1212
|
-
parts.push(element.textContent || '');
|
|
1213
|
-
return true;
|
|
1214
|
-
}
|
|
1215
|
-
else if (element.hasChildNodes()) {
|
|
1216
|
-
for (const child of element.childNodes) {
|
|
1217
|
-
const result = Consume(child, target, offset, parts);
|
|
1218
|
-
if (!result) {
|
|
1219
|
-
return false;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
return true;
|
|
1223
|
-
}
|
|
1224
|
-
return false;
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
const selection = window.getSelection();
|
|
1228
|
-
if (selection?.rangeCount ?? 0 > 0) {
|
|
1229
|
-
|
|
1230
|
-
const range = selection?.getRangeAt(0);
|
|
1231
|
-
if (range) {
|
|
1232
|
-
let [target, offset] = start ?
|
|
1233
|
-
[range.startContainer, range.startOffset] :
|
|
1234
|
-
[range.endContainer, range.endOffset];
|
|
1235
|
-
|
|
1236
|
-
const parts: string[] = [];
|
|
1237
|
-
Consume(node, target, offset, parts);
|
|
1238
|
-
return parts.join('');
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
return '';
|
|
1245
|
-
|
|
1246
|
-
}
|
|
1247
|
-
*/
|
|
1248
|
-
|
|
1249
|
-
/* *
|
|
1250
|
-
* we've been carrying this function around for a while. is this
|
|
1251
|
-
* still the best way to do this in 2023? (...)
|
|
1252
|
-
*
|
|
1253
|
-
* get text substring to caret position, irrespective of node structure
|
|
1254
|
-
*
|
|
1255
|
-
* @param start - use the start of the selection instead of the end
|
|
1256
|
-
* /
|
|
1257
|
-
protected SubstringToCaret(node: HTMLElement, start = false): string {
|
|
1258
|
-
|
|
1259
|
-
if (node !== this.editor_node || node !== document.activeElement) {
|
|
1260
|
-
return '';
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// we could probably shortcut if text is empty
|
|
1264
|
-
|
|
1265
|
-
const selection = window.getSelection();
|
|
1266
|
-
if (!selection) {
|
|
1267
|
-
throw new Error('error getting selection');
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
if (selection.rangeCount === 0) {
|
|
1271
|
-
// console.warn('range count is 0');
|
|
1272
|
-
return '';
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const range = selection.getRangeAt(0);
|
|
1276
|
-
const preCaretRange = range.cloneRange();
|
|
1277
|
-
|
|
1278
|
-
preCaretRange.selectNodeContents(node);
|
|
1279
|
-
if (start) {
|
|
1280
|
-
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
1281
|
-
}
|
|
1282
|
-
else {
|
|
1283
|
-
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
this.measurement_node.textContent = '';
|
|
1287
|
-
this.measurement_node.appendChild(preCaretRange.cloneContents());
|
|
1288
|
-
|
|
1289
|
-
const result = this.measurement_node.textContent || '';
|
|
1290
|
-
const result2 = this.SubstringToCaret2(node, start);
|
|
1291
|
-
|
|
1292
|
-
// console.info('X', result === result2, {result, result2});
|
|
1293
|
-
if (result !== result2) {
|
|
1294
|
-
throw new Error('mismatch');
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
return result;
|
|
1298
|
-
|
|
1299
|
-
// return this.measurement_node.textContent || '';
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
*/
|
|
1303
|
-
|
|
1304
1208
|
}
|
|
@@ -70,7 +70,7 @@ export class ExternalEditor extends Editor {
|
|
|
70
70
|
|
|
71
71
|
// if it's already focused, set as active
|
|
72
72
|
|
|
73
|
-
if (
|
|
73
|
+
if (descriptor.node.ownerDocument.activeElement === descriptor.node) {
|
|
74
74
|
this.active_editor = descriptor;
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -25,7 +25,7 @@ import { Parser } from 'treb-parser';
|
|
|
25
25
|
import type { DataModel, ViewModel } from '../types/data_model';
|
|
26
26
|
import type { GridOptions } from '../types/grid_options';
|
|
27
27
|
import { Autocomplete } from './autocomplete';
|
|
28
|
-
import {
|
|
28
|
+
import { DOMContext } from 'treb-base-types';
|
|
29
29
|
|
|
30
30
|
// --- from formula_bar ---
|
|
31
31
|
|
|
@@ -162,6 +162,7 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
162
162
|
) {
|
|
163
163
|
|
|
164
164
|
super(model, view, autocomplete);
|
|
165
|
+
const DOM = DOMContext.GetInstance(container.ownerDocument);
|
|
165
166
|
|
|
166
167
|
const inner_node = container.querySelector('.treb-formula-bar') as HTMLElement;
|
|
167
168
|
inner_node.removeAttribute('hidden');
|
|
@@ -172,7 +173,7 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
172
173
|
this.InitAddressLabel();
|
|
173
174
|
|
|
174
175
|
if (this.options.insert_function_button) {
|
|
175
|
-
this.button =
|
|
176
|
+
this.button = DOM.Create('button', 'formula-button', inner_node);
|
|
176
177
|
this.button.addEventListener('click', () => {
|
|
177
178
|
const formula: string = this.active_editor ? this.active_editor.node.textContent || '' : '';
|
|
178
179
|
this.Publish({ type: 'formula-button', formula });
|
|
@@ -270,20 +271,24 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
270
271
|
this.RegisterListener(descriptor, 'keyup', this.FormulaKeyUp.bind(this));
|
|
271
272
|
|
|
272
273
|
if (this.options.expand_formula_button) {
|
|
273
|
-
this.expand_button =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
this.expand_button = DOM.Create('button', 'expand-button', inner_node, {
|
|
275
|
+
events: {
|
|
276
|
+
click: (event: MouseEvent) => {
|
|
277
|
+
event.stopPropagation();
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
if (this.active_editor) {
|
|
280
|
+
this.active_editor.node.scrollTop = 0;
|
|
281
|
+
}
|
|
282
|
+
if (inner_node.hasAttribute('expanded')) {
|
|
283
|
+
inner_node.removeAttribute('expanded');
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
inner_node.setAttribute('expanded', '');
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
},
|
|
286
290
|
});
|
|
291
|
+
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
}
|
|
@@ -303,24 +308,19 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
303
308
|
|
|
304
309
|
this.address_label.addEventListener('focusin', (event) => {
|
|
305
310
|
|
|
311
|
+
const doc = this.address_label.ownerDocument;
|
|
312
|
+
|
|
306
313
|
// FIXME: close any open editors? (...)
|
|
307
314
|
|
|
308
315
|
// we're now doing this async for all browsers... it's only really
|
|
309
316
|
// necessary for IE11 and safari, but doesn't hurt
|
|
310
317
|
|
|
311
318
|
requestAnimationFrame(() => {
|
|
312
|
-
|
|
313
|
-
const range =
|
|
314
|
-
range.moveToElementText(this.address_label);
|
|
315
|
-
range.select();
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
const selection = window.getSelection();
|
|
319
|
-
const range = document.createRange();
|
|
319
|
+
const selection = (doc.defaultView as (Window & typeof globalThis)).getSelection();
|
|
320
|
+
const range = doc.createRange();
|
|
320
321
|
range.selectNodeContents(this.address_label);
|
|
321
322
|
selection?.removeAllRanges();
|
|
322
323
|
selection?.addRange(range);
|
|
323
|
-
}
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
});
|
|
@@ -209,7 +209,7 @@ export class OverlayEditor extends Editor<ResetSelectionEvent> {
|
|
|
209
209
|
// by default the editor is at (0, 0), so we need to move it before we
|
|
210
210
|
// focus on it (but only in this case).
|
|
211
211
|
|
|
212
|
-
if (this.edit_node !==
|
|
212
|
+
if (this.edit_node !== this.edit_node.ownerDocument.activeElement) {
|
|
213
213
|
|
|
214
214
|
// this was not correct, but should we add those 2 pixels back?
|
|
215
215
|
|