@trebco/treb 23.6.5 → 25.0.0-rc2

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