@trebco/treb 27.12.2 → 28.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +6 -0
  2. package/dist/treb-spreadsheet-light.mjs +16 -0
  3. package/dist/treb-spreadsheet.mjs +14 -12
  4. package/dist/treb.d.ts +31 -3
  5. package/esbuild-custom-element.mjs +3 -1
  6. package/package.json +4 -4
  7. package/treb-base-types/src/dom-utilities.ts +157 -19
  8. package/treb-base-types/src/theme.ts +5 -4
  9. package/treb-charts/src/renderer.ts +4 -58
  10. package/treb-embed/markup/layout.html +4 -0
  11. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +131 -87
  12. package/treb-embed/src/embedded-spreadsheet.ts +146 -111
  13. package/treb-embed/src/options.ts +9 -0
  14. package/treb-embed/src/spinner.ts +5 -3
  15. package/treb-embed/src/toolbar-message.ts +5 -0
  16. package/treb-embed/style/layout.scss +65 -1
  17. package/treb-grid/src/editors/autocomplete.ts +24 -13
  18. package/treb-grid/src/editors/editor.ts +43 -139
  19. package/treb-grid/src/editors/external_editor.ts +1 -1
  20. package/treb-grid/src/editors/formula_bar.ts +24 -24
  21. package/treb-grid/src/editors/overlay_editor.ts +1 -1
  22. package/treb-grid/src/layout/base_layout.ts +34 -25
  23. package/treb-grid/src/layout/grid_layout.ts +20 -20
  24. package/treb-grid/src/render/selection-renderer.ts +3 -3
  25. package/treb-grid/src/render/svg_header_overlay.ts +6 -4
  26. package/treb-grid/src/render/svg_selection_block.ts +10 -7
  27. package/treb-grid/src/types/grid.ts +80 -81
  28. package/treb-grid/src/types/scale-control.ts +69 -81
  29. package/treb-grid/src/types/sheet.ts +3 -52
  30. package/treb-grid/src/types/tab_bar.ts +27 -13
  31. package/treb-grid/src/util/fontmetrics2.ts +24 -21
  32. package/treb-utils/src/event_source.ts +23 -23
  33. package/treb-utils/src/index.ts +2 -2
  34. package/treb-utils/src/measurement.ts +24 -24
  35. package/treb-utils/src/serialize_html.ts +25 -21
  36. package/treb-utils/src/dispatch.ts +0 -57
  37. package/treb-utils/src/resizable.ts +0 -159
package/dist/treb.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v27.12. Copyright 2018-2023 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v28.0. Copyright 2018-2023 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
 
3
3
  /**
4
4
  * add our tag to the map
@@ -86,6 +86,9 @@ export interface EmbeddedSpreadsheetOptions {
86
86
  /** add resizable wrapper */
87
87
  resizable?: boolean;
88
88
 
89
+ /** even if we allow resizing, constrain width. this is to support fixed width columns. */
90
+ constrain_width?: boolean;
91
+
89
92
  /** export to xlsx, now optional */
90
93
  export?: boolean;
91
94
 
@@ -247,6 +250,12 @@ export interface EmbeddedSpreadsheetOptions {
247
250
  * was renamed from `revert` to avoid any ambiguity.
248
251
  */
249
252
  revert_button?: boolean;
253
+
254
+ /**
255
+ * show the revert indicator. this is an indicator that shows on the
256
+ * top-left of the spreadsheet when a network document has local changes.
257
+ */
258
+ revert_indicator?: boolean;
250
259
  }
251
260
 
252
261
  /**
@@ -302,6 +311,22 @@ export declare class EmbeddedSpreadsheet {
302
311
  */
303
312
  get state(): number;
304
313
 
314
+ /**
315
+ * this flag indicates we can revert the document. what that means is
316
+ * we loaded a user-created version from localStorage, but there's a
317
+ * backing network or inline document. or we did load the original version
318
+ * but the user has made some document changes.
319
+ *
320
+ * it's like `dirty`, but that uses the load source as the ground truth,
321
+ * which means if you load a modified document from localStorage it's
322
+ * initially considered not-dirty (which is maybe just a bad design?)
323
+ *
324
+ * the intent of this field is to support enabling/disabling revert
325
+ * logic, or to add a visual indicator that you are not looking at the
326
+ * canonical version.
327
+ */
328
+ get can_revert(): boolean;
329
+
305
330
  /**
306
331
  * indicates the current revision of the document is not equal to the
307
332
  * last-saved revision of the document.
@@ -550,8 +575,11 @@ export declare class EmbeddedSpreadsheet {
550
575
  UnmergeCells(range?: RangeReference): void;
551
576
 
552
577
  /**
553
- * revert to the network version of this document, if both `local_storage`
554
- * and `network_document` are set.
578
+ * revert to the network version of this document, if `local_storage`
579
+ * is set and the create options had either `document` or `inline-document`
580
+ * set.
581
+ *
582
+ * FIXME: we should adjust for documents that fail to load.
555
583
  */
556
584
  Revert(): void;
557
585
 
@@ -3,6 +3,7 @@
3
3
  import * as esbuild from 'esbuild';
4
4
 
5
5
  import { SassPlugin, WorkerPlugin, NotifyPlugin, HTMLPlugin } from './esbuild-utils.mjs';
6
+ import { promises as fs } from 'fs';
6
7
 
7
8
  import pkg from './package.json' assert { type: 'json' };
8
9
 
@@ -90,5 +91,6 @@ if (options.watch) {
90
91
  await context.watch();
91
92
  }
92
93
  else {
93
- await esbuild.build(build_options);
94
+ const result = await esbuild.build(build_options);
95
+ await fs.writeFile('esbuild-metafile.json', JSON.stringify(result.metafile), { encoding: 'utf-8' });
94
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "27.12.2",
3
+ "version": "28.0.5",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "type": "module",
13
13
  "devDependencies": {
14
14
  "@types/html-minifier": "^4.0.2",
15
- "@types/node": "^20.4.0",
15
+ "@types/node": "^20.8.5",
16
16
  "@typescript-eslint/eslint-plugin": "^6.1.0",
17
17
  "@typescript-eslint/parser": "^6.1.0",
18
18
  "archiver": "^6.0.1",
@@ -22,7 +22,7 @@
22
22
  "fast-xml-parser": "^4.0.7",
23
23
  "html-minifier": "^4.0.0",
24
24
  "jszip": "^3.6.0",
25
- "sass": "^1.53.0",
25
+ "sass": "^1.69.3",
26
26
  "treb-base-types": "file:treb-base-types",
27
27
  "treb-calculator": "file:treb-calculator",
28
28
  "treb-charts": "file:treb-charts",
@@ -47,6 +47,6 @@
47
47
  "watch-tsc": "tsc -b treb-embed/modern.tsconfig.json -w",
48
48
  "watch-api": "ts-node-dev --respawn api-generator/api-generator.ts --config api-config.json",
49
49
  "generate-api": "ts-node-dev api-generator/api-generator.ts --config api-config.json",
50
- "release": "npm run rebuild-tsc && npm run build && npm run generate-api"
50
+ "release": "npm run rebuild-tsc && npm run build && npm run generate-api && npm run build-light"
51
51
  }
52
52
  }
@@ -21,24 +21,120 @@
21
21
 
22
22
  const SVGNS = 'http://www.w3.org/2000/svg';
23
23
 
24
- export class DOMUtilities {
24
+ type EventHandlerMap = {
25
+ [key in keyof HTMLElementEventMap]: (event: HTMLElementEventMap[key]) => any;
26
+ }
27
+
28
+ type StyleMap = {
29
+ [key in keyof CSSStyleDeclaration]: string;
30
+ }
31
+
32
+ export interface CreateElementOptions {
33
+
34
+ /**
35
+ * FIXME: can we lock this down a bit?
36
+ *
37
+ * actually that might not be wise since we sometimes use "scope" type
38
+ * attributes that are not standard (maybe we should fix that, instead?)
39
+ */
40
+ attrs?: Record<string, string>;
41
+
42
+ /**
43
+ * optionally style as a map
44
+ */
45
+ style?: Partial<StyleMap>;
46
+
47
+ /** dataset or data-* attributes */
48
+ data?: Record<string, string>;
49
+
50
+ /** innerText */
51
+ text?: string;
52
+
53
+ /** innerHTML */
54
+ html?: string;
55
+
56
+ /** event handlers */
57
+ events?: Partial<EventHandlerMap>;
58
+
59
+ }
60
+
61
+ /**
62
+ * we still need a good way to represent the 'no-DOM' context,
63
+ * ideally without throwing exceptions.
64
+ *
65
+ */
66
+ export class DOMContext {
67
+
68
+ protected static instances: DOMContext[] = [];
69
+
70
+ /**
71
+ * FIXME: how about we default to document, so it won't break?
72
+ * that will make it harder to debug though.
73
+ */
74
+ public static GetInstance(doc?: Document) {
75
+
76
+ for (const instance of this.instances) {
77
+ if (instance.doc === doc) {
78
+ return instance;
79
+ }
80
+ }
81
+
82
+ // not found, create
83
+ const instance = new DOMContext(doc);
84
+ this.instances.push(instance);
85
+ return instance;
86
+
87
+ }
88
+
89
+ /** ugh sloppy */
90
+ public doc?: Document; // placeholder temp
91
+
92
+ /** ugh sloppy */
93
+ public view?: (Window & typeof globalThis) | null;
94
+
95
+ /*
96
+ public get document(): Document {
97
+ return this.doc;
98
+ }
99
+
100
+ public get window(): (Window & typeof globalThis) {
101
+ return this.view;
102
+ }
103
+ */
104
+
105
+ /** class for `instanceof` comparison */
106
+ public get HTMLElement() {
107
+ return this.view?.HTMLElement;
108
+ }
109
+
110
+ protected constructor(doc?: Document) {
111
+ if (doc) {
112
+ this.doc = doc;
113
+ this.view = doc?.defaultView;
114
+ }
115
+ }
116
+
117
+ /** wrapper for window.getSelection */
118
+ public GetSelection() {
119
+ return this.view?.getSelection();
120
+ }
25
121
 
26
122
  /** creates a div and assigns class name/names */
27
- public static Div(classes?: string|string[], parent?: HTMLElement, scope?: string): HTMLDivElement {
28
- return this.Create('div', classes, parent, scope);
123
+ public Div(classes?: string|string[], parent?: HTMLElement, options?: CreateElementOptions): HTMLDivElement {
124
+ return this.Create('div', classes, parent, options);
29
125
  }
30
126
 
31
- public static ClassNames(element: HTMLElement|SVGElement, classes: string|string[]) {
127
+ public ClassNames(element: HTMLElement|SVGElement, classes: string|string[]) {
32
128
  element.classList.add(...(Array.isArray(classes) ? classes : [classes]).reduce((arr: string[], entry) => [...arr, ...entry.split(/\s+/g)], []));
33
129
  }
34
130
 
35
- public static SVG<K extends keyof SVGElementTagNameMap>(
131
+ public SVG<K extends keyof SVGElementTagNameMap>(
36
132
  tag: K,
37
133
  classes?: string|string[],
38
- parent?: HTMLElement|SVGElement
134
+ parent?: HTMLElement|SVGElement|DocumentFragment
39
135
  ): SVGElementTagNameMap[K] {
40
136
 
41
- const element = document.createElementNS(SVGNS, tag);
137
+ const element = (this.doc as Document).createElementNS(SVGNS, tag);
42
138
 
43
139
  if (classes) {
44
140
  this.ClassNames(element, classes);
@@ -51,29 +147,71 @@ export class DOMUtilities {
51
147
  return element;
52
148
  }
53
149
 
150
+ /**
151
+ * this is a wrapper for createTextNode. but if we want to expose
152
+ * the element/node classes (@see HTMLElement, above) then this
153
+ * should properly be the Text class and not a method. So we should
154
+ * rename it.
155
+ *
156
+ * @param data
157
+ * @returns
158
+ */
159
+ public Text(data: string) {
160
+ return (this.doc as Document).createTextNode(data);
161
+ }
162
+
163
+ public Fragment() {
164
+ return (this.doc as Document).createDocumentFragment();
165
+ }
166
+
54
167
  /** better typing */
55
- public static Create<K extends keyof HTMLElementTagNameMap>(
168
+ public Create<K extends keyof HTMLElementTagNameMap>(
56
169
  tag: K,
57
170
  classes?: string|string[],
58
- parent?: HTMLElement,
59
- scope?: string,
60
- attrs?: Record<string, string>): HTMLElementTagNameMap[K] {
171
+ parent?: HTMLElement|DocumentFragment,
172
+ options?: CreateElementOptions
173
+ ): HTMLElementTagNameMap[K] {
61
174
 
62
- const element = document.createElement(tag);
175
+ const element = (this.doc as Document).createElement(tag);
63
176
 
64
177
  if (classes) {
65
178
  this.ClassNames(element, classes);
66
179
  }
67
180
 
68
- if (scope) {
69
- element.setAttribute(scope, ''); // scope?
70
- }
181
+ if (options) {
182
+
183
+ if (options.attrs) {
184
+ for (const [key, value] of Object.entries(options.attrs)) {
185
+ element.setAttribute(key, value);
186
+ }
187
+ }
188
+
189
+ if (options.data) {
190
+ for (const [key, value] of Object.entries(options.data)) {
191
+ element.dataset[key] = value;
192
+ }
193
+ }
194
+
195
+ if (options.text) {
196
+ element.textContent = options.text;
197
+ }
198
+
199
+ if (options.html) {
200
+ element.innerHTML = options.html;
201
+ }
71
202
 
72
- if (attrs) {
73
- const keys = Object.keys(attrs);
74
- for (const key of keys) {
75
- element.setAttribute(key, attrs[key]);
203
+ if (options.events) {
204
+ for (const [key, value] of Object.entries(options.events)) {
205
+ element.addEventListener(key, value as any); // typing works well up until this point
206
+ }
76
207
  }
208
+
209
+ if (options.style) {
210
+ for (const [key, value] of Object.entries(options.style)) {
211
+ (element.style as any)[key] = value; // more sloppy typing
212
+ }
213
+ }
214
+
77
215
  }
78
216
 
79
217
  if (parent) {
@@ -21,7 +21,7 @@
21
21
 
22
22
  import type { Color, CellStyle } from './style';
23
23
  import { ColorFunctions } from './color';
24
- import { DOMUtilities } from './dom-utilities';
24
+ import { DOMContext } from './dom-utilities';
25
25
 
26
26
  /*
27
27
  * so this is a little strange. we use CSS to populate a theme object,
@@ -493,13 +493,14 @@ export const ThemeColorTable = (theme_color: number, tint = .7): TableTheme => {
493
493
  export const LoadThemeProperties = (container: HTMLElement): Theme => {
494
494
 
495
495
  const theme: Theme = JSON.parse(JSON.stringify(DefaultTheme));
496
+ const DOM = DOMContext.GetInstance(container.ownerDocument);
496
497
 
497
498
  const Append = (parent: HTMLElement, classes: string): HTMLDivElement => {
498
- return DOMUtilities.Div(classes, parent);
499
+ return DOM.Div(classes, parent);
499
500
  }
500
501
 
501
502
  const ElementCSS = (parent: HTMLElement, classes: string): CSSStyleDeclaration => {
502
- return window.getComputedStyle(Append(parent, classes));
503
+ return DOM.view?.getComputedStyle(Append(parent, classes)) as CSSStyleDeclaration;
503
504
  }
504
505
 
505
506
  const node = Append(container, '');
@@ -572,7 +573,7 @@ export const LoadThemeProperties = (container: HTMLElement): Theme => {
572
573
  // we could just parse, we know the returned css format is going
573
574
  // to be an rgb triple (I think?)
574
575
 
575
- const canvas = DOMUtilities.Create('canvas');
576
+ const canvas = DOM.Create('canvas');
576
577
 
577
578
  canvas.width = 3;
578
579
  canvas.height = 3;
@@ -33,33 +33,6 @@ export interface Metrics {
33
33
  y_offset: number;
34
34
  }
35
35
 
36
- // const trident = /trident/i.test(navigator?.userAgent || '');
37
-
38
- /*
39
- let dom_parser: DOMParser | undefined;
40
- const SetSVG = trident ? (node: SVGElement, svg: string) => {
41
-
42
- if (!dom_parser) {
43
- dom_parser = new DOMParser();
44
- (dom_parser as any).async = false;
45
- }
46
-
47
- const element = dom_parser.parseFromString(
48
- '<svg xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\'>' + svg + '</svg>',
49
- 'text/xml').documentElement;
50
-
51
- node.textContent = '';
52
-
53
- let child = element.firstChild;
54
-
55
- while (child) {
56
- node.appendChild(document.importNode(child, true));
57
- child = child.nextSibling;
58
- }
59
-
60
- } : (node: SVGElement, svg: string) => node.innerHTML = svg;
61
- */
62
-
63
36
  const SVGNode = (tag: string, attribute_map: {[index: string]: any} = {}, text?: string): SVGElement => {
64
37
  const node = document.createElementNS(SVGNS, tag);
65
38
  for (const key of Object.keys(attribute_map)) {
@@ -115,7 +88,6 @@ export class ChartRenderer {
115
88
  this.svg_node.style.width = '100%';
116
89
  this.svg_node.style.height = '100%';
117
90
 
118
- // this.group = document.createElementNS(SVGNS, 'g');
119
91
  this.svg_node.appendChild(this.container_group);
120
92
 
121
93
  // FIXME: validate parent is relative/absolute
@@ -676,8 +648,6 @@ export class ChartRenderer {
676
648
  titles?: string[],
677
649
  classes?: string | string[]): void {
678
650
 
679
-
680
- // const node = document.createElementNS(SVGNS, 'path');
681
651
  const group = SVGNode('g');
682
652
 
683
653
  const d1: string[] = [];
@@ -853,7 +823,7 @@ export class ChartRenderer {
853
823
  // circles...
854
824
 
855
825
  if (titles && circles.length) {
856
- const circle_group = document.createElementNS(SVGNS, 'g');
826
+ const circle_group = SVGNode('g');
857
827
  for (const circle of circles) {
858
828
 
859
829
  const shape = SVGNode('circle', {cx: circle.x, cy: circle.y, r: step});
@@ -880,8 +850,7 @@ export class ChartRenderer {
880
850
  titles?: string[],
881
851
  classes?: string | string[]) {
882
852
 
883
- // const node = document.createElementNS(SVGNS, 'path');
884
- const group = document.createElementNS(SVGNS, 'g');
853
+ const group = SVGNode('g');
885
854
 
886
855
  const d1: string[] = [];
887
856
  const d2: string[] = [];
@@ -952,18 +921,11 @@ export class ChartRenderer {
952
921
  // circles...
953
922
 
954
923
  if (titles && circles.length) {
955
- const circle_group = document.createElementNS(SVGNS, 'g');
924
+ const circle_group = SVGNode('g');
956
925
  for (const circle of circles) {
957
926
 
958
927
  const shape = SVGNode('circle', { cx: circle.x, cy: circle.y, r: step });
959
928
 
960
- /*
961
- const shape = document.createElementNS(SVGNS, 'circle');
962
- shape.setAttribute('cx', circle.x.toString());
963
- shape.setAttribute('cy', circle.y.toString());
964
- shape.setAttribute('r', (step).toString());
965
- */
966
-
967
929
  shape.addEventListener('mouseenter', (event) => {
968
930
  this.parent.setAttribute('title', titles[circle.i] || '');
969
931
  });
@@ -1255,15 +1217,6 @@ export class ChartRenderer {
1255
1217
  const d: string[] = [];
1256
1218
  const areas: string[] = [];
1257
1219
 
1258
- /*
1259
- const group = document.createElementNS(SVGNS, 'g');
1260
- if (typeof classes !== 'undefined') {
1261
- if (typeof classes === 'string') {
1262
- classes = [classes];
1263
- }
1264
- group.setAttribute('class', classes.join(' '));
1265
- }
1266
- */
1267
1220
  const group = SVGNode('g', {class: classes});
1268
1221
 
1269
1222
  // if (title) node.setAttribute('title', title);
@@ -1392,7 +1345,6 @@ export class ChartRenderer {
1392
1345
 
1393
1346
  public RenderPoints(area: Area, x: number[], y: number[], classes?: string | string[]) {
1394
1347
 
1395
- // const node = document.createElementNS(SVGNS, 'path');
1396
1348
  const d: string[] = [];
1397
1349
 
1398
1350
  for (let i = 0; i < x.length; i++) {
@@ -1693,12 +1645,6 @@ export class ChartRenderer {
1693
1645
  d.push(`M${PointOnCircle(center, inner_radius + (outer_radius - inner_radius) / 2, half_angle)}`);
1694
1646
  d.push(`L${anchor}`);
1695
1647
 
1696
- /*
1697
- const callout = document.createElementNS(SVGNS, 'path');
1698
- callout.setAttribute('d', d.join(' '));
1699
- callout.setAttribute('class', 'callout');
1700
- donut.appendChild(callout);
1701
- */
1702
1648
  donut.appendChild(SVGNode('path', { d, class: 'callout' }));
1703
1649
 
1704
1650
  const text_parts: string[] = [];
@@ -1793,7 +1739,7 @@ export class ChartRenderer {
1793
1739
 
1794
1740
  callout_label.textContent = '';
1795
1741
  for (const part of parts) {
1796
- const tspan = document.createElementNS(SVGNS, 'tspan');
1742
+ const tspan = SVGNode('tspan');
1797
1743
  tspan.textContent = part.text;
1798
1744
 
1799
1745
  const part_x = (corrected > Math.PI) ?
@@ -121,6 +121,10 @@
121
121
 
122
122
  <div class="treb-layout-resize-handle" data-conditional="resize"></div>
123
123
 
124
+ <div class="treb-revert-indicator"
125
+ data-command="revert-indicator"
126
+ title="This document has been modified from the original version."></div>
127
+
124
128
  </div> <!-- /treb-view -->
125
129
  </template>
126
130