@trebco/treb 23.6.5 → 25.0.0-rc1
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/.eslintignore +8 -0
- package/.eslintrc.js +164 -0
- package/README-shadow-DOM.md +88 -0
- package/README.md +37 -130
- package/api-config.json +29 -0
- package/api-generator/api-generator-types.ts +82 -0
- package/api-generator/api-generator.ts +1172 -0
- package/api-generator/package.json +3 -0
- package/build/treb-spreadsheet.mjs +14 -0
- package/{treb.d.ts → build/treb.d.ts} +285 -269
- package/esbuild-custom-element.mjs +336 -0
- package/esbuild.js +305 -0
- package/package.json +43 -14
- package/treb-base-types/package.json +5 -0
- package/treb-base-types/src/api_types.ts +36 -0
- package/treb-base-types/src/area.ts +583 -0
- package/treb-base-types/src/basic_types.ts +45 -0
- package/treb-base-types/src/cell.ts +612 -0
- package/treb-base-types/src/cells.ts +1066 -0
- package/treb-base-types/src/color.ts +124 -0
- package/treb-base-types/src/import.ts +71 -0
- package/treb-base-types/src/index-standalone.ts +29 -0
- package/treb-base-types/src/index.ts +42 -0
- package/treb-base-types/src/layout.ts +47 -0
- package/treb-base-types/src/localization.ts +187 -0
- package/treb-base-types/src/rectangle.ts +145 -0
- package/treb-base-types/src/render_text.ts +72 -0
- package/treb-base-types/src/style.ts +545 -0
- package/treb-base-types/src/table.ts +109 -0
- package/treb-base-types/src/text_part.ts +54 -0
- package/treb-base-types/src/theme.ts +608 -0
- package/treb-base-types/src/union.ts +152 -0
- package/treb-base-types/src/value-type.ts +164 -0
- package/treb-base-types/style/resizable.css +59 -0
- package/treb-calculator/modern.tsconfig.json +11 -0
- package/treb-calculator/package.json +5 -0
- package/treb-calculator/src/calculator.ts +2546 -0
- package/treb-calculator/src/complex-math.ts +558 -0
- package/treb-calculator/src/dag/array-vertex.ts +198 -0
- package/treb-calculator/src/dag/graph.ts +951 -0
- package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
- package/treb-calculator/src/dag/vertex.ts +352 -0
- package/treb-calculator/src/descriptors.ts +162 -0
- package/treb-calculator/src/expression-calculator.ts +1069 -0
- package/treb-calculator/src/function-error.ts +103 -0
- package/treb-calculator/src/function-library.ts +103 -0
- package/treb-calculator/src/functions/base-functions.ts +1214 -0
- package/treb-calculator/src/functions/checkbox.ts +164 -0
- package/treb-calculator/src/functions/complex-functions.ts +253 -0
- package/treb-calculator/src/functions/finance-functions.ts +399 -0
- package/treb-calculator/src/functions/information-functions.ts +102 -0
- package/treb-calculator/src/functions/matrix-functions.ts +182 -0
- package/treb-calculator/src/functions/sparkline.ts +335 -0
- package/treb-calculator/src/functions/statistics-functions.ts +350 -0
- package/treb-calculator/src/functions/text-functions.ts +298 -0
- package/treb-calculator/src/index.ts +27 -0
- package/treb-calculator/src/notifier-types.ts +59 -0
- package/treb-calculator/src/primitives.ts +428 -0
- package/treb-calculator/src/utilities.ts +305 -0
- package/treb-charts/package.json +5 -0
- package/treb-charts/src/chart-functions.ts +156 -0
- package/treb-charts/src/chart-types.ts +230 -0
- package/treb-charts/src/chart.ts +1288 -0
- package/treb-charts/src/index.ts +24 -0
- package/treb-charts/src/main.ts +37 -0
- package/treb-charts/src/rectangle.ts +52 -0
- package/treb-charts/src/renderer.ts +1841 -0
- package/treb-charts/src/util.ts +122 -0
- package/treb-charts/style/charts.scss +221 -0
- package/treb-charts/style/old-charts.scss +250 -0
- package/treb-embed/markup/layout.html +137 -0
- package/treb-embed/markup/toolbar.html +175 -0
- package/treb-embed/modern.tsconfig.json +25 -0
- package/treb-embed/src/custom-element/content-types.d.ts +18 -0
- package/treb-embed/src/custom-element/global.d.ts +11 -0
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
- package/treb-embed/src/custom-element/treb-global.ts +44 -0
- package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
- package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
- package/treb-embed/src/index.ts +16 -0
- package/treb-embed/src/language-model.ts +41 -0
- package/treb-embed/src/options.ts +320 -0
- package/treb-embed/src/progress-dialog.ts +228 -0
- package/treb-embed/src/selection-state.ts +16 -0
- package/treb-embed/src/spinner.ts +42 -0
- package/treb-embed/src/toolbar-message.ts +96 -0
- package/treb-embed/src/types.ts +167 -0
- package/treb-embed/style/autocomplete.scss +103 -0
- package/treb-embed/style/dark-theme.scss +114 -0
- package/treb-embed/style/defaults.scss +36 -0
- package/treb-embed/style/dialog.scss +181 -0
- package/treb-embed/style/dropdown-select.scss +101 -0
- package/treb-embed/style/formula-bar.scss +193 -0
- package/treb-embed/style/grid.scss +374 -0
- package/treb-embed/style/layout.scss +424 -0
- package/treb-embed/style/mouse-mask.scss +67 -0
- package/treb-embed/style/note.scss +92 -0
- package/treb-embed/style/overlay-editor.scss +102 -0
- package/treb-embed/style/spinner.scss +92 -0
- package/treb-embed/style/tab-bar.scss +228 -0
- package/treb-embed/style/table.scss +80 -0
- package/treb-embed/style/theme-defaults.scss +444 -0
- package/treb-embed/style/toolbar.scss +416 -0
- package/treb-embed/style/tooltip.scss +68 -0
- package/treb-embed/style/treb-icons.scss +130 -0
- package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
- package/treb-embed/style/z-index.scss +43 -0
- package/treb-export/docs/charts.md +68 -0
- package/treb-export/modern.tsconfig.json +19 -0
- package/treb-export/package.json +4 -0
- package/treb-export/src/address-type.ts +77 -0
- package/treb-export/src/base-template.ts +22 -0
- package/treb-export/src/column-width.ts +85 -0
- package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
- package/treb-export/src/drawing2/chart2.ts +282 -0
- package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
- package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
- package/treb-export/src/drawing2/drawing2.ts +355 -0
- package/treb-export/src/drawing2/embedded-image.ts +71 -0
- package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
- package/treb-export/src/export-worker/export-worker.ts +99 -0
- package/treb-export/src/export-worker/index-modern.ts +22 -0
- package/treb-export/src/export2.ts +2204 -0
- package/treb-export/src/import2.ts +882 -0
- package/treb-export/src/relationship.ts +36 -0
- package/treb-export/src/shared-strings2.ts +128 -0
- package/treb-export/src/template-2.ts +22 -0
- package/treb-export/src/unescape_xml.ts +47 -0
- package/treb-export/src/workbook-sheet2.ts +182 -0
- package/treb-export/src/workbook-style2.ts +1285 -0
- package/treb-export/src/workbook-theme2.ts +88 -0
- package/treb-export/src/workbook2.ts +491 -0
- package/treb-export/src/xml-utils.ts +201 -0
- package/treb-export/template/base/[Content_Types].xml +2 -0
- package/treb-export/template/base/_rels/.rels +2 -0
- package/treb-export/template/base/docProps/app.xml +2 -0
- package/treb-export/template/base/docProps/core.xml +12 -0
- package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
- package/treb-export/template/base/xl/sharedStrings.xml +2 -0
- package/treb-export/template/base/xl/styles.xml +2 -0
- package/treb-export/template/base/xl/theme/theme1.xml +2 -0
- package/treb-export/template/base/xl/workbook.xml +2 -0
- package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
- package/treb-export/template/base.xlsx +0 -0
- package/treb-format/package.json +8 -0
- package/treb-format/src/format.test.ts +213 -0
- package/treb-format/src/format.ts +942 -0
- package/treb-format/src/format_cache.ts +199 -0
- package/treb-format/src/format_parser.ts +723 -0
- package/treb-format/src/index.ts +25 -0
- package/treb-format/src/number_format_section.ts +100 -0
- package/treb-format/src/value_parser.ts +337 -0
- package/treb-grid/package.json +5 -0
- package/treb-grid/src/editors/autocomplete.ts +394 -0
- package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
- package/treb-grid/src/editors/formula_bar.ts +473 -0
- package/treb-grid/src/editors/formula_editor_base.ts +910 -0
- package/treb-grid/src/editors/overlay_editor.ts +511 -0
- package/treb-grid/src/index.ts +37 -0
- package/treb-grid/src/layout/base_layout.ts +2618 -0
- package/treb-grid/src/layout/grid_layout.ts +299 -0
- package/treb-grid/src/layout/rectangle_cache.ts +86 -0
- package/treb-grid/src/render/selection-renderer.ts +414 -0
- package/treb-grid/src/render/svg_header_overlay.ts +93 -0
- package/treb-grid/src/render/svg_selection_block.ts +187 -0
- package/treb-grid/src/render/tile_renderer.ts +2122 -0
- package/treb-grid/src/types/annotation.ts +216 -0
- package/treb-grid/src/types/border_constants.ts +34 -0
- package/treb-grid/src/types/clipboard_data.ts +31 -0
- package/treb-grid/src/types/data_model.ts +334 -0
- package/treb-grid/src/types/drag_mask.ts +81 -0
- package/treb-grid/src/types/grid.ts +7743 -0
- package/treb-grid/src/types/grid_base.ts +3644 -0
- package/treb-grid/src/types/grid_command.ts +470 -0
- package/treb-grid/src/types/grid_events.ts +124 -0
- package/treb-grid/src/types/grid_options.ts +97 -0
- package/treb-grid/src/types/grid_selection.ts +60 -0
- package/treb-grid/src/types/named_range.ts +369 -0
- package/treb-grid/src/types/scale-control.ts +202 -0
- package/treb-grid/src/types/serialize_options.ts +72 -0
- package/treb-grid/src/types/set_range_options.ts +52 -0
- package/treb-grid/src/types/sheet.ts +3099 -0
- package/treb-grid/src/types/sheet_types.ts +95 -0
- package/treb-grid/src/types/tab_bar.ts +464 -0
- package/treb-grid/src/types/tile.ts +59 -0
- package/treb-grid/src/types/update_flags.ts +75 -0
- package/treb-grid/src/util/dom_utilities.ts +44 -0
- package/treb-grid/src/util/fontmetrics2.ts +179 -0
- package/treb-grid/src/util/ua.ts +104 -0
- package/treb-logo.svg +18 -0
- package/treb-parser/package.json +5 -0
- package/treb-parser/src/csv-parser.ts +122 -0
- package/treb-parser/src/index.ts +25 -0
- package/treb-parser/src/md-parser.ts +526 -0
- package/treb-parser/src/parser-types.ts +397 -0
- package/treb-parser/src/parser.test.ts +298 -0
- package/treb-parser/src/parser.ts +2673 -0
- package/treb-utils/package.json +5 -0
- package/treb-utils/src/dispatch.ts +57 -0
- package/treb-utils/src/event_source.ts +147 -0
- package/treb-utils/src/ievent_source.ts +33 -0
- package/treb-utils/src/index.ts +31 -0
- package/treb-utils/src/measurement.ts +174 -0
- package/treb-utils/src/resizable.ts +160 -0
- package/treb-utils/src/scale.ts +137 -0
- package/treb-utils/src/serialize_html.ts +124 -0
- package/treb-utils/src/template.ts +70 -0
- package/treb-utils/src/validate_uri.ts +61 -0
- package/tsconfig.json +10 -0
- package/tsproject.json +30 -0
- package/util/license-plugin-esbuild.js +86 -0
- package/util/list-css-vars.sh +46 -0
- package/README-esm.md +0 -37
- package/treb-bundle.css +0 -2
- package/treb-bundle.mjs +0 -15
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
|
|
2
|
+
import { EmbeddedSpreadsheet } from '../embedded-spreadsheet';
|
|
3
|
+
import type { EmbeddedSpreadsheetOptions } from '../options';
|
|
4
|
+
|
|
5
|
+
import css from '../../style/treb-spreadsheet-element.scss';
|
|
6
|
+
import html from '../../markup/layout.html';
|
|
7
|
+
import toolbar_html from '../../markup/toolbar.html';
|
|
8
|
+
|
|
9
|
+
import { NumberFormatCache } from 'treb-format';
|
|
10
|
+
import { Style, Color } from 'treb-base-types';
|
|
11
|
+
import { Measurement } from 'treb-utils';
|
|
12
|
+
import type { ToolbarMessage } from '../toolbar-message';
|
|
13
|
+
|
|
14
|
+
interface ElementOptions {
|
|
15
|
+
data: Record<string, string>;
|
|
16
|
+
text: string;
|
|
17
|
+
style: string;
|
|
18
|
+
title: string;
|
|
19
|
+
classes: string|string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const Element = <T extends HTMLElement>(tag: string, parent?: HTMLElement|DocumentFragment, options: Partial<ElementOptions> = {}, attrs: Record<string, string> = {}): T => {
|
|
23
|
+
const element = document.createElement(tag) as T;
|
|
24
|
+
if (options.classes) {
|
|
25
|
+
|
|
26
|
+
// you can't use an array destructure in a ternary expression? TIL
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(options.classes)) {
|
|
29
|
+
element.classList.add(...options.classes);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
element.classList.add(options.classes);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
if (options.title) {
|
|
37
|
+
element.title = options.title;
|
|
38
|
+
}
|
|
39
|
+
if (options.text) {
|
|
40
|
+
element.textContent = options.text;
|
|
41
|
+
}
|
|
42
|
+
if (options.style) {
|
|
43
|
+
element.setAttribute('style', options.style);
|
|
44
|
+
}
|
|
45
|
+
if (options.data) {
|
|
46
|
+
for (const [key, value] of Object.entries(options.data)) {
|
|
47
|
+
element.dataset[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
52
|
+
element.setAttribute(key, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (parent) {
|
|
56
|
+
parent.appendChild(element);
|
|
57
|
+
}
|
|
58
|
+
return element;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @internal */
|
|
62
|
+
export class SpreadsheetConstructor {
|
|
63
|
+
|
|
64
|
+
/** container, if any */
|
|
65
|
+
public root?: HTMLElement;
|
|
66
|
+
|
|
67
|
+
/** spreadsheet instance */
|
|
68
|
+
public sheet?: EmbeddedSpreadsheet
|
|
69
|
+
|
|
70
|
+
/** current border color. will be applied to new borders. */
|
|
71
|
+
protected border_color?: Style.Color;
|
|
72
|
+
|
|
73
|
+
/** color bar elements, since we update them frequently */
|
|
74
|
+
protected color_bar_elements: Record<string, HTMLElement> = {};
|
|
75
|
+
|
|
76
|
+
/** some menu buttons change icons from time to time */
|
|
77
|
+
protected replace_targets: Record<string, HTMLElement> = {};
|
|
78
|
+
|
|
79
|
+
/** root layout element */
|
|
80
|
+
protected layout_element?: HTMLElement;
|
|
81
|
+
|
|
82
|
+
/** cached controls */
|
|
83
|
+
protected toolbar_controls: Record<string, HTMLElement> = {};
|
|
84
|
+
|
|
85
|
+
/** swatch lists in color chooser */
|
|
86
|
+
protected swatch_lists: {
|
|
87
|
+
theme?: HTMLDivElement,
|
|
88
|
+
other?: HTMLDivElement,
|
|
89
|
+
} = {};
|
|
90
|
+
|
|
91
|
+
//
|
|
92
|
+
|
|
93
|
+
constructor(root?: HTMLElement|string) {
|
|
94
|
+
|
|
95
|
+
if (typeof root === 'string') {
|
|
96
|
+
root = document.querySelector(root) as HTMLElement;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// there's a possibility this could be running in a node environment.
|
|
100
|
+
// in that case (wihtout a shim) HTMLElement will not exist, so we can't
|
|
101
|
+
// check type.
|
|
102
|
+
|
|
103
|
+
if (typeof HTMLElement !== 'undefined' && root instanceof HTMLElement) {
|
|
104
|
+
this.root = root;
|
|
105
|
+
|
|
106
|
+
const style_node = document.head.querySelector('style[treb-stylesheet]');
|
|
107
|
+
if (!style_node) {
|
|
108
|
+
const style = document.createElement('style');
|
|
109
|
+
style.setAttribute('treb-stylesheet', '');
|
|
110
|
+
style.textContent = css;
|
|
111
|
+
document.head.prepend(style);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
if (!SpreadsheetConstructor.stylesheets_attached) {
|
|
118
|
+
const style = document.createElement('style');
|
|
119
|
+
style.textContent = css;
|
|
120
|
+
document.head.prepend(style);
|
|
121
|
+
SpreadsheetConstructor.stylesheets_attached = true;
|
|
122
|
+
}
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* coerce an attribute value into a more useful type. for attributes,
|
|
131
|
+
* having no value implies "true". false should be explicitly set as
|
|
132
|
+
* "false"; we don't, atm, support falsy values like '0' (that would be
|
|
133
|
+
* coerced to a number).
|
|
134
|
+
*/
|
|
135
|
+
public CoerceAttributeValue(value: string|null): number|boolean|string {
|
|
136
|
+
|
|
137
|
+
if (value === null || value.toString().toLowerCase() === 'true' || value === '') {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
else if (value.toLowerCase() === 'false') {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const test = Number(value);
|
|
145
|
+
if (!isNaN(test)) {
|
|
146
|
+
return test;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// default to string, if it was null default to empty string (no nulls)
|
|
151
|
+
return value || '';
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* get options from node attributes. we're still working on final
|
|
157
|
+
* semantics but at the moment we'll translate hyphen-separated-options
|
|
158
|
+
* to our standard snake_case_options.
|
|
159
|
+
*
|
|
160
|
+
* we also support the old-style data-options
|
|
161
|
+
*
|
|
162
|
+
* @returns
|
|
163
|
+
*/
|
|
164
|
+
public ParseOptionAttributes(): Partial<EmbeddedSpreadsheetOptions> {
|
|
165
|
+
|
|
166
|
+
const attribute_options: Record<string, string|boolean|number> = {};
|
|
167
|
+
|
|
168
|
+
if (this.root) {
|
|
169
|
+
|
|
170
|
+
const names = this.root.getAttributeNames();
|
|
171
|
+
|
|
172
|
+
for (let name of names) {
|
|
173
|
+
|
|
174
|
+
switch (name) {
|
|
175
|
+
|
|
176
|
+
// skip
|
|
177
|
+
case 'class':
|
|
178
|
+
case 'style':
|
|
179
|
+
case 'id':
|
|
180
|
+
continue;
|
|
181
|
+
|
|
182
|
+
// old-style options (in two flavors). old-style options are
|
|
183
|
+
// comma-delimited an in the form `key=value`, or just `key`
|
|
184
|
+
// for boolean true.
|
|
185
|
+
|
|
186
|
+
case 'data-options':
|
|
187
|
+
case 'options':
|
|
188
|
+
{
|
|
189
|
+
// in this case use the original name, which should
|
|
190
|
+
// be in snake_case (for backcompat)
|
|
191
|
+
|
|
192
|
+
const value = this.root.getAttribute(name) || '';
|
|
193
|
+
const elements = value.split(',');
|
|
194
|
+
// console.info(elements);
|
|
195
|
+
|
|
196
|
+
for (const element of elements) {
|
|
197
|
+
const parts = element.split(/=/);
|
|
198
|
+
if (parts.length === 1) {
|
|
199
|
+
attribute_options[parts[0]] = true;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
attribute_options[parts[0]] = this.CoerceAttributeValue(parts[1]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
|
|
209
|
+
// old style (not handling though)
|
|
210
|
+
case 'data-treb':
|
|
211
|
+
continue;
|
|
212
|
+
|
|
213
|
+
// special case
|
|
214
|
+
case 'document':
|
|
215
|
+
continue;
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// attrtibute options are in kebab-case while our internal
|
|
220
|
+
// options are still in snake_case.
|
|
221
|
+
|
|
222
|
+
name = name.replace(/-/g, '_');
|
|
223
|
+
attribute_options[name] = this.CoerceAttributeValue(this.root.getAttribute(name));
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
...attribute_options
|
|
230
|
+
} as Partial<EmbeddedSpreadsheetOptions>;
|
|
231
|
+
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* attach content to element. for custom elements, this is called via
|
|
236
|
+
* the connectedCallback call. for elements created with the API, we
|
|
237
|
+
* call it immediately.
|
|
238
|
+
*/
|
|
239
|
+
public AttachElement(options: EmbeddedSpreadsheetOptions = {}) {
|
|
240
|
+
|
|
241
|
+
options = {
|
|
242
|
+
...this.ParseOptionAttributes(),
|
|
243
|
+
...options,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (this.root) {
|
|
247
|
+
|
|
248
|
+
// set a default size if the node does not have width or height.
|
|
249
|
+
// we do this with a class, so it's easier to override if desired.
|
|
250
|
+
// could we use vars? (...)
|
|
251
|
+
|
|
252
|
+
if (!options.headless) {
|
|
253
|
+
const rect = this.root.getBoundingClientRect();
|
|
254
|
+
if (!rect.width || !rect.height) {
|
|
255
|
+
this.root.classList.add('treb-default-size');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.root.innerHTML = html;
|
|
260
|
+
options.container = this.root.querySelector('.treb-layout-spreadsheet') as HTMLElement;
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// set a local variable so we don't have to keep testing the member
|
|
265
|
+
|
|
266
|
+
const sheet = new EmbeddedSpreadsheet(options);
|
|
267
|
+
|
|
268
|
+
// console.info(sheet.options);
|
|
269
|
+
|
|
270
|
+
this.sheet = sheet;
|
|
271
|
+
|
|
272
|
+
if (!this.root) {
|
|
273
|
+
return; // the rest is UI setup
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- not headless (headful?) ---------------------------------------------
|
|
277
|
+
|
|
278
|
+
const root = this.root; // for async/callback functions
|
|
279
|
+
|
|
280
|
+
// call our internal resize method when the node is resized
|
|
281
|
+
// (primary instance will handle views)
|
|
282
|
+
|
|
283
|
+
// why are we doing this here? ... because this is layout? dunno
|
|
284
|
+
|
|
285
|
+
const resizeObserver = new ResizeObserver(() => sheet.Resize());
|
|
286
|
+
resizeObserver.observe(root);
|
|
287
|
+
|
|
288
|
+
// const resizeObserver = new ResizeObserver(() => sheet.Resize());
|
|
289
|
+
// resizeObserver.observe(root);
|
|
290
|
+
|
|
291
|
+
// handle sidebar collapse
|
|
292
|
+
|
|
293
|
+
this.layout_element = root.querySelector('.treb-layout') as HTMLElement;
|
|
294
|
+
const button = root.querySelector('.treb-toggle-sidebar-button');
|
|
295
|
+
|
|
296
|
+
if (button && this.layout_element) {
|
|
297
|
+
const element = this.layout_element;
|
|
298
|
+
button.addEventListener('click', () => {
|
|
299
|
+
|
|
300
|
+
// attribute is set if it has a value and that value is either
|
|
301
|
+
// empty or "true"; we don't accept any other values, because
|
|
302
|
+
// that just makes extra work.
|
|
303
|
+
|
|
304
|
+
const value = element.getAttribute('collapsed');
|
|
305
|
+
const state = (typeof value === 'string' && (value === '' || value === 'true'));
|
|
306
|
+
|
|
307
|
+
// toggle
|
|
308
|
+
|
|
309
|
+
if (state) {
|
|
310
|
+
element.removeAttribute('collapsed');
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
element.setAttribute('collapsed', '');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- set initial state before enabling transitions -----------------------
|
|
320
|
+
|
|
321
|
+
if (sheet.options.toolbar === 'show' || sheet.options.toolbar === 'show-narrow') {
|
|
322
|
+
this.layout_element?.setAttribute('toolbar', '');
|
|
323
|
+
}
|
|
324
|
+
if (sheet.options.collapsed) {
|
|
325
|
+
this.layout_element?.setAttribute('collapsed', '');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// --- toolbar/sidebar -----------------------------------------------------
|
|
329
|
+
|
|
330
|
+
const sidebar = root.querySelector('.treb-layout-sidebar');
|
|
331
|
+
sidebar?.addEventListener('click', event => {
|
|
332
|
+
const target = event.target as HTMLElement;
|
|
333
|
+
if (target.dataset.command) {
|
|
334
|
+
switch (target.dataset.command) {
|
|
335
|
+
|
|
336
|
+
case 'toggle-toolbar':
|
|
337
|
+
this.ToggleToolbar();
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
default:
|
|
341
|
+
sheet.HandleToolbarMessage({
|
|
342
|
+
command: target.dataset.command,
|
|
343
|
+
} as ToolbarMessage);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (sheet.options.toolbar) {
|
|
350
|
+
this.AttachToolbar(sheet, root);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- hide/remove ---------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
// compare conditional items against options. not sure which way we're
|
|
356
|
+
// ultimately going to land with the option names. for the time being
|
|
357
|
+
// I'm going to do this the verbose way.
|
|
358
|
+
|
|
359
|
+
const conditional_map: Record<string, boolean> = {
|
|
360
|
+
// 'file-menu': !!sheet.options.file_menu,
|
|
361
|
+
'table-button': !!sheet.options.table_button,
|
|
362
|
+
// 'chart-menu': !!sheet.options.chart_menu,
|
|
363
|
+
// 'font-scale': !!sheet.options.font_scale,
|
|
364
|
+
'revert': !!sheet.options.revert_button,
|
|
365
|
+
'toolbar': !!sheet.options.toolbar,
|
|
366
|
+
'export': !!sheet.options.export,
|
|
367
|
+
|
|
368
|
+
// the following won't work as expected in split, because this
|
|
369
|
+
// code won't be run when the new view is created -- do something
|
|
370
|
+
// else
|
|
371
|
+
|
|
372
|
+
// resize should actually work because we're hiding new view
|
|
373
|
+
// resize handles via positioning
|
|
374
|
+
|
|
375
|
+
'resize': !!sheet.options.resizable,
|
|
376
|
+
|
|
377
|
+
// add-tab and delete-tab will still work for the menu
|
|
378
|
+
|
|
379
|
+
'add-tab': !!sheet.options.add_tab,
|
|
380
|
+
'delete-tab': !!sheet.options.delete_tab,
|
|
381
|
+
|
|
382
|
+
// we actually don't want to remove stats if it's not in use, because
|
|
383
|
+
// we need it for layout
|
|
384
|
+
// 'stats': !!sheet.options.stats,
|
|
385
|
+
|
|
386
|
+
// scale control is not (yet) declarative, so this isn't effective anyway
|
|
387
|
+
// 'scale-control': !!sheet.options.scale_control,
|
|
388
|
+
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const [key, value] of Object.entries(conditional_map)) {
|
|
392
|
+
if (!value) {
|
|
393
|
+
const elements = this.layout_element.querySelectorAll(`[data-conditional=${key}]`) as NodeListOf<HTMLElement>;
|
|
394
|
+
for (const element of Array.from(elements)) {
|
|
395
|
+
element.style.display = 'none';
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- resize --------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
if (sheet.options.resizable) {
|
|
403
|
+
|
|
404
|
+
const size = { width: 0, height: 0 };
|
|
405
|
+
const position = { x: 0, y: 0 };
|
|
406
|
+
const delta = { x: 0, y: 0 };
|
|
407
|
+
|
|
408
|
+
// const resize_container = root.querySelector('.treb-layout-resize-container');
|
|
409
|
+
const views = root.querySelector('.treb-views');
|
|
410
|
+
|
|
411
|
+
let mask: HTMLElement|undefined;
|
|
412
|
+
let resizer: HTMLElement|undefined;
|
|
413
|
+
|
|
414
|
+
const resize_handle = this.root.querySelector('.treb-layout-resize-handle') as HTMLElement;
|
|
415
|
+
|
|
416
|
+
// mouse up handler added to mask (when created)
|
|
417
|
+
const mouse_up = () => finish();
|
|
418
|
+
|
|
419
|
+
// mouse move handler added to mask (when created)
|
|
420
|
+
const mouse_move = ((event: MouseEvent) => {
|
|
421
|
+
if (event.buttons === 0) {
|
|
422
|
+
finish();
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
delta.x = event.screenX - position.x;
|
|
426
|
+
delta.y = event.screenY - position.y;
|
|
427
|
+
if (resizer) {
|
|
428
|
+
resizer.style.width = (size.width + delta.x) + 'px';
|
|
429
|
+
resizer.style.height = (size.height + delta.y) + 'px';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// clean up mask and layout rectangle
|
|
435
|
+
const finish = () => {
|
|
436
|
+
|
|
437
|
+
// resize_handle.classList.remove('retain-opacity'); // we're not using this anymore
|
|
438
|
+
|
|
439
|
+
if (delta.x || delta.y) {
|
|
440
|
+
const rect = root.getBoundingClientRect();
|
|
441
|
+
root.style.width = (rect.width + delta.x) + 'px';
|
|
442
|
+
root.style.height = (rect.height + delta.y) + 'px';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (mask) {
|
|
446
|
+
mask.removeEventListener('mouseup', mouse_up);
|
|
447
|
+
mask.removeEventListener('mousemove', mouse_move);
|
|
448
|
+
mask.parentElement?.removeChild(mask);
|
|
449
|
+
mask = undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
resizer?.parentElement?.removeChild(resizer);
|
|
453
|
+
resizer = undefined;
|
|
454
|
+
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
resize_handle.addEventListener('mousedown', (event: MouseEvent) => {
|
|
458
|
+
|
|
459
|
+
event.stopPropagation();
|
|
460
|
+
event.preventDefault();
|
|
461
|
+
|
|
462
|
+
resizer = Element<HTMLDivElement>('div', document.body, { classes: 'treb-resize-rect' });
|
|
463
|
+
|
|
464
|
+
mask = Element<HTMLDivElement>('div', document.body, {
|
|
465
|
+
classes: 'treb-resize-mask',
|
|
466
|
+
style: 'cursor: nw-resize;',
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
mask.addEventListener('mouseup', mouse_up);
|
|
470
|
+
mask.addEventListener('mousemove', mouse_move);
|
|
471
|
+
|
|
472
|
+
// resize_handle.classList.add('retain-opacity'); // we're not using this anymore
|
|
473
|
+
|
|
474
|
+
position.x = event.screenX;
|
|
475
|
+
position.y = event.screenY;
|
|
476
|
+
|
|
477
|
+
delta.x = 0;
|
|
478
|
+
delta.y = 0;
|
|
479
|
+
|
|
480
|
+
const layouts = views?.querySelectorAll('.treb-spreadsheet-body');
|
|
481
|
+
const rects = Array.from(layouts||[]).map(element => element.getBoundingClientRect());
|
|
482
|
+
if (rects.length) {
|
|
483
|
+
|
|
484
|
+
const composite: { top: number, left: number, right: number, bottom: number } =
|
|
485
|
+
JSON.parse(JSON.stringify(rects.shift()));
|
|
486
|
+
|
|
487
|
+
for (const rect of rects) {
|
|
488
|
+
composite.top = Math.min(rect.top, composite.top);
|
|
489
|
+
composite.left = Math.min(rect.left, composite.left);
|
|
490
|
+
composite.right = Math.max(rect.right, composite.right);
|
|
491
|
+
composite.bottom = Math.max(rect.bottom, composite.bottom);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const width = composite.right - composite.left;
|
|
495
|
+
const height = composite.bottom - composite.top;
|
|
496
|
+
|
|
497
|
+
resizer.style.top = (composite.top) + 'px';
|
|
498
|
+
resizer.style.left = (composite.left) + 'px';
|
|
499
|
+
|
|
500
|
+
resizer.style.width = (width) + 'px';
|
|
501
|
+
resizer.style.height = (height) + 'px';
|
|
502
|
+
|
|
503
|
+
size.width = width;
|
|
504
|
+
size.height = height;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// --- animated ------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
// requestAnimationFrame(() => {
|
|
514
|
+
setTimeout(() => this.layout_element?.setAttribute('animate', ''), 250);
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
public ToggleToolbar() {
|
|
519
|
+
|
|
520
|
+
if (this.layout_element) {
|
|
521
|
+
const value = this.layout_element.getAttribute('toolbar');
|
|
522
|
+
const state = (typeof value === 'string' && (value === '' || value === 'true'));
|
|
523
|
+
|
|
524
|
+
if (state) {
|
|
525
|
+
this.layout_element.removeAttribute('toolbar');
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
this.layout_element.setAttribute('toolbar', '');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
public UpdateSelectionStyle(sheet: EmbeddedSpreadsheet, toolbar: HTMLElement, comment_box: HTMLTextAreaElement) {
|
|
535
|
+
|
|
536
|
+
const state = sheet.selection_state;
|
|
537
|
+
|
|
538
|
+
// unset all
|
|
539
|
+
|
|
540
|
+
comment_box.value = '';
|
|
541
|
+
|
|
542
|
+
for (const [key, value] of Object.entries(this.toolbar_controls)) {
|
|
543
|
+
if (value) {
|
|
544
|
+
// value.classList.remove('treb-active');
|
|
545
|
+
value.removeAttribute('active');
|
|
546
|
+
if (value.dataset.title) {
|
|
547
|
+
value.title = value.dataset.title;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const Activate = (element?: HTMLElement) => {
|
|
553
|
+
if (element) {
|
|
554
|
+
// element.classList.add('treb-active');
|
|
555
|
+
element.setAttribute('active', '');
|
|
556
|
+
if (element.dataset.activeTitle) {
|
|
557
|
+
element.title = element.dataset.activeTitle;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
if (state.comment) {
|
|
563
|
+
Activate(this.toolbar_controls.comment);
|
|
564
|
+
comment_box.value = state.comment;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (state.style?.locked) {
|
|
568
|
+
Activate(this.toolbar_controls.locked);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (state.frozen) {
|
|
572
|
+
Activate(this.toolbar_controls.freeze);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (state.style?.wrap) {
|
|
576
|
+
Activate(this.toolbar_controls.wrap);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (this.toolbar_controls.table) {
|
|
580
|
+
if (state.table) {
|
|
581
|
+
Activate(this.toolbar_controls.table);
|
|
582
|
+
this.toolbar_controls.table.dataset.command = 'remove-table';
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
this.toolbar_controls.table.dataset.command = 'insert-table';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (this.toolbar_controls.merge) {
|
|
590
|
+
if (state.merge) {
|
|
591
|
+
Activate(this.toolbar_controls.merge);
|
|
592
|
+
this.toolbar_controls.merge.dataset.command = 'unmerge-cells';
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
this.toolbar_controls.merge.dataset.command = 'merge-cells';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const format = this.toolbar_controls.format as HTMLInputElement;
|
|
600
|
+
if (format) {
|
|
601
|
+
if (state.style?.number_format) {
|
|
602
|
+
format.value = NumberFormatCache.SymbolicName(state.style.number_format) || state.style.number_format;
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
format.value = 'General';
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const scale = this.toolbar_controls.scale as HTMLInputElement;
|
|
610
|
+
if (scale) {
|
|
611
|
+
scale.value = sheet.FormatNumber(state.relative_font_size || 1, '0.00');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
switch (state.style?.horizontal_align) {
|
|
615
|
+
case Style.HorizontalAlign.Left:
|
|
616
|
+
Activate(this.toolbar_controls.left);
|
|
617
|
+
break;
|
|
618
|
+
case Style.HorizontalAlign.Center:
|
|
619
|
+
Activate(this.toolbar_controls.center);
|
|
620
|
+
break;
|
|
621
|
+
case Style.HorizontalAlign.Right:
|
|
622
|
+
Activate(this.toolbar_controls.right);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
switch (state.style?.vertical_align) {
|
|
627
|
+
case Style.VerticalAlign.Top:
|
|
628
|
+
Activate(this.toolbar_controls.top);
|
|
629
|
+
break;
|
|
630
|
+
case Style.VerticalAlign.Middle:
|
|
631
|
+
Activate(this.toolbar_controls.middle);
|
|
632
|
+
break;
|
|
633
|
+
case Style.VerticalAlign.Bottom:
|
|
634
|
+
Activate(this.toolbar_controls.bottom);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
public UpdateDocumentStyles(sheet: EmbeddedSpreadsheet, format_menu: HTMLElement) {
|
|
642
|
+
|
|
643
|
+
// --- colors -------------------------------------------------------------
|
|
644
|
+
|
|
645
|
+
{
|
|
646
|
+
|
|
647
|
+
let fragment = document.createDocumentFragment();
|
|
648
|
+
|
|
649
|
+
const length = sheet.document_styles.theme_colors.length;
|
|
650
|
+
const themes = ['Background', 'Text', 'Background', 'Text', 'Accent'];
|
|
651
|
+
|
|
652
|
+
if (length) {
|
|
653
|
+
const depth = sheet.document_styles.theme_colors[0].length;
|
|
654
|
+
|
|
655
|
+
for (let i = 0; i < depth; i++) {
|
|
656
|
+
for (let j = 0; j < length; j++) {
|
|
657
|
+
const entry = sheet.document_styles.theme_colors[j][i];
|
|
658
|
+
const style = `background: ${entry.resolved};`;
|
|
659
|
+
let title = themes[j] || themes[4];
|
|
660
|
+
if (entry.color.tint) {
|
|
661
|
+
// title += ` (${Math.abs(entry.color.tint) * 100}% ${ entry.color.tint > 0 ? 'lighter' : 'darker'})`;
|
|
662
|
+
title += ` (${(entry.color.tint > 0 ? '+' : '') + (entry.color.tint) * 100}%)`;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
|
|
666
|
+
// set theme default colors
|
|
667
|
+
|
|
668
|
+
if (j === 0) {
|
|
669
|
+
this.color_bar_elements.fill?.style.setProperty('--treb-default-color', entry.resolved);
|
|
670
|
+
}
|
|
671
|
+
else if (j === 1) {
|
|
672
|
+
this.color_bar_elements.text?.style.setProperty('--treb-default-color', entry.resolved);
|
|
673
|
+
this.color_bar_elements.border?.style.setProperty('--treb-default-color', entry.resolved);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
}
|
|
677
|
+
Element<HTMLButtonElement>('button', fragment, { style, title, data: { command: 'set-color', color: JSON.stringify(entry.color) } });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
this.swatch_lists.theme?.replaceChildren(fragment);
|
|
684
|
+
|
|
685
|
+
fragment = document.createDocumentFragment();
|
|
686
|
+
Element<HTMLButtonElement>('button', fragment, {
|
|
687
|
+
classes: 'treb-default-color',
|
|
688
|
+
title: 'Default color',
|
|
689
|
+
data: { command: 'set-color', color: JSON.stringify({}) },
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const colors = ['Black', 'White', 'Gray', 'Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Violet'];
|
|
693
|
+
|
|
694
|
+
const lc = colors.map(color => color.toLowerCase());
|
|
695
|
+
const additional_colors = sheet.document_styles.colors.filter(test => {
|
|
696
|
+
return !lc.includes(test.toLowerCase());
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
for (const text of [...colors, ...additional_colors]) {
|
|
700
|
+
const style = `background: ${text.toLowerCase()};`;
|
|
701
|
+
Element<HTMLButtonElement>('button', fragment, { style, title: text, data: { command: 'set-color', color: JSON.stringify({text: text.toLowerCase()})}});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
this.swatch_lists.other?.replaceChildren(fragment);
|
|
705
|
+
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --- number formats -----------------------------------------------------
|
|
709
|
+
|
|
710
|
+
const number_formats: string[] = [
|
|
711
|
+
'General', 'Number', 'Integer', 'Percent', 'Fraction', 'Accounting', 'Currency', 'Scientific',
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
const date_formats: string[] = [
|
|
715
|
+
'Timestamp', 'Long Date', 'Short Date',
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
for (const format of sheet.document_styles.number_formats) {
|
|
719
|
+
if (NumberFormatCache.SymbolicName(NumberFormatCache.Translate(format))) { continue; }
|
|
720
|
+
const instance = NumberFormatCache.Get(format);
|
|
721
|
+
if (instance.date_format) {
|
|
722
|
+
date_formats.push(format);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
number_formats.push(format);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const Button = (format: string) => {
|
|
730
|
+
return Element<HTMLButtonElement>('button', undefined, {
|
|
731
|
+
text: format, data: { format, command: 'number-format' },
|
|
732
|
+
});
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const fragment = document.createDocumentFragment();
|
|
736
|
+
fragment.append(...number_formats.map(format => Button(format)));
|
|
737
|
+
|
|
738
|
+
fragment.append(Element<HTMLDivElement>('div', undefined, {}, {separator: ''}));
|
|
739
|
+
fragment.append(...date_formats.map(format => Button(format)));
|
|
740
|
+
|
|
741
|
+
format_menu.textContent = '';
|
|
742
|
+
format_menu.append(fragment);
|
|
743
|
+
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* replace a given template with its contents.
|
|
748
|
+
*/
|
|
749
|
+
public ReplaceTemplate(root: HTMLElement, selector: string, remove = true) {
|
|
750
|
+
const template = root.querySelector(selector) as HTMLTemplateElement;
|
|
751
|
+
if (template && template.parentElement) {
|
|
752
|
+
// console.info(template, template.parentElement);
|
|
753
|
+
for (const child of Array.from(template.content.children)) {
|
|
754
|
+
template.parentElement.insertBefore(child, template);
|
|
755
|
+
}
|
|
756
|
+
if (remove) {
|
|
757
|
+
template.parentElement.removeChild(template);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
console.warn('template not found', selector);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
public AttachToolbar(sheet: EmbeddedSpreadsheet, root: HTMLElement) {
|
|
766
|
+
|
|
767
|
+
// --- layout --------------------------------------------------------------
|
|
768
|
+
|
|
769
|
+
const scroller = root.querySelector('.treb-layout-header') as HTMLElement;
|
|
770
|
+
const toolbar = root.querySelector('.treb-toolbar') as HTMLElement;
|
|
771
|
+
|
|
772
|
+
toolbar.innerHTML = toolbar_html;
|
|
773
|
+
|
|
774
|
+
// adjust toolbar based on options
|
|
775
|
+
|
|
776
|
+
const remove: Array<Element|null> = [];
|
|
777
|
+
|
|
778
|
+
// wide or narrow menu
|
|
779
|
+
if (sheet.options.toolbar === 'narrow' || sheet.options.toolbar === 'show-narrow') {
|
|
780
|
+
remove.push(...Array.from(toolbar.querySelectorAll('[wide]')));
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
remove.push(...Array.from(toolbar.querySelectorAll('[narrow]')));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// optional toolbar items
|
|
787
|
+
if (!sheet.options.file_menu) {
|
|
788
|
+
remove.push(toolbar.querySelector('[file-menu]'));
|
|
789
|
+
}
|
|
790
|
+
if (!sheet.options.font_scale) {
|
|
791
|
+
remove.push(toolbar.querySelector('[font-scale]'));
|
|
792
|
+
}
|
|
793
|
+
if (!sheet.options.chart_menu) {
|
|
794
|
+
remove.push(toolbar.querySelector('[chart-menu]'));
|
|
795
|
+
}
|
|
796
|
+
if (!sheet.options.freeze_button) {
|
|
797
|
+
remove.push(toolbar.querySelector('[freeze-button]'));
|
|
798
|
+
}
|
|
799
|
+
if (!sheet.options.table_button) {
|
|
800
|
+
remove.push(toolbar.querySelector('[table-button]'));
|
|
801
|
+
}
|
|
802
|
+
if (!sheet.options.add_tab && !sheet.options.delete_tab) {
|
|
803
|
+
remove.push(...Array.from(toolbar.querySelectorAll('[add-remove-sheet]')));
|
|
804
|
+
}
|
|
805
|
+
if (!sheet.options.toolbar_recalculate_button) {
|
|
806
|
+
remove.push(toolbar.querySelector('[recalculate-button]'));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
for (const element of remove) {
|
|
810
|
+
if (element) {
|
|
811
|
+
element.parentElement?.removeChild(element);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const color_chooser = toolbar.querySelector('.treb-color-chooser') as HTMLElement;
|
|
816
|
+
const comment_box = toolbar.querySelector('.treb-comment-box textarea') as HTMLTextAreaElement;
|
|
817
|
+
|
|
818
|
+
// --- controls ------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
for (const [key, value] of Object.entries({
|
|
821
|
+
|
|
822
|
+
// for align/justify make sure we are collecting the wide
|
|
823
|
+
// versions. narrow versions don't highlight.
|
|
824
|
+
|
|
825
|
+
'top': '[wide] [data-command=align-top]',
|
|
826
|
+
'middle': '[wide] [data-command=align-middle]',
|
|
827
|
+
'bottom': '[wide] [data-command=align-bottom]',
|
|
828
|
+
|
|
829
|
+
'left': '[wide] [data-command=justify-left]',
|
|
830
|
+
'right': '[wide] [data-command=justify-right]',
|
|
831
|
+
'center': '[wide] [data-command=justify-center]',
|
|
832
|
+
|
|
833
|
+
'wrap': '[data-command=wrap-text]',
|
|
834
|
+
'merge': '[data-id=merge]',
|
|
835
|
+
'comment': '[data-icon=comment]',
|
|
836
|
+
'locked': '[data-command=lock-cells]',
|
|
837
|
+
'freeze': '[data-command=freeze-panes]',
|
|
838
|
+
'table': '[data-icon=table]',
|
|
839
|
+
|
|
840
|
+
'format': 'input.treb-number-format',
|
|
841
|
+
'scale': 'input.treb-font-scale',
|
|
842
|
+
|
|
843
|
+
})) {
|
|
844
|
+
|
|
845
|
+
const element = toolbar.querySelector(value) as HTMLElement;
|
|
846
|
+
if (element) {
|
|
847
|
+
this.toolbar_controls[key] = element;
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
// console.warn('missing toolbar element', value);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const swatch_lists = color_chooser.querySelectorAll('.treb-swatches');
|
|
856
|
+
this.swatch_lists = {
|
|
857
|
+
theme: swatch_lists[0] as HTMLDivElement,
|
|
858
|
+
other: swatch_lists[1] as HTMLDivElement,
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
let button = root.querySelector('[data-command=increase-precision') as HTMLElement;
|
|
862
|
+
if (button) {
|
|
863
|
+
button.textContent = this.sheet?.FormatNumber(0, '0.00') || '';
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
button = root.querySelector('[data-command=decrease-precision') as HTMLElement;
|
|
867
|
+
if (button) {
|
|
868
|
+
button.textContent = this.sheet?.FormatNumber(0, '0.0') || '';
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
button = toolbar.querySelector('[data-command=update-comment]') as HTMLButtonElement;
|
|
872
|
+
comment_box.addEventListener('keydown', event => {
|
|
873
|
+
if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) {
|
|
874
|
+
button.click();
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// why are we not just getting all? (...)
|
|
879
|
+
|
|
880
|
+
for (const entry of ['border', 'annotation', 'align', 'justify']) {
|
|
881
|
+
this.replace_targets[entry] = toolbar.querySelector(`[data-target=${entry}`) as HTMLElement;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
for (const entry of ['fill', 'text', 'border']) {
|
|
885
|
+
this.color_bar_elements[entry] = toolbar.querySelector(`[data-color-bar=${entry}]`) as HTMLElement;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
//
|
|
889
|
+
// unified click handler for toolbar controls
|
|
890
|
+
//
|
|
891
|
+
toolbar.addEventListener('click', event => {
|
|
892
|
+
|
|
893
|
+
const target = event.target as HTMLElement;
|
|
894
|
+
|
|
895
|
+
// the toolbar message used to take "data" for historical
|
|
896
|
+
// reasdons, now it takes inline properties. we can be a little
|
|
897
|
+
// more precise about this, although we'll have to update if
|
|
898
|
+
// we add any new data types.
|
|
899
|
+
|
|
900
|
+
const props: {
|
|
901
|
+
comment?: string;
|
|
902
|
+
color?: Style.Color;
|
|
903
|
+
format?: string;
|
|
904
|
+
scale?: string;
|
|
905
|
+
} = {
|
|
906
|
+
format: target.dataset.format,
|
|
907
|
+
scale: target.dataset.scale,
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
let command = target?.dataset.command;
|
|
911
|
+
// console.info(command);
|
|
912
|
+
|
|
913
|
+
if (command) {
|
|
914
|
+
|
|
915
|
+
// we may need to replace an icon in the toolbar
|
|
916
|
+
const replace = (target.parentElement as HTMLElement)?.dataset.replace;
|
|
917
|
+
if (replace) {
|
|
918
|
+
const replace_target = this.replace_targets[replace];
|
|
919
|
+
if (replace_target) {
|
|
920
|
+
replace_target.dataset.command = command;
|
|
921
|
+
replace_target.title = target.title || '';
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// for borders, if we have a cached border color add that to the event data
|
|
926
|
+
if (/^border-/.test(command)) {
|
|
927
|
+
props.color = this.border_color || {};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
switch (command) {
|
|
931
|
+
case 'text-color':
|
|
932
|
+
case 'fill-color':
|
|
933
|
+
props.color = {};
|
|
934
|
+
try {
|
|
935
|
+
props.color = JSON.parse(target.dataset.color || '{}');
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
console.error(err);
|
|
939
|
+
}
|
|
940
|
+
break;
|
|
941
|
+
|
|
942
|
+
case 'set-color':
|
|
943
|
+
|
|
944
|
+
// swap command
|
|
945
|
+
command = color_chooser.dataset.colorCommand || '';
|
|
946
|
+
|
|
947
|
+
// convert string to color
|
|
948
|
+
props.color = {};
|
|
949
|
+
try {
|
|
950
|
+
props.color = JSON.parse(target.dataset.color || '{}');
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
console.error(err);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// cache for later
|
|
957
|
+
if (command === 'border-color') {
|
|
958
|
+
this.border_color = props.color;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// update color bar
|
|
962
|
+
if (color_chooser.dataset.target) {
|
|
963
|
+
const replace = this.color_bar_elements[color_chooser.dataset.target];
|
|
964
|
+
if (replace) {
|
|
965
|
+
replace.style.setProperty('--treb-color-bar-color', target.style.backgroundColor);
|
|
966
|
+
replace.dataset.color = target.dataset.color || '{}';
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
break;
|
|
971
|
+
|
|
972
|
+
case 'update-comment':
|
|
973
|
+
props.comment = comment_box.value;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
sheet.HandleToolbarMessage({
|
|
978
|
+
command,
|
|
979
|
+
...props,
|
|
980
|
+
} as ToolbarMessage);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// common
|
|
986
|
+
|
|
987
|
+
const CreateInputHandler = (selector: string, handler: (value: string) => boolean) => {
|
|
988
|
+
const input = toolbar.querySelector(selector) as HTMLInputElement;
|
|
989
|
+
if (input) {
|
|
990
|
+
let cached_value = '';
|
|
991
|
+
input.addEventListener('focusin', () => cached_value = input.value);
|
|
992
|
+
input.addEventListener('keydown', event => {
|
|
993
|
+
switch (event.key) {
|
|
994
|
+
case 'Escape':
|
|
995
|
+
input.value = cached_value;
|
|
996
|
+
sheet.Focus();
|
|
997
|
+
break;
|
|
998
|
+
|
|
999
|
+
case 'Enter':
|
|
1000
|
+
if (!handler(input.value)) {
|
|
1001
|
+
input.value = cached_value;
|
|
1002
|
+
sheet.Focus();
|
|
1003
|
+
}
|
|
1004
|
+
break;
|
|
1005
|
+
|
|
1006
|
+
default:
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
event.stopPropagation();
|
|
1011
|
+
event.preventDefault();
|
|
1012
|
+
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// number format input
|
|
1018
|
+
|
|
1019
|
+
CreateInputHandler('input.treb-number-format', (format: string) => {
|
|
1020
|
+
if (!format) { return false; }
|
|
1021
|
+
sheet.HandleToolbarMessage({
|
|
1022
|
+
command: 'number-format',
|
|
1023
|
+
format,
|
|
1024
|
+
})
|
|
1025
|
+
return true;
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// font scale input
|
|
1029
|
+
|
|
1030
|
+
CreateInputHandler('input.treb-font-scale', (value: string) => {
|
|
1031
|
+
const scale = Number(value);
|
|
1032
|
+
if (!scale || isNaN(scale)) {
|
|
1033
|
+
console.warn('invalid scale value');
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
sheet.HandleToolbarMessage({
|
|
1037
|
+
command: 'font-scale',
|
|
1038
|
+
scale,
|
|
1039
|
+
});
|
|
1040
|
+
return true;
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// color chooser
|
|
1044
|
+
|
|
1045
|
+
const color_input = color_chooser.querySelector('input') as HTMLInputElement;
|
|
1046
|
+
const color_button = color_chooser.querySelector('input + button') as HTMLButtonElement;
|
|
1047
|
+
|
|
1048
|
+
color_input.addEventListener('input', (event: Event) => {
|
|
1049
|
+
|
|
1050
|
+
if (event instanceof InputEvent && event.isComposing) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
color_button.style.background = color_input.value || '';
|
|
1055
|
+
|
|
1056
|
+
// this is a check for "did it resolve properly"
|
|
1057
|
+
const resolved = color_button.style.backgroundColor || '#fff';
|
|
1058
|
+
const bytes = Measurement.MeasureColor(resolved);
|
|
1059
|
+
const hsl = Color.RGBToHSL(bytes[0], bytes[1], bytes[2]);
|
|
1060
|
+
|
|
1061
|
+
// light or dark based on background
|
|
1062
|
+
color_button.style.color = (hsl.l > .5) ? '#000' : '#fff';
|
|
1063
|
+
|
|
1064
|
+
// color for command
|
|
1065
|
+
color_button.dataset.color = JSON.stringify(
|
|
1066
|
+
color_button.style.backgroundColor ? { text: color_button.style.backgroundColor } : {});
|
|
1067
|
+
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
color_input.addEventListener('keydown', event => {
|
|
1071
|
+
if (event.key === 'Enter') {
|
|
1072
|
+
event.stopPropagation();
|
|
1073
|
+
event.preventDefault();
|
|
1074
|
+
|
|
1075
|
+
color_button.click();
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// --- menus ---------------------------------------------------------------
|
|
1080
|
+
|
|
1081
|
+
// since we are positioning menus with script, they'll get detached
|
|
1082
|
+
// if you scroll the toolbar. we could track scrolling, but it makes
|
|
1083
|
+
// as much sense to just close any open menu.
|
|
1084
|
+
|
|
1085
|
+
scroller.addEventListener('scroll', () => sheet.Focus());
|
|
1086
|
+
|
|
1087
|
+
// we set up a key listener for the escape key when menus are open, we
|
|
1088
|
+
// need to remove it if focus goes out of the toolbar
|
|
1089
|
+
|
|
1090
|
+
let handlers_attached = false;
|
|
1091
|
+
|
|
1092
|
+
const escape_handler = (event: KeyboardEvent) => {
|
|
1093
|
+
if (event.key === 'Escape') {
|
|
1094
|
+
event.stopPropagation();
|
|
1095
|
+
event.preventDefault();
|
|
1096
|
+
Promise.resolve().then(() => sheet.Focus());
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const focusout_handler = (event: FocusEvent) => {
|
|
1101
|
+
if (handlers_attached) {
|
|
1102
|
+
if (event.relatedTarget instanceof Node && toolbar.contains(event.relatedTarget)) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
toolbar.removeEventListener('keydown', escape_handler);
|
|
1106
|
+
toolbar.removeEventListener('focusout', focusout_handler);
|
|
1107
|
+
handlers_attached = false;
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
// positioning on focusin will catch keyboard and mouse navigation
|
|
1112
|
+
|
|
1113
|
+
toolbar.addEventListener('focusin', event => {
|
|
1114
|
+
|
|
1115
|
+
const target = event.target as HTMLElement;
|
|
1116
|
+
const parent = target?.parentElement;
|
|
1117
|
+
|
|
1118
|
+
if (parent?.classList.contains('treb-menu')) {
|
|
1119
|
+
|
|
1120
|
+
if (!handlers_attached) {
|
|
1121
|
+
toolbar.addEventListener('focusout', focusout_handler);
|
|
1122
|
+
toolbar.addEventListener('keydown', escape_handler);
|
|
1123
|
+
handlers_attached = true;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// we're sharing the color chooser, drop it in to
|
|
1127
|
+
// the target if this is a color menu
|
|
1128
|
+
|
|
1129
|
+
if (parent.dataset.colorCommand) {
|
|
1130
|
+
color_chooser.querySelector('.treb-default-color')?.setAttribute('title', parent.dataset.defaultColorText || 'Default color');
|
|
1131
|
+
|
|
1132
|
+
parent.appendChild(color_chooser);
|
|
1133
|
+
color_chooser.dataset.colorCommand = parent.dataset.colorCommand;
|
|
1134
|
+
color_chooser.dataset.target = parent.dataset.replaceColor || '';
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const menu = parent.querySelector('div') as HTMLElement;
|
|
1138
|
+
|
|
1139
|
+
const scroller_rect = scroller.getBoundingClientRect();
|
|
1140
|
+
const target_rect = target.getBoundingClientRect();
|
|
1141
|
+
|
|
1142
|
+
let { left } = target_rect;
|
|
1143
|
+
|
|
1144
|
+
// for composite controls, align to the first component
|
|
1145
|
+
// (that only needs to apply on left-aligning)
|
|
1146
|
+
|
|
1147
|
+
const group = parent.parentElement;
|
|
1148
|
+
|
|
1149
|
+
if (group?.hasAttribute('composite')) {
|
|
1150
|
+
const element = group.firstElementChild as HTMLElement;
|
|
1151
|
+
const rect = element.getBoundingClientRect();
|
|
1152
|
+
left = rect.left;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const menu_rect = menu.getBoundingClientRect();
|
|
1156
|
+
|
|
1157
|
+
if (parent.classList.contains('treb-submenu')) {
|
|
1158
|
+
|
|
1159
|
+
menu.style.top = (target_rect.top - menu_rect.height / 2) + 'px';
|
|
1160
|
+
|
|
1161
|
+
if (left + target_rect.width + 6 + menu_rect.width > scroller_rect.right) {
|
|
1162
|
+
menu.style.left = (left - 6 - menu_rect.width) + 'px';
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
menu.style.left = (left + target_rect.width + 6) + 'px';
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
menu.style.top = target_rect.bottom + 'px';
|
|
1171
|
+
|
|
1172
|
+
// right-align if we would overflow the toolbar
|
|
1173
|
+
|
|
1174
|
+
if (left + menu_rect.width > scroller_rect.right - 6) {
|
|
1175
|
+
menu.style.left = (target_rect.right - menu_rect.width) + 'px';
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
menu.style.left = left + 'px';
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const focus = menu.querySelector('textarea, input') as HTMLElement;
|
|
1184
|
+
if (focus) {
|
|
1185
|
+
requestAnimationFrame(() => focus.focus());
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const format_menu = this.root?.querySelector('.treb-number-format-menu') as HTMLElement;
|
|
1193
|
+
if (format_menu) {
|
|
1194
|
+
|
|
1195
|
+
// the first time we call this (now) we want to get the default
|
|
1196
|
+
// colors for text, fill, and border to set buttons.
|
|
1197
|
+
|
|
1198
|
+
this.UpdateDocumentStyles(sheet, format_menu);
|
|
1199
|
+
this.UpdateSelectionStyle(sheet, toolbar, comment_box);
|
|
1200
|
+
|
|
1201
|
+
sheet.Subscribe(event => {
|
|
1202
|
+
switch (event.type) {
|
|
1203
|
+
|
|
1204
|
+
// need to do something with this
|
|
1205
|
+
case 'focus-view':
|
|
1206
|
+
break;
|
|
1207
|
+
|
|
1208
|
+
case 'data':
|
|
1209
|
+
case 'document-change':
|
|
1210
|
+
case 'load':
|
|
1211
|
+
case 'reset':
|
|
1212
|
+
this.UpdateDocumentStyles(sheet, format_menu);
|
|
1213
|
+
this.UpdateSelectionStyle(sheet, toolbar, comment_box);
|
|
1214
|
+
break;
|
|
1215
|
+
|
|
1216
|
+
case 'selection':
|
|
1217
|
+
this.UpdateSelectionStyle(sheet, toolbar, comment_box);
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
}
|
|
1227
|
+
|