@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.
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} +285 -269
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -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 +1227 -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 +5362 -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 +320 -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,2618 @@
1
+ /*
2
+ * This file is part of TREB.
3
+ *
4
+ * TREB is free software: you can redistribute it and/or modify it under the
5
+ * terms of the GNU General Public License as published by the Free Software
6
+ * Foundation, either version 3 of the License, or (at your option) any
7
+ * later version.
8
+ *
9
+ * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ * details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License along
15
+ * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ * Copyright 2022-2023 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ import { DOMUtilities } from '../util/dom_utilities';
23
+ import type { DataModel, ViewModel } from '../types/data_model';
24
+
25
+ import type { Tile } from '../types/tile';
26
+ import { Style, Theme, Point, Extent, Size, Position, Area, ICellAddress, Rectangle, ThemeColor, Table } from 'treb-base-types';
27
+
28
+ import { MouseDrag } from '../types/drag_mask';
29
+ import type { GridEvent } from '../types/grid_events';
30
+
31
+ // aliasing Area as TileRange. this seemed like a good idea, initially, because
32
+ // it can help clarify the function calls and return values when we "overload"
33
+ // area to refer to ranges of tiles.
34
+ //
35
+ // on the other hand, it seems like it might be error-prone because we can swap
36
+ // one for the other pretty easily and typescript won't complain.
37
+ //
38
+ // a more thorough (and probably over-engineered) way to do this would be to
39
+ // define area as a generic, then define it on some arbitrary value. that would
40
+ // force separation of all the functions between the two types (I think)
41
+
42
+ import { Area as TileRange, CellValue, AnnotationLayout, Corner } from 'treb-base-types';
43
+ import type { Annotation } from '../types/annotation';
44
+
45
+ export { Area as TileRange } from 'treb-base-types';
46
+
47
+ const SVGNS = 'http://www.w3.org/2000/svg';
48
+
49
+ export interface TooltipOptions {
50
+ up?: true;
51
+ left?: true;
52
+ text?: string;
53
+ x?: number;
54
+ y?: number;
55
+ }
56
+
57
+ /**
58
+ * layout structure and management functions
59
+ */
60
+ export abstract class BaseLayout {
61
+
62
+ public column_header!: HTMLDivElement;
63
+ public row_header!: HTMLDivElement;
64
+ public contents!: HTMLDivElement;
65
+
66
+ public buffer_canvas!: HTMLCanvasElement;
67
+
68
+ public corner!: HTMLDivElement;
69
+ public corner_canvas!: HTMLCanvasElement; // HTMLDivElement;
70
+
71
+ public grid_selection!: SVGElement;
72
+
73
+ public grid_cover!: HTMLDivElement;
74
+ public column_header_cover!: HTMLDivElement;
75
+ public row_header_cover!: HTMLDivElement;
76
+
77
+ public annotation_container!: HTMLDivElement;
78
+
79
+ public mask!: HTMLDivElement;
80
+ public mock_selection!: HTMLDivElement;
81
+ public container?: HTMLElement; // reference to container
82
+
83
+ public grid_tiles: Tile[][] = [];
84
+ public column_header_tiles: Tile[] = [];
85
+ public row_header_tiles: Tile[] = [];
86
+
87
+ public corner_selection!: SVGElement;
88
+ public row_header_selection!: SVGElement;
89
+ public column_header_selection!: SVGElement;
90
+
91
+ public corner_annotations!: HTMLDivElement;
92
+ public row_header_annotations!: HTMLDivElement;
93
+ public column_header_annotations!: HTMLDivElement;
94
+
95
+ public frozen_row_tiles: Tile[] = [];
96
+ public frozen_column_tiles: Tile[] = [];
97
+
98
+ public header_size: Size = { width: 0, height: 0 };
99
+
100
+ /**
101
+ * last rendered column. this is used to calculate the limits of
102
+ * cell overflows, which may exceed actual data in the sheet.
103
+ */
104
+ public last_column = 0;
105
+
106
+ public total_height = 0;
107
+ public total_width = 0;
108
+
109
+ public default_row_height = 0;
110
+ public default_column_width = 0;
111
+ public header_offset = {
112
+ x: 0, y: 0,
113
+ };
114
+
115
+ /** freeze rows/columns */
116
+ // public freeze = { rows: 0, columns: 0 };
117
+
118
+ /**
119
+ * NOTE: dpr can probably change, on zoom; but I'm not sure there's
120
+ * an event we can trap for that. it might be necessary to test this
121
+ * periodically.
122
+ */
123
+ public dpr = Math.max(1, self.devicePixelRatio || 1);
124
+
125
+ /** separate scale, user-controlled (testing...) */
126
+ public scale = 1;
127
+
128
+ /**
129
+ * this is a reference to the node that handles scrolling. it needs
130
+ * to be different for legacy renderer.
131
+ */
132
+ public scroll_reference_node!: HTMLElement;
133
+
134
+ public get scroll_offset(): { x: number, y: number } {
135
+ if (!this.scroll_reference_node) {
136
+ return { x: 0, y: 0 };
137
+ }
138
+ return {
139
+ x: this.scroll_reference_node.scrollLeft,
140
+ y: this.scroll_reference_node.scrollTop,
141
+ };
142
+ }
143
+
144
+ public set scroll_offset(offset: { x: number; y: number }) {
145
+ if (!this.scroll_reference_node) {
146
+ return;
147
+ }
148
+ this.scroll_reference_node.scrollLeft = offset.x;
149
+ this.scroll_reference_node.scrollTop = offset.y;
150
+ }
151
+
152
+ protected dropdown_caret: SVGSVGElement;
153
+
154
+ /** we have to disable mock selection for IE or it breaks key handling */
155
+ private trident = ((typeof navigator !== 'undefined') &&
156
+ navigator.userAgent && /trident/i.test(navigator.userAgent));
157
+
158
+ // private default_tile_size: Size = { width: 600, height: 400 };
159
+ private default_tile_size: Size = { width: 1200, height: 800 };
160
+
161
+ private tooltip_state?: 'up' | 'left';
162
+
163
+ private tooltip: HTMLDivElement;
164
+
165
+ private dropdown_list: HTMLDivElement;
166
+ private dropdown_caret_visible = false;
167
+ private dropdown_callback?: (value: CellValue) => void;
168
+ private dropdown_selected?: HTMLElement;
169
+
170
+ // private selection_layout_token?: any;
171
+
172
+ // private error_highlight: HTMLDivElement;
173
+ // private error_highlight_timeout?: any;
174
+
175
+ private note_node: HTMLDivElement;
176
+ private sort_button: HTMLButtonElement;
177
+
178
+ private title_node: HTMLDivElement;
179
+
180
+ private row_cache: number[] = [];
181
+ private column_cache: number[] = [];
182
+
183
+ /**
184
+ * flag so we don't try to paint before we have tiles
185
+ */
186
+ private initialized = false;
187
+
188
+
189
+ constructor(protected model: DataModel, protected view: ViewModel) {
190
+
191
+ // now attaching to node... no longer global
192
+ // actually if we are not in a web component, we might as well
193
+ // use global...
194
+
195
+ // can't use global if it's inside a block because of z-stacking
196
+ // contexts; the mask will be under the next sheet. so either
197
+ // global in body, or instance local.
198
+
199
+ this.mask = // document.querySelector('.treb-mouse-mask'); // ||
200
+ DOMUtilities.CreateDiv('treb-mouse-mask');
201
+ this.tooltip = // document.querySelector('.treb-tooltip'); // ||
202
+ DOMUtilities.CreateDiv('treb-tooltip');
203
+
204
+ // this.error_highlight = DOMUtilities.CreateDiv('treb-error-highlight');
205
+
206
+ this.dropdown_caret = document.createElementNS(SVGNS, 'svg') as SVGSVGElement;
207
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
208
+ this.dropdown_caret.setAttribute('viewBox', '0 0 24 24');
209
+ this.dropdown_caret.tabIndex = -1;
210
+
211
+ const caret = document.createElementNS(SVGNS, 'path');
212
+ caret.setAttribute('d', 'M5,7 L12,17 L19,7');
213
+ this.dropdown_caret.appendChild(caret);
214
+
215
+ this.dropdown_caret.addEventListener('click', (event) => {
216
+
217
+ event.stopPropagation();
218
+ event.preventDefault();
219
+
220
+ this.grid_cover.classList.remove('nub-select');
221
+
222
+ // the classList polyfill doesn't apply to svg elements (not sure
223
+ // if that's an oversight, or IE11 just won't support it) -- but
224
+ // either way we can't use it
225
+
226
+ const class_name = this.dropdown_caret.getAttribute('class') || '';
227
+
228
+ if (/active/i.test(class_name)) {
229
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
230
+ }
231
+ else {
232
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret active');
233
+ this.dropdown_list.focus();
234
+ }
235
+
236
+ });
237
+
238
+ // we used to focus on caret. that broke when we started supporting
239
+ // long lists and scrolling. so now we focus on the list.
240
+
241
+ /*
242
+ this.dropdown_caret.addEventListener('focusout', () => {
243
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
244
+ this.container?.focus();
245
+ });
246
+ */
247
+
248
+ this.dropdown_list = DOMUtilities.CreateDiv('treb-dropdown-list');
249
+ this.dropdown_list.setAttribute('tabindex', '-1'); // focusable
250
+
251
+ // this.dropdown_caret.addEventListener('keydown', (event) => {
252
+ this.dropdown_list.addEventListener('keydown', (event) => {
253
+ let delta = 0;
254
+
255
+ switch (event.key) {
256
+ case 'ArrowDown':
257
+ delta = 1;
258
+ break;
259
+ case 'ArrowUp':
260
+ delta = -1;
261
+ break;
262
+ case 'Escape':
263
+ break;
264
+ case 'Enter':
265
+ break;
266
+ default:
267
+ console.info(event.key);
268
+ return;
269
+ }
270
+
271
+ event.stopPropagation();
272
+ event.preventDefault();
273
+
274
+ if (event.key === 'Escape' || event.key === 'Enter') {
275
+ this.container?.focus();
276
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
277
+ if (event.key === 'Enter' && this.dropdown_callback) {
278
+ if (this.dropdown_selected) {
279
+ this.dropdown_callback.call(0, (this.dropdown_selected as any).dropdown_value);
280
+ }
281
+ }
282
+ }
283
+ else if (delta) {
284
+ if (this.dropdown_selected) {
285
+ if (delta > 0 && this.dropdown_selected.nextSibling) {
286
+ (this.dropdown_selected.nextSibling as HTMLElement).classList.add('selected');
287
+ this.dropdown_selected.classList.remove('selected');
288
+ this.dropdown_selected = this.dropdown_selected.nextSibling as HTMLElement;
289
+
290
+ // support scrolling
291
+
292
+ const bottom = this.dropdown_selected.offsetTop + this.dropdown_selected.offsetHeight;
293
+ if (bottom >
294
+ this.dropdown_list.offsetHeight + this.dropdown_list.scrollTop) {
295
+ this.dropdown_list.scrollTop = bottom - this.dropdown_list.offsetHeight;
296
+ }
297
+
298
+ }
299
+ else if (delta < 0 && this.dropdown_selected.previousSibling) {
300
+ (this.dropdown_selected.previousSibling as HTMLElement).classList.add('selected');
301
+ this.dropdown_selected.classList.remove('selected');
302
+ this.dropdown_selected = this.dropdown_selected.previousSibling as HTMLElement;
303
+
304
+ // support scrolling
305
+
306
+ if (this.dropdown_selected.offsetTop < this.dropdown_list.scrollTop) {
307
+ this.dropdown_list.scrollTop = this.dropdown_selected.offsetTop;
308
+ }
309
+
310
+ }
311
+ }
312
+ }
313
+
314
+ });
315
+
316
+ this.dropdown_list.addEventListener('mousedown', (event) => {
317
+
318
+ const target = event.target as HTMLElement;
319
+ if (event.target === this.dropdown_list) {
320
+ return;
321
+ }
322
+
323
+ event.stopPropagation();
324
+ event.preventDefault();
325
+
326
+ this.container?.focus();
327
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
328
+
329
+ if (this.dropdown_callback) {
330
+ this.dropdown_callback.call(0, (target as any).dropdown_value);
331
+ }
332
+ });
333
+
334
+ this.dropdown_list.addEventListener('mousemove', (event) => {
335
+ const target = event.target as HTMLElement;
336
+ if (target === this.dropdown_selected) {
337
+ return;
338
+ }
339
+ this.grid_cover.classList.remove('nub-select');
340
+ if (this.dropdown_selected) {
341
+ this.dropdown_selected.classList.remove('selected');
342
+ }
343
+ target.classList.add('selected');
344
+ this.dropdown_selected = target as HTMLElement;
345
+ });
346
+
347
+ this.mock_selection = DOMUtilities.CreateDiv('mock-selection-node');
348
+ this.mock_selection.innerHTML = '&nbsp;';
349
+
350
+ this.note_node = DOMUtilities.CreateDiv('treb-note');
351
+ this.title_node = DOMUtilities.CreateDiv('treb-hover-title');
352
+
353
+ this.sort_button = DOMUtilities.Create<HTMLButtonElement>(
354
+ 'button',
355
+ 'treb-sort-button', undefined, undefined, { title: 'Sort table'});
356
+
357
+ this.HideNote();
358
+
359
+ }
360
+
361
+ /**
362
+ * if the DPR has changed, update it and return true. otherwise return
363
+ * false. this is used on resize events: if the scale has changed, we
364
+ * probably want to repaint (and we need to update scale).
365
+ */
366
+ public UpdateDPR(): boolean {
367
+ const dpr = Math.max(1, self.devicePixelRatio || 1);
368
+ if (dpr === this.dpr) {
369
+ return false;
370
+ }
371
+ this.dpr = dpr;
372
+ return true;
373
+ }
374
+
375
+ /** wrapper around sheet method, incorporating scale */
376
+ public ColumnWidth(column: number): number {
377
+ return Math.round(this.view.active_sheet.GetColumnWidth(column) * this.scale);
378
+ }
379
+
380
+ /** wrapper around sheet method, incorporating scale */
381
+ public RowHeight(row: number): number {
382
+ return Math.round(this.view.active_sheet.GetRowHeight(row) * this.scale);
383
+ }
384
+
385
+ /**
386
+ * wrapper around sheet method, incorporating scale
387
+ *
388
+ * NOTE: this does not update total size, so unless there's a subsequent call
389
+ * to a layout update, total size will be out of sync
390
+ */
391
+ public SetRowHeight(row: number, height: number): void {
392
+ this.view.active_sheet.SetRowHeight(row, Math.round(height / this.scale));
393
+ }
394
+
395
+ /**
396
+ * wrapper around sheet method, incorporating scale
397
+ *
398
+ * NOTE: this does not update total size, so unless there's a subsequent call
399
+ * to a layout update, total size will be out of sync
400
+ */
401
+ public SetColumnWidth(column: number, width: number): void {
402
+ this.view.active_sheet.SetColumnWidth(column, Math.round(width / this.scale));
403
+ }
404
+
405
+ /**
406
+ * show/hide grid selections. used when selecting annotations.
407
+ */
408
+ public ShowSelections(show = true): void {
409
+ this.grid_selection.style.display = show ? 'block' : 'none';
410
+ }
411
+
412
+ public HideTitle(): void {
413
+ this.title_node.style.opacity = '0';
414
+ // this.title_node.style.pointerEvents = 'none';
415
+ }
416
+
417
+ public ShowTitle(text: string, address: ICellAddress /*, event?: MouseEvent */): void {
418
+ this.title_node.textContent = text;
419
+
420
+ if (!this.title_node.parentElement) return;
421
+
422
+ // const note_size = this.title_node.getBoundingClientRect();
423
+ const container = this.title_node.parentElement.getBoundingClientRect();
424
+
425
+ const rect = this.OffsetCellAddressToRectangle(address).Shift(
426
+ this.header_size.width, this.header_size.height);
427
+
428
+ this.title_node.style.left = (
429
+ container.left + rect.left - this.scroll_reference_node.scrollLeft + 0) + 'px';
430
+
431
+ this.title_node.style.top = (
432
+ container.top + rect.bottom - this.scroll_reference_node.scrollTop + 8) + 'px';
433
+
434
+ // FIXME: use class
435
+
436
+ this.title_node.style.opacity = '1';
437
+ // this.title_node.style.pointerEvents = 'auto';
438
+
439
+ }
440
+
441
+ public HideNote(): void {
442
+
443
+ // FIXME: use class
444
+
445
+ this.note_node.style.opacity = '0';
446
+ this.note_node.style.pointerEvents = 'none';
447
+ }
448
+
449
+ public HideTableSortButton() {
450
+ this.sort_button.style.opacity = '0';
451
+ this.sort_button.style.pointerEvents = 'none';
452
+ }
453
+
454
+ public ShowTableSortButton(table: Table, column: number, address: ICellAddress): void {
455
+
456
+ if (!this.sort_button.parentElement) {
457
+ return;
458
+ }
459
+
460
+ let asc = true;
461
+ let initial = false;
462
+
463
+ if (table.sort) {
464
+ if (table.sort.column === column) {
465
+ asc = !table.sort.asc;
466
+ initial = true;
467
+ }
468
+ }
469
+
470
+ this.sort_button.style.opacity = '1';
471
+ this.sort_button.style.pointerEvents = 'initial';
472
+ this.sort_button.classList.remove('asc', 'desc');
473
+ if (initial) {
474
+ this.sort_button.classList.add(asc ? 'asc' : 'desc');
475
+ }
476
+
477
+ this.sort_button.dataset.asc = asc.toString();
478
+ this.sort_button.dataset.table = table.name;
479
+ this.sort_button.dataset.column = column.toString();
480
+
481
+ const rect = this.OffsetCellAddressToRectangle(address).Shift(
482
+ this.header_size.width, this.header_size.height);
483
+
484
+ const button_size = this.sort_button.getBoundingClientRect();
485
+
486
+ // const container = this.sort_button.parentElement.getBoundingClientRect();
487
+ // const offset = { x: 8, y: 2 };
488
+
489
+ this.sort_button.style.left = (rect.right - button_size.width - button_size.width / 2) + 'px';
490
+
491
+ this.sort_button.style.top =
492
+ (rect.top + (rect.height - button_size.height) / 2) + 'px';
493
+
494
+ }
495
+
496
+ /**
497
+ * adding html parameter at the end, so we can keep the function
498
+ * signature otherwise the same. this is for markdown formatting.
499
+ */
500
+ public ShowNote(note: string, address: ICellAddress, event?: MouseEvent, markdown?: string): void {
501
+
502
+ // UPDATE for MD (optional)
503
+
504
+ if (markdown) {
505
+ this.note_node.innerHTML = markdown;
506
+ }
507
+ else {
508
+ this.note_node.textContent = note;
509
+ }
510
+
511
+ if (!this.note_node.parentElement) return;
512
+
513
+ const note_size = this.note_node.getBoundingClientRect();
514
+ const container = this.note_node.parentElement.getBoundingClientRect();
515
+
516
+ const offset = { x: 8, y: 2 };
517
+
518
+ const rect = this.OffsetCellAddressToRectangle(address).Shift(
519
+ this.header_size.width, this.header_size.height);
520
+
521
+ this.note_node.style.left = (
522
+ container.left + rect.right - this.scroll_reference_node.scrollLeft + offset.x) + 'px';
523
+ this.note_node.style.top = (
524
+ container.top + rect.top - this.scroll_reference_node.scrollTop - (note_size.height / 5) - offset.y) + 'px';
525
+
526
+ // FIXME: use class
527
+
528
+ this.note_node.style.opacity = '1';
529
+ this.note_node.style.pointerEvents = 'auto';
530
+ }
531
+
532
+ /* * needed for IE11, legacy only * /
533
+ public FixBrokenSelection() {
534
+ // ...
535
+ }
536
+ */
537
+
538
+ /**
539
+ * raise or lower annotation in z-order (implicit)
540
+ *
541
+ * returns true if we've made changes, so you can trigger any necessary
542
+ * events or side-effects
543
+ */
544
+ public AnnotationLayoutOrder(annotation: Annotation, delta: number): boolean {
545
+
546
+ // find index
547
+ let index = -1;
548
+ for (let i = 0; i < this.view.active_sheet.annotations.length; i++) {
549
+ if (this.view.active_sheet.annotations[i] === annotation) {
550
+ index = i;
551
+ break;
552
+ }
553
+ }
554
+
555
+ if (index < 0) {
556
+ return false; // not found
557
+ }
558
+
559
+ const target = Math.min(Math.max(0, index + delta), this.view.active_sheet.annotations.length - 1);
560
+
561
+ if (target === index) {
562
+ return false; // not moving (probably at edge)
563
+ }
564
+
565
+ // change in array order, so it's preserved
566
+
567
+ this.view.active_sheet.annotations.splice(index, 1);
568
+ this.view.active_sheet.annotations.splice(target, 0, annotation);
569
+
570
+ // update layout, use z-indexes
571
+
572
+ for (let i = 0; i < this.view.active_sheet.annotations.length; i++) {
573
+
574
+ // updating to shift frozen annotations as well
575
+
576
+ const key = this.view.active_sheet.annotations[i].key;
577
+ const elements = this.container?.querySelectorAll(`.annotation[data-key="${key}"]`)
578
+ if (elements) {
579
+ for (let j = 0; j < elements?.length; j++) {
580
+ (elements[j] as HTMLElement).style.zIndex = (i + 1).toString();
581
+ }
582
+ }
583
+
584
+ /*
585
+ const node = this.view.active_sheet.annotations[i].node;
586
+ if (node) {
587
+ node.style.zIndex = (i + 1).toString();
588
+ }
589
+ */
590
+
591
+ }
592
+
593
+ return true;
594
+
595
+ }
596
+
597
+ /**
598
+ *
599
+ */
600
+ public PointToAnnotationCorner(point: Point): Corner {
601
+ const address = this.PointToAddress_Grid(point, undefined, false);
602
+ const cell_rect = this.CellAddressToRectangle(address);
603
+
604
+ return {
605
+ address,
606
+ offset: {
607
+ x: (point.x - cell_rect.left) / cell_rect.width,
608
+ y: (point.y - cell_rect.top) / cell_rect.height,
609
+ },
610
+ };
611
+ }
612
+
613
+ /**
614
+ * utility for managing (new) annotation layout. we offset the {top, left}
615
+ * by {1, 1} pixel so that the alignment snaps to cell boundaries.
616
+ */
617
+ public RectToAnnotationLayout(rect: Partial<Rectangle>): AnnotationLayout {
618
+ return {
619
+ tl: this.PointToAnnotationCorner({ x: (rect.left || 0) + 1, y: (rect.top || 0) + 1 }),
620
+ br: this.PointToAnnotationCorner({ x: rect.right || rect.left || 100, y: rect.bottom || rect.top || 100 }),
621
+ };
622
+ }
623
+
624
+ public AddressToAnnotationLayout(tl: ICellAddress, br: ICellAddress): AnnotationLayout {
625
+ const rects = {
626
+ tl: this.CellAddressToRectangle(tl),
627
+ br: this.CellAddressToRectangle(br),
628
+ };
629
+ return {
630
+ tl: this.PointToAnnotationCorner({ x: (rects.tl.left || 0), y: (rects.tl.top || 0) }),
631
+ br: this.PointToAnnotationCorner({ x: rects.br.right || rects.tl.left || 100, y: rects.br.bottom || rects.tl.left || 100 }),
632
+ };
633
+ }
634
+
635
+ /**
636
+ * @see RectToAnnotationLayout regarding the 1 pixel shift
637
+ */
638
+ public AnnotationLayoutToRect(layout: AnnotationLayout): Rectangle {
639
+
640
+ const tl = this.CellAddressToRectangle(layout.tl.address);
641
+ const br = this.CellAddressToRectangle(layout.br.address);
642
+
643
+ const left = tl.left + tl.width * layout.tl.offset.x - 1;
644
+ const top = tl.top + tl.height * layout.tl.offset.y - 1;
645
+
646
+ return new Rectangle(
647
+ left, top,
648
+ br.left + br.width * layout.br.offset.x - left,
649
+ br.top + br.height * layout.br.offset.y - top,
650
+ );
651
+
652
+ }
653
+
654
+ public UpdateAnnotation(elements: Annotation | Annotation[]): void {
655
+ if (!Array.isArray(elements)) elements = [elements];
656
+ for (const annotation of elements) {
657
+ const view = annotation.view[this.view.view_index] || {};
658
+ if (view.node) {
659
+
660
+ /*
661
+ if (annotation.node.dataset.scale && annotation.node.dataset.scale !== this.scale.toString()) {
662
+ console.info('scale out of sync');
663
+ }
664
+ */
665
+
666
+ view.node.dataset.scale = this.scale.toString();
667
+ view.node.style.fontSize = `${10 * this.scale}pt`;
668
+
669
+ // update the layout here if necessary. after that it should
670
+ // be persisted (assuming it's saved). eventually this should
671
+ // be superfluous...
672
+
673
+ if (annotation.rect && !annotation.layout) {
674
+
675
+ // this is breaking on freeze when the spreadsheet is scrolled because
676
+ // the top-left uses the freeze panes. stop doing that.
677
+
678
+ annotation.scaled_rect = annotation.rect.Scale(this.scale);
679
+ annotation.layout = this.RectToAnnotationLayout(annotation.scaled_rect);
680
+ }
681
+
682
+
683
+ // FIXME: merge cells? [...]
684
+
685
+ if (annotation.layout) {
686
+
687
+ const rect = this.AnnotationLayoutToRect(annotation.layout);
688
+ rect.ApplyStyle(view.node);
689
+
690
+ // NOTE: we still set the scaled rect, because that's used in
691
+ // manipulating at scale. we will need to make sure that we update
692
+ // the layout when the scaled rect / regular rect changes...
693
+
694
+ annotation.scaled_rect = rect; // .Scale(this.scale);
695
+
696
+ }
697
+
698
+ view.node.dataset.key = annotation.key.toString();
699
+
700
+ // FIXME: only do this if necessary (if frozen).
701
+
702
+ if (this.view.active_sheet.freeze.rows || this.view.active_sheet.freeze.columns) {
703
+ this.CloneFrozenAnnotation(annotation);
704
+ }
705
+
706
+ }
707
+ }
708
+ }
709
+
710
+ /** returns a list of copies painted to frozen panes, for move/size */
711
+ public GetFrozenAnnotations(annotation: Annotation): HTMLElement[] {
712
+ const containers = [this.row_header_annotations, this.column_header_annotations, this.corner_annotations];
713
+ return containers.map((container) => container.querySelector(`.annotation[data-key="${annotation.key}"]`)).filter(test => test !== null) as HTMLElement[];
714
+ }
715
+
716
+ /**
717
+ * clone all annotations into freeze panes
718
+ */
719
+ public CloneFrozenAnnotations(): void {
720
+ for (const annotation of this.view.active_sheet.annotations) {
721
+ const view = annotation.view[this.view.view_index];
722
+ if (view?.node && annotation.key) {
723
+ this.CloneFrozenAnnotation(annotation);
724
+ }
725
+ }
726
+ }
727
+
728
+ /**
729
+ * remove all annotations from freeze panes
730
+ *
731
+ */
732
+ public ClearFrozenAnnotations(): void {
733
+ for (const container of [this.row_header_annotations, this.column_header_annotations, this.corner_annotations]) {
734
+ const elements = container.querySelectorAll('.annotation');
735
+ for (let i = 0; i < elements.length; i++) {
736
+ // FIXME: remove event listeners
737
+ elements[i].parentElement?.removeChild(elements[i]);
738
+ }
739
+ }
740
+ }
741
+
742
+ /**
743
+ * remove a frozen annotation
744
+ * @param annotation
745
+ */
746
+ public RemoveFrozenAnnotation(annotation: Annotation): void {
747
+ for (const container of [this.row_header_annotations, this.column_header_annotations, this.corner_annotations]) {
748
+ const element = container.querySelector(`.annotation[data-key="${annotation.key}"]`);
749
+ if (element) {
750
+ // FIXME: remove event listeners
751
+ element.parentElement?.removeChild(element);
752
+ }
753
+ }
754
+ }
755
+
756
+ /**
757
+ * clone a single annotation. usually this will be used on create, but
758
+ * we batch them when we freeze panes (from unfrozen)
759
+ */
760
+ public CloneFrozenAnnotation(annotation: Annotation): void {
761
+
762
+ for (const container of [this.row_header_annotations, this.column_header_annotations, this.corner_annotations]) {
763
+
764
+ // FIXME: could reuse? not sure it's worth it
765
+ let element: Element | Node | null | undefined = container.querySelector(`.annotation[data-key="${annotation.key}"]`);
766
+ if (element) {
767
+ element.parentElement?.removeChild(element);
768
+ }
769
+
770
+ const view = annotation.view[this.view.view_index];
771
+ element = view?.node?.cloneNode(true);
772
+
773
+ if (element) {
774
+
775
+ const move_target = (element as HTMLElement).querySelector('.annotation-move-target') as HTMLElement;
776
+ const resize_target = (element as HTMLElement).querySelector('.annotation-resize-target') as HTMLElement;
777
+
778
+ (element as HTMLElement).addEventListener('mousedown', (event: MouseEvent) => {
779
+ const node = view.node;
780
+ requestAnimationFrame(() => {
781
+ // console.info('calling focus on', node);
782
+ node?.focus();
783
+ });
784
+ this.AnnotationMouseDown(annotation, view.node as HTMLElement, event, move_target, resize_target);
785
+ });
786
+ container.appendChild(element);
787
+ }
788
+
789
+ }
790
+
791
+ }
792
+
793
+ public RemoveAnnotation(annotation: Annotation): void {
794
+ const view = annotation.view[this.view.view_index] || {};
795
+ if (view.node) {
796
+ view.node.parentElement?.removeChild(view.node);
797
+ }
798
+ this.RemoveFrozenAnnotation(annotation);
799
+ }
800
+
801
+ /**
802
+ * remove annotation nodes from the container, without impacting
803
+ * the underlying data. annotations will still retain nodes, they
804
+ * just won't be attached to anything.
805
+ *
806
+ * NOTE: IE destroys nodes if you do this? (...)
807
+ * patch in legacy... actually we'll do it here
808
+ */
809
+ public RemoveAnnotationNodes(): void {
810
+
811
+ // we were using a shortcut, innerText = '', but if you do that
812
+ // in IE it destroys the nodes (!) -- so we need to explicitly
813
+ // remove them
814
+
815
+ // FIXME: we are explicitly adding them, why not just maintain a list?
816
+
817
+ const children = Array.prototype.map.call(
818
+ this.annotation_container.children, (node) => node) as HTMLElement[];
819
+
820
+ for (const child of children) {
821
+ this.annotation_container.removeChild(child);
822
+ }
823
+
824
+ if (this.view.active_sheet.freeze.rows || this.view.active_sheet.freeze.columns) {
825
+ this.ClearFrozenAnnotations();
826
+ }
827
+
828
+ }
829
+
830
+ public AddAnnotation(annotation: Annotation): void {
831
+ const view = annotation.view[this.view.view_index] || {};
832
+ if (!view.node) {
833
+ throw new Error('annotation view/node missing');
834
+ }
835
+ this.annotation_container.appendChild(view.node);
836
+ this.UpdateAnnotation(annotation);
837
+ }
838
+
839
+ // testing moving this here...
840
+ public AnnotationMouseDown(annotation: Annotation, node: HTMLElement, event: MouseEvent, move_target: HTMLElement, resize_target: HTMLElement): Promise<GridEvent | void> {
841
+
842
+ // console.info('annotation mousedown (in layout)', annotation);
843
+
844
+ const rect = annotation.scaled_rect;
845
+ if (!rect) {
846
+ console.info('missing scaled rect!');
847
+ return Promise.reject(); // ?
848
+ }
849
+
850
+ return new Promise<GridEvent | void>((resolve) => {
851
+
852
+ const origin = {
853
+ left: rect.left,
854
+ top: rect.top,
855
+ width: rect.width,
856
+ height: rect.height,
857
+ };
858
+
859
+ const scroll_node = this.scroll_reference_node;
860
+ const scroll_rect = scroll_node.getBoundingClientRect();
861
+
862
+ const bounding_rect = node.getBoundingClientRect();
863
+
864
+ // IE11 is not targeting the child nodes? why not? (...)
865
+ // console.info('target', (event.target as HTMLElement)?.className);
866
+
867
+ if (event.target === move_target || (event.target !== resize_target && event.altKey)) {
868
+
869
+ event.stopPropagation();
870
+ event.preventDefault();
871
+ node.focus();
872
+
873
+ const offset = {
874
+ x: bounding_rect.left + event.offsetX - rect.left,
875
+ y: bounding_rect.top + event.offsetY - rect.top,
876
+ };
877
+
878
+ const elements = [node, ...this.GetFrozenAnnotations(annotation)];
879
+ const scroll_delta = 25;
880
+
881
+ const grid_rect =
882
+ this.CellAddressToRectangle({ row: 0, column: 0 }).Combine(
883
+ this.CellAddressToRectangle({
884
+ row: this.view.active_sheet.rows - 1,
885
+ column: this.view.active_sheet.columns - 1,
886
+ })).Expand(-1, -1);
887
+
888
+ MouseDrag(this.mask, 'move', (move_event) => {
889
+
890
+ // check if we are oob the grid
891
+ // FIXME: clamp annotation to cell bounds (...) this is OK for now though
892
+
893
+ if (move_event.offsetY - scroll_rect.top < this.header_offset.y) {
894
+ const delta = Math.min(scroll_delta, scroll_node.scrollTop);
895
+ scroll_node.scrollTop -= delta;
896
+ offset.y += delta;
897
+ }
898
+ else if (move_event.offsetY - scroll_rect.top >= scroll_rect.height) {
899
+ if (scroll_node.scrollTop + scroll_rect.height < grid_rect.height) {
900
+ const delta = scroll_delta;
901
+ scroll_node.scrollTop += delta;
902
+ offset.y -= delta;
903
+ }
904
+ }
905
+
906
+ if (move_event.offsetX - scroll_rect.left < this.header_offset.x) {
907
+ const delta = Math.min(scroll_delta, scroll_node.scrollLeft);
908
+ scroll_node.scrollLeft -= delta;
909
+ offset.x += delta;
910
+ }
911
+ else if (move_event.offsetX - scroll_rect.left >= scroll_rect.width) {
912
+ if (scroll_node.scrollLeft + scroll_rect.width < grid_rect.width) {
913
+ const delta = scroll_delta;
914
+ scroll_node.scrollLeft += delta;
915
+ offset.x -= delta;
916
+ }
917
+ }
918
+
919
+ rect.top = move_event.offsetY - offset.y;
920
+ rect.left = move_event.offsetX - offset.x;
921
+
922
+ if (move_event.shiftKey) {
923
+
924
+ // move in one direction at a time
925
+ const dx = Math.abs(rect.left - origin.left);
926
+ const dy = Math.abs(rect.top - origin.top);
927
+
928
+ if (dx <= dy) { rect.left = origin.left; }
929
+ else { rect.top = origin.top; }
930
+
931
+ }
932
+
933
+ if (move_event.ctrlKey) {
934
+ const point = this.ClampToGrid({
935
+ x: rect.left, y: rect.top,
936
+ });
937
+ rect.left = point.x;
938
+ rect.top = point.y;
939
+ }
940
+
941
+ // node.style.top = (rect.top) + 'px';
942
+ // node.style.left = (rect.left) + 'px';
943
+
944
+ for (const element of elements) {
945
+ element.style.top = (rect.top) + 'px';
946
+ element.style.left = (rect.left) + 'px';
947
+ }
948
+
949
+ }, () => {
950
+ annotation.extent = undefined; // reset
951
+ // annotation.rect = rect.Scale(1/this.scale);
952
+ annotation.layout = this.RectToAnnotationLayout(rect);
953
+ // this.grid_events.Publish({ type: 'annotation', annotation, event: 'move' });
954
+ resolve({ type: 'annotation', annotation, event: 'move' })
955
+ });
956
+
957
+ return;
958
+
959
+ }
960
+ else if (event.target === resize_target) {
961
+
962
+ //if ((bounding_rect.width - event.offsetX <= 13) &&
963
+ // (bounding_rect.height - event.offsetY <= 13)) {
964
+
965
+ event.stopPropagation();
966
+ event.preventDefault();
967
+ node.focus();
968
+
969
+ let aspect = 0;
970
+ if (annotation.data?.original_size
971
+ && annotation.data.original_size.width
972
+ && annotation.data.original_size.height) {
973
+ aspect = annotation.data.original_size.width /
974
+ annotation.data.original_size.height;
975
+ }
976
+
977
+ const bounds = node.getBoundingClientRect();
978
+ const offset = {
979
+ x: bounds.left + event.offsetX - rect.width + resize_target.offsetLeft,
980
+ y: bounds.top + event.offsetY - rect.height + resize_target.offsetTop,
981
+ };
982
+
983
+ MouseDrag(this.mask, 'nw-resize', (move_event) => {
984
+
985
+ const elements = [node, ...this.GetFrozenAnnotations(annotation)];
986
+
987
+ rect.height = move_event.offsetY - offset.y;
988
+ rect.width = move_event.offsetX - offset.x;
989
+
990
+ if (move_event.shiftKey && move_event.ctrlKey) {
991
+ if (aspect) {
992
+
993
+ const dx = Math.abs(rect.width - origin.width);
994
+ const dy = Math.abs(rect.height - origin.height);
995
+
996
+ if (dx < dy) {
997
+ rect.width = aspect * rect.height;
998
+ }
999
+ else {
1000
+ rect.height = rect.width / aspect;
1001
+ }
1002
+
1003
+ }
1004
+ }
1005
+ else if (move_event.shiftKey) {
1006
+ // move in one direction at a time [is this backwards? ...]
1007
+ const dx = Math.abs(rect.height - origin.height);
1008
+ const dy = Math.abs(rect.width - origin.width);
1009
+
1010
+ if (dx > dy) { rect.width = origin.width; }
1011
+ else { rect.height = origin.height; }
1012
+ }
1013
+ else if (move_event.ctrlKey) {
1014
+ const point = this.ClampToGrid({
1015
+ x: rect.right, y: rect.bottom,
1016
+ });
1017
+ rect.width = point.x - rect.left + 1;
1018
+ rect.height = point.y - rect.top + 1;
1019
+ }
1020
+
1021
+ // node.style.height = (rect.height) + 'px';
1022
+ // node.style.width = (rect.width) + 'px';
1023
+
1024
+ for (const element of elements) {
1025
+ element.style.height = (rect.height) + 'px';
1026
+ element.style.width = (rect.width) + 'px';
1027
+ }
1028
+
1029
+ }, () => {
1030
+ annotation.extent = undefined; // reset
1031
+ // annotation.rect = rect.Scale(1/this.scale);
1032
+ annotation.layout = this.RectToAnnotationLayout(rect);
1033
+
1034
+ // this.grid_events.Publish({ type: 'annotation', annotation, event: 'resize' });
1035
+ resolve({ type: 'annotation', annotation, event: 'resize' });
1036
+
1037
+ });
1038
+
1039
+ return;
1040
+ }
1041
+ else {
1042
+ resolve();
1043
+ }
1044
+
1045
+ });
1046
+
1047
+
1048
+ }
1049
+
1050
+ /**
1051
+ * this used to be an abstract method for initializing. we're taking it
1052
+ * over to do some additional work post init, and renaming the subclass-specific
1053
+ * method (@see InitializeInternal).
1054
+ */
1055
+ public Initialize(container: HTMLElement, callbacks: {
1056
+ scroll: () => void,
1057
+ dropdown: (value: CellValue) => void,
1058
+ sort: (table: string, column: number, asc: boolean) => void,
1059
+ focus: () => void,
1060
+ },
1061
+ // scroll_callback: () => void,
1062
+ // dropdown_callback: (value: CellValue) => void,
1063
+ // sort_callback: (table: string, column: number, asc: boolean) => void,
1064
+ // focus_callback: () => void,
1065
+ scroll = true): void {
1066
+
1067
+ if (!this.mask.parentElement) {
1068
+ container.appendChild(this.mask);
1069
+ }
1070
+
1071
+ //if (!this.error_highlight.parentElement) {
1072
+ // container.appendChild(this.error_highlight);
1073
+ //}
1074
+
1075
+ if (!this.tooltip.parentElement) {
1076
+ container.appendChild(this.tooltip);
1077
+ }
1078
+
1079
+ // FIXME: -> instance specific, b/c trident
1080
+
1081
+ if (!this.dropdown_caret.parentElement) {
1082
+ container.appendChild(this.dropdown_caret);
1083
+ }
1084
+
1085
+ if (!this.dropdown_list.parentElement) {
1086
+ container.appendChild(this.dropdown_list);
1087
+ }
1088
+
1089
+ if (!this.note_node.parentElement) {
1090
+ container.appendChild(this.note_node);
1091
+ }
1092
+
1093
+ if (!this.sort_button.parentElement) {
1094
+ container.appendChild(this.sort_button);
1095
+ this.sort_button.addEventListener('click', () => {
1096
+
1097
+ // console.info(this.sort_button.dataset);
1098
+
1099
+ callbacks.sort(
1100
+ this.sort_button.dataset.table || '',
1101
+ Number(this.sort_button.dataset.column || '0') || 0,
1102
+ /true/i.test(this.sort_button.dataset.asc || ''));
1103
+
1104
+ this.sort_button.classList.remove('asc', 'desc');
1105
+ if (this.sort_button.dataset.asc === 'true') {
1106
+ this.sort_button.dataset.asc = 'false';
1107
+ this.sort_button.classList.add('desc');
1108
+ }
1109
+ else {
1110
+ this.sort_button.dataset.asc = 'true';
1111
+ this.sort_button.classList.add('asc');
1112
+ }
1113
+
1114
+ callbacks.focus();
1115
+
1116
+ });
1117
+ }
1118
+
1119
+ if (!this.title_node.parentElement) {
1120
+ container.appendChild(this.title_node);
1121
+ }
1122
+
1123
+ this.InitializeInternal(container, callbacks.scroll);
1124
+ if (!scroll && this.scroll_reference_node) {
1125
+ this.scroll_reference_node.style.overflow = 'hidden';
1126
+ }
1127
+
1128
+ this.dropdown_callback = callbacks.dropdown;
1129
+
1130
+ this.initialized = true;
1131
+
1132
+ }
1133
+
1134
+ /**
1135
+ * create a selection so that this node (and parents) receive
1136
+ * a copy event on ctrl+c (or any other system copy event).
1137
+ * seems to break IE, so split.
1138
+ */
1139
+ public MockSelection(): void {
1140
+
1141
+ if (!this.container) {
1142
+ return;
1143
+ }
1144
+
1145
+ // disable for IE, but leave in legacy renderer because it works
1146
+ // in safari/edge. there may be some way to fix IE... although copy
1147
+ // events aren't available, so we would have to do the fake-csv thing
1148
+ // (which I don't want to do).
1149
+
1150
+ if (this.trident) {
1151
+ return;
1152
+ }
1153
+
1154
+ // edge handles this differently than chrome/ffx. in edge, the
1155
+ // cursor does not move to the end of the selection, which is
1156
+ // what we want. so we need to fix that for edge:
1157
+
1158
+ // FIXME: limit to edge (causing problems in chrome? ...)
1159
+
1160
+ const selection = window.getSelection();
1161
+
1162
+ if (selection) {
1163
+ const range = document.createRange();
1164
+ range.selectNodeContents(this.mock_selection);
1165
+ selection.removeAllRanges();
1166
+ selection.addRange(range);
1167
+
1168
+ // selection.collapseToEnd();
1169
+ }
1170
+
1171
+ }
1172
+
1173
+ /**
1174
+ * FIXME: this is public for now but tiles should move into
1175
+ * this class, then this method can become private
1176
+ */
1177
+ public CreateTile(
1178
+ classes: string,
1179
+ size: Size,
1180
+ position: Position,
1181
+ first_cell: Position,
1182
+ cell_extent: Extent,
1183
+ pixel_start: Point,
1184
+ parent: HTMLElement,
1185
+ mark_dirty = true): Tile {
1186
+
1187
+ const tile = document.createElement('canvas') as Tile;
1188
+ tile.setAttribute('class', classes);
1189
+ tile.logical_size = size;
1190
+ tile.width = size.width * this.dpr;
1191
+ tile.height = size.height * this.dpr;
1192
+
1193
+ tile.style.width = `${size.width}px`;
1194
+ tile.style.height = `${size.height}px`;
1195
+
1196
+ tile.tile_position = position;
1197
+ tile.first_cell = first_cell;
1198
+
1199
+ this.UpdateTileGridPosition(tile);
1200
+
1201
+ tile.last_cell = {
1202
+ row: first_cell.row + cell_extent.rows - 1,
1203
+ column: first_cell.column + cell_extent.columns - 1,
1204
+ };
1205
+ tile.pixel_start = pixel_start;
1206
+ tile.pixel_end = {
1207
+ x: pixel_start.x + size.width,
1208
+ y: pixel_start.y + size.height,
1209
+ };
1210
+ tile.dirty = !!mark_dirty;
1211
+ tile.needs_full_repaint = true; // never painted
1212
+
1213
+ parent.appendChild(tile);
1214
+
1215
+ // NOTE re: text rendering. you can't use baseline = top, because that's
1216
+ // inconsistent among browsers. in fact of all baselines, the only ones that
1217
+ // are even close are alphabetic and bottom -- bottom is slightly different
1218
+ // in ffx compared to chrome and edge, but that could be because of different
1219
+ // font rendering schemes. alphabetic is the closest, but requires offset for
1220
+ // ascender (or descender).
1221
+
1222
+ // actually it looks like there's a 1px difference in bottom baseline...
1223
+ // alphabetic is the only one that's consistent.
1224
+
1225
+ // FIXME: why not just offset on a per-browser basis? it might be ugly
1226
+ // but it's simpler.
1227
+
1228
+ // for the time being we will use bottom.
1229
+
1230
+ // why were we prepainting (because firefox, below?) and why was this so
1231
+ // slow? do we need to preset the context text parameters?
1232
+
1233
+ /*
1234
+ const context = tile.getContext('2d', {alpha: false});
1235
+
1236
+ if (context) {
1237
+ context.textAlign = 'left';
1238
+ context.textBaseline = 'alphabetic';
1239
+
1240
+ // prepaint -- firefox is a little slow so flashes empty tiles sometimes
1241
+
1242
+ // context.fillStyle = '#fff'; // FIXME: use theme color
1243
+ // context.fillRect(0, 0, tile.width, tile.height);
1244
+ }
1245
+ */
1246
+
1247
+ return tile;
1248
+
1249
+ }
1250
+
1251
+ /**
1252
+ * applies theme to nodes, as necessary
1253
+ */
1254
+ public ApplyTheme(theme: Theme): void {
1255
+ this.row_header.style.backgroundColor =
1256
+ this.column_header.style.backgroundColor =
1257
+ this.corner.style.backgroundColor =
1258
+ theme.headers?.fill ? ThemeColor(theme, theme.headers.fill) : '';
1259
+
1260
+ // theme.headers?.background || '';
1261
+ // theme.header_background_color || ''; // this.theme.header_background;
1262
+
1263
+ this.corner.style.borderColor =
1264
+ theme.grid_color || ''; // this.theme.header_border_color;
1265
+ // this.row_header.style.backgroundColor = this.theme.header_background;
1266
+ // this.column_header.style.backgroundColor = this.theme.header_background;
1267
+
1268
+ for (const row of this.grid_tiles) {
1269
+ for (const tile of row) {
1270
+ tile.style.backgroundColor = ThemeColor(theme, theme.grid_cell?.fill) || '#fff';
1271
+ }
1272
+ }
1273
+
1274
+ /*
1275
+ this.tooltip.style.fontFamily = theme.tooltip_font_face || '';
1276
+ this.tooltip.style.fontSize = theme.tooltip_font_size ? `${theme.tooltip_font_size}pt` : '';
1277
+ this.tooltip.style.backgroundColor = theme.tooltip_background || '';
1278
+ this.tooltip.style.borderColor = theme.tooltip_background || ''; // for arrow
1279
+ this.tooltip.style.color = theme.tooltip_color || '';
1280
+ */
1281
+
1282
+ // TODO: dropdown caret
1283
+
1284
+ // this.dropdown_list.style.fontFamily = theme.cell_font || '';
1285
+ // const font_size = (theme.cell_font_size_value || 10) * this.scale;
1286
+ // this.dropdown_list.style.fontSize = (font_size) + (theme.cell_font_size_unit || 'pt');
1287
+ this.dropdown_list.style.font = Style.Font(theme.grid_cell || {});
1288
+
1289
+ }
1290
+
1291
+ public UpdateTotalSize(): void {
1292
+
1293
+ this.total_height = 0;
1294
+ const rows = this.view.active_sheet.rows;
1295
+ for (let i = 0; i < rows; i++) {
1296
+ this.total_height += this.RowHeight(i);
1297
+ }
1298
+
1299
+ this.total_width = 0;
1300
+ const columns = this.view.active_sheet.columns;
1301
+ for (let i = 0; i < columns; i++) {
1302
+ this.total_width += this.ColumnWidth(i);
1303
+ }
1304
+
1305
+ }
1306
+
1307
+
1308
+ public UpdateContentsSize(): void {
1309
+
1310
+ const height = this.row_header_tiles.reduce((a, tile) => a + tile.logical_size.height, 0);
1311
+ const width = this.column_header_tiles.reduce((a, tile) => a + tile.logical_size.width, 0);
1312
+
1313
+ this.column_header.style.width = this.contents.style.width = `${width}px`;
1314
+ this.row_header.style.height = this.contents.style.height = `${height}px`;
1315
+
1316
+ }
1317
+
1318
+ /** hides column/row resize tooltip and removes any specific classes */
1319
+ public HideTooltip(): void {
1320
+ this.tooltip.style.display = 'none';
1321
+ this.tooltip_state = undefined;
1322
+ this.tooltip.classList.remove('arrow-up');
1323
+ this.tooltip.classList.remove('arrow-left');
1324
+ }
1325
+
1326
+ /*
1327
+
1328
+ highlight error removed in favor of container errors, event reporting
1329
+
1330
+ * briefly flash red, to indicate an error * /
1331
+ public HighlightError(address: ICellAddress): void {
1332
+
1333
+ const target_rect = this.OffsetCellAddressToRectangle(address).Shift(
1334
+ this.header_size.width, this.header_size.height);
1335
+
1336
+ target_rect.ApplyStyle(this.error_highlight);
1337
+ this.error_highlight.style.opacity = '1';
1338
+
1339
+ // we don't like to rely on transitionend events. the concern is that
1340
+ // if they overlap eventually one will get lost... because this can be
1341
+ // triggered faster than the transition, we can almost always make that
1342
+ // happen
1343
+
1344
+ if (this.error_highlight_timeout) {
1345
+ clearTimeout(this.error_highlight_timeout);
1346
+ }
1347
+
1348
+ this.error_highlight_timeout = setTimeout(() => {
1349
+ this.error_highlight.style.opacity = '0';
1350
+ this.error_highlight_timeout = undefined;
1351
+ }, 250)
1352
+
1353
+ }
1354
+ */
1355
+
1356
+ /** show column/row resize tooltip */
1357
+ public ShowTooltip(options: TooltipOptions = {}): void {
1358
+ if (options.up) {
1359
+ this.tooltip.classList.add('arrow-up');
1360
+ this.tooltip_state = 'up';
1361
+ }
1362
+ else if (options.left) {
1363
+ this.tooltip.classList.add('arrow-left');
1364
+ this.tooltip_state = 'left';
1365
+ }
1366
+ this.tooltip.style.display = 'block';
1367
+ this.UpdateTooltip(options);
1368
+ }
1369
+
1370
+ public ShowDropdownCaret(area: Area, list: CellValue[], current: CellValue): void {
1371
+
1372
+ let target_rect = this.OffsetCellAddressToRectangle(area.start);
1373
+
1374
+ if (area.count > 1) {
1375
+ target_rect = target_rect.Combine(this.OffsetCellAddressToRectangle(area.end));
1376
+ }
1377
+
1378
+ target_rect = target_rect.Shift(
1379
+ this.header_size.width, this.header_size.height);
1380
+
1381
+ // FIXME: max size? (...)
1382
+
1383
+ const height = Math.round(this.scale * Math.max(8, Math.min(20, target_rect.height)));
1384
+
1385
+ this.dropdown_caret.style.height = `${height}px`;
1386
+ this.dropdown_caret.style.width = `${height}px`;
1387
+ this.dropdown_caret.style.left = `${target_rect.right + 1}px`;
1388
+ this.dropdown_caret.style.top = `${target_rect.bottom - height}px`;
1389
+
1390
+ this.dropdown_list.style.top = `${target_rect.bottom + 2}px`;
1391
+ this.dropdown_list.style.left = `${target_rect.left + 2}px`;
1392
+ this.dropdown_list.style.minWidth = `${target_rect.width}px`;
1393
+
1394
+ this.dropdown_list.textContent = '';
1395
+ for (const value of list) {
1396
+ const entry = DOMUtilities.CreateDiv(undefined, this.dropdown_list);
1397
+ if (current === value) {
1398
+ this.dropdown_selected = entry;
1399
+ entry.classList.add('selected');
1400
+ }
1401
+ (entry as any).dropdown_value = value;
1402
+ entry.textContent = value?.toString() || '';
1403
+ }
1404
+
1405
+ //this.dropdown_caret.classList.remove('active');
1406
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
1407
+
1408
+ this.dropdown_caret.style.display = 'block';
1409
+ this.dropdown_caret_visible = true;
1410
+ }
1411
+
1412
+ public HideDropdownCaret(): void {
1413
+ if (this.dropdown_caret_visible) {
1414
+ // this.dropdown_caret.classList.remove('active');
1415
+ this.dropdown_caret.setAttribute('class', 'treb-dropdown-caret');
1416
+ this.dropdown_caret_visible = false;
1417
+ this.dropdown_caret.style.display = 'none';
1418
+ }
1419
+ }
1420
+
1421
+ /**
1422
+ * this method returns the scroll offset adjusted for headers.
1423
+ * if you just want the raw scroll offset, use the accessor.
1424
+ *
1425
+ * @param offset_headers
1426
+ * @returns
1427
+ */
1428
+ public GetScrollOffset(): Point {
1429
+ return {
1430
+ x: this.scroll_reference_node.scrollLeft + this.header_offset.x,
1431
+ y: this.scroll_reference_node.scrollTop + this.header_offset.y,
1432
+ };
1433
+ }
1434
+
1435
+ public ScrollTo(address: ICellAddress, x = true, y = true, smooth = false): void {
1436
+ const target_rect = this.CellAddressToRectangle(address);
1437
+
1438
+ if (smooth && !!this.scroll_reference_node.scrollTo) {
1439
+
1440
+ const current = {
1441
+ left: this.scroll_reference_node.scrollLeft,
1442
+ top: this.scroll_reference_node.scrollTop,
1443
+ };
1444
+
1445
+ const options: ScrollToOptions = {
1446
+ left: x ? target_rect.left : current.left,
1447
+ top: y ? target_rect.top : current.top,
1448
+ behavior: 'smooth',
1449
+ }
1450
+
1451
+ this.scroll_reference_node.scrollTo(options);
1452
+
1453
+ }
1454
+ else {
1455
+ if (y) {
1456
+ this.scroll_reference_node.scrollTop = target_rect.top;
1457
+ }
1458
+ if (x) {
1459
+ this.scroll_reference_node.scrollLeft = target_rect.left;
1460
+ }
1461
+ }
1462
+ }
1463
+
1464
+ /**
1465
+ * scroll address into view, at top-left or bottom-right depending on
1466
+ * target and current position. also offsets for frozen rows, columns.
1467
+ */
1468
+ public ScrollIntoView(address: ICellAddress): void {
1469
+
1470
+ const target_rect = this.CellAddressToRectangle(address);
1471
+
1472
+ const width = this.scroll_reference_node.clientWidth - this.row_header.offsetWidth;
1473
+ const height = this.scroll_reference_node.clientHeight - this.column_header.offsetHeight;
1474
+
1475
+ const offset = { x: 0, y: 0 };
1476
+ const lock = { x: false, y: false };
1477
+
1478
+ const viewport = new Rectangle(
1479
+ this.scroll_reference_node.scrollLeft,
1480
+ this.scroll_reference_node.scrollTop,
1481
+ width, height);
1482
+
1483
+ // if there are frozen rows/columns, we need to scroll such that the
1484
+ // cell is visible outside of the frozen area. but only if we're *outside*
1485
+ // the frozen area, because otherwise we're on screen essentially by default.
1486
+
1487
+ if (this.view.active_sheet.freeze.rows || this.view.active_sheet.freeze.columns) {
1488
+ if (this.view.active_sheet.freeze.rows && address.row >= this.view.active_sheet.freeze.rows) {
1489
+ offset.y = this.frozen_row_tiles[0].logical_size.height;
1490
+ }
1491
+ else if (this.view.active_sheet.freeze.rows) lock.y = true;
1492
+
1493
+ if (this.view.active_sheet.freeze.columns && address.column >= this.view.active_sheet.freeze.columns) {
1494
+ offset.x = this.frozen_column_tiles[0].logical_size.width;
1495
+ }
1496
+ else if (this.view.active_sheet.freeze.columns) lock.x = true;
1497
+ }
1498
+
1499
+ // NOTE: in theory it's possible we scroll twice, which would result
1500
+ // in two scroll events. however in practice this is called on key events,
1501
+ // so it's unlikely.
1502
+
1503
+ if (address.row !== Infinity) {
1504
+ if (target_rect.top < viewport.top + offset.y && !lock.y) {
1505
+ this.scroll_reference_node.scrollTop = target_rect.top - offset.y;
1506
+ }
1507
+ else if (target_rect.bottom > viewport.bottom) {
1508
+ this.scroll_reference_node.scrollTop = target_rect.bottom - height;
1509
+ }
1510
+ }
1511
+
1512
+ if (address.column !== Infinity) {
1513
+ if (target_rect.left < viewport.left + offset.x && !lock.x) {
1514
+ this.scroll_reference_node.scrollLeft = target_rect.left - offset.x;
1515
+ }
1516
+ else if (target_rect.right > viewport.right) {
1517
+ this.scroll_reference_node.scrollLeft = target_rect.right - width;
1518
+ }
1519
+ }
1520
+
1521
+ }
1522
+
1523
+ public UpdateTooltip(options: TooltipOptions = {}): void {
1524
+ if (typeof options.text !== 'undefined') {
1525
+ this.tooltip.textContent = options.text;
1526
+ }
1527
+ if (typeof options.x !== 'undefined') {
1528
+ let x = options.x || 0;
1529
+ if (this.tooltip_state === 'up') {
1530
+ x -= this.tooltip.offsetWidth / 2;
1531
+ }
1532
+ this.tooltip.style.left = Math.round(x) + 'px';
1533
+ }
1534
+ if (typeof options.y !== 'undefined') {
1535
+ let y = options.y || 0;
1536
+ if (this.tooltip_state === 'left') {
1537
+ y -= this.tooltip.offsetHeight / 2;
1538
+ }
1539
+ this.tooltip.style.top = Math.round(y) + 'px';
1540
+ }
1541
+ }
1542
+
1543
+
1544
+ /**
1545
+ * y coordinate to row header. for consistency we return an address.
1546
+ */
1547
+ public CoordinateToRowHeader(y: number): ICellAddress {
1548
+ const result = { column: Infinity, row: 0 };
1549
+
1550
+ if (this.view.active_sheet.freeze.rows &&
1551
+ this.frozen_row_tiles[0].pixel_end.y >= y - this.scroll_reference_node.scrollTop) {
1552
+
1553
+ let height = 0;
1554
+ y -= this.scroll_reference_node.scrollTop;
1555
+
1556
+ for (let i = 0; i < this.view.active_sheet.freeze.rows; i++) {
1557
+ height += this.RowHeight(i);
1558
+ if (height >= y) {
1559
+ result.row = i;
1560
+ return result;
1561
+ }
1562
+ }
1563
+
1564
+ }
1565
+
1566
+ for (const tile of this.row_header_tiles) {
1567
+ if (tile.pixel_end.y >= y) {
1568
+
1569
+ // now map within the tile
1570
+ let top = y - tile.pixel_start.y;
1571
+ let height = 0;
1572
+
1573
+ result.row = tile.first_cell.row;
1574
+ for (; result.row <= tile.last_cell.row; result.row++, top -= height) {
1575
+ height = this.RowHeight(result.row);
1576
+ if (height > top) {
1577
+ return result;
1578
+ }
1579
+ }
1580
+
1581
+ return result;
1582
+ }
1583
+ }
1584
+ return result;
1585
+
1586
+ }
1587
+
1588
+ /**
1589
+ * x coordinate to colum header. for consistency we return an address.
1590
+ */
1591
+ public CoordinateToColumnHeader(x: number): ICellAddress {
1592
+ const result = { row: Infinity, column: 0 };
1593
+
1594
+ if (this.view.active_sheet.freeze.columns &&
1595
+ this.frozen_column_tiles[0].pixel_end.x >= x - this.scroll_reference_node.scrollLeft) {
1596
+
1597
+ let width = 0;
1598
+ x -= this.scroll_reference_node.scrollLeft;
1599
+
1600
+ for (let i = 0; i < this.view.active_sheet.freeze.columns; i++) {
1601
+ width += this.ColumnWidth(i);
1602
+ if (width >= x) {
1603
+ result.column = i;
1604
+ return result;
1605
+ }
1606
+ }
1607
+
1608
+ }
1609
+
1610
+ for (const tile of this.column_header_tiles) {
1611
+ if (tile.pixel_end.x >= x) {
1612
+
1613
+ // now map within the tile
1614
+ let left = x - tile.pixel_start.x;
1615
+ let width = 0;
1616
+
1617
+ result.column = tile.first_cell.column;
1618
+
1619
+ for (; result.column <= tile.last_cell.column; result.column++, left -= width) {
1620
+ width = this.ColumnWidth(result.column);
1621
+ if (width > left) return result;
1622
+ }
1623
+
1624
+ return result;
1625
+ }
1626
+ }
1627
+ return result;
1628
+
1629
+ }
1630
+
1631
+ /**
1632
+ * point to cell address (grid only)
1633
+ *
1634
+ * FIXME: implement cap_maximum parameter (not sure where we would need it)
1635
+ */
1636
+ public PointToAddress_Grid(point: Point, cap_maximum = false, offset_freeze = true): ICellAddress {
1637
+
1638
+ // offset for freeze pane
1639
+
1640
+ if (offset_freeze) {
1641
+
1642
+ if (this.view.active_sheet.freeze.rows) {
1643
+ const frozen_height = this.frozen_row_tiles[0].logical_size.height;
1644
+ if (point.y - this.scroll_reference_node.scrollTop < frozen_height) {
1645
+ point.y -= this.scroll_reference_node.scrollTop;
1646
+ }
1647
+ }
1648
+
1649
+ if (this.view.active_sheet.freeze.columns) {
1650
+ const frozen_width = this.frozen_column_tiles[0].logical_size.width;
1651
+ if (point.x - this.scroll_reference_node.scrollLeft < frozen_width) {
1652
+ point.x -= this.scroll_reference_node.scrollLeft;
1653
+ }
1654
+ }
1655
+
1656
+ }
1657
+
1658
+ // we used to find the containing tile and then calculate row and column.
1659
+ // to support overflow, we now have two separate loops.
1660
+
1661
+ const result = {
1662
+ row: 0,
1663
+ column: 0,
1664
+ };
1665
+
1666
+ // FIXME: these could be cached when created
1667
+
1668
+ const last_column = this.grid_tiles[this.grid_tiles.length - 1];
1669
+ const last_tile = last_column[last_column.length - 1];
1670
+
1671
+ // ---- find row -----------------------------------------------------------
1672
+
1673
+ if (point.y > last_tile.pixel_end.y) {
1674
+
1675
+ // overflow case
1676
+
1677
+ let top = point.y - last_tile.pixel_end.y;
1678
+ result.row = last_tile.last_cell.row;
1679
+
1680
+ while (top > 0) {
1681
+ result.row++;
1682
+ top -= this.default_row_height;
1683
+ }
1684
+
1685
+ }
1686
+ else {
1687
+
1688
+ // normal behavior
1689
+
1690
+ for (const cell of last_column) {
1691
+ if (cell.pixel_start.y <= point.y && cell.pixel_end.y >= point.y) {
1692
+
1693
+ let top = point.y - cell.pixel_start.y;
1694
+ let height = 0;
1695
+
1696
+ result.row = cell.first_cell.row;
1697
+
1698
+ for (; result.row <= cell.last_cell.row; result.row++, top -= height) {
1699
+ height = this.RowHeight(result.row);
1700
+ if (height > top) {
1701
+ break;
1702
+ }
1703
+ }
1704
+
1705
+ break;
1706
+ }
1707
+ }
1708
+ }
1709
+
1710
+ // ---- find column --------------------------------------------------------
1711
+
1712
+ if (point.x > last_tile.pixel_end.x) {
1713
+
1714
+ // overflow case
1715
+
1716
+ let left = point.x - last_tile.pixel_end.x;
1717
+ result.column = last_tile.last_cell.column;
1718
+
1719
+ while (left > 0) {
1720
+ result.column++;
1721
+ left -= this.default_column_width;
1722
+ }
1723
+
1724
+ }
1725
+ else {
1726
+
1727
+ // normal behavior
1728
+
1729
+ for (const column of this.grid_tiles) {
1730
+ if (column[0].pixel_start.x <= point.x && column[0].pixel_end.x >= point.x) {
1731
+
1732
+ const cell = column[0];
1733
+
1734
+ let left = point.x - cell.pixel_start.x;
1735
+ let width = 0;
1736
+
1737
+ result.column = cell.first_cell.column;
1738
+
1739
+ for (; result.column <= cell.last_cell.column; result.column++, left -= width) {
1740
+ width = this.ColumnWidth(result.column);
1741
+ if (width > left) {
1742
+ break;
1743
+ }
1744
+ }
1745
+
1746
+ break;
1747
+ }
1748
+ }
1749
+ }
1750
+
1751
+ return result;
1752
+
1753
+ }
1754
+
1755
+ /**
1756
+ * get an adjacent tile. this is used by the renderer when a merge or
1757
+ * overflow runs out of the painted tile, and we need to paint it.
1758
+ */
1759
+ public AdjacentTile(tile: Tile, row_offset = 0, column_offset = 0): Tile|undefined {
1760
+
1761
+ if (!row_offset && !column_offset) {
1762
+ return tile;
1763
+ }
1764
+
1765
+ const position = tile.tile_position;
1766
+
1767
+ const row = tile.tile_position.row + row_offset;
1768
+ const column = tile.tile_position.column + column_offset;
1769
+
1770
+ if (row < 0 || column < 0) return undefined;
1771
+
1772
+ // check various stores for match
1773
+
1774
+ if (this.grid_tiles[position.column] && this.grid_tiles[position.column][position.row] === tile) {
1775
+ if (this.grid_tiles[column]) return this.grid_tiles[column][row];
1776
+ }
1777
+
1778
+ if (!position.column && this.frozen_column_tiles[position.row] === tile) {
1779
+ return this.frozen_column_tiles[row];
1780
+ }
1781
+
1782
+ if (!position.row && this.frozen_row_tiles[position.column] === tile) {
1783
+ return this.frozen_row_tiles[column];
1784
+ }
1785
+
1786
+ return undefined;
1787
+
1788
+ }
1789
+
1790
+ public UpdateTiles(): void {
1791
+
1792
+ // so the new layout uses variable-sized tiles, which are sized
1793
+ // to a number of rows/columns (FIXME: nearest to a given size?)
1794
+ // that way we don't have to worry about overlap, and resizing
1795
+ // is much easier.
1796
+
1797
+ // note that this doesn't mean there isn't overlapping rendering,
1798
+ // because there will be on merges.
1799
+
1800
+ if (!this.container) throw new Error('invalid container');
1801
+
1802
+ // flush... FIXME: why not reuse? maybe more trouble than it's worth?
1803
+
1804
+ this.grid_tiles.forEach((arr) => {
1805
+ arr.forEach((tile) => {
1806
+ if (tile.parentElement) {
1807
+ tile.parentElement.removeChild(tile);
1808
+ }
1809
+ });
1810
+ });
1811
+
1812
+ for (const tileset of [
1813
+ this.column_header_tiles,
1814
+ this.row_header_tiles,
1815
+ this.frozen_row_tiles,
1816
+ this.frozen_column_tiles,
1817
+ ]) {
1818
+ for (const tile of tileset) {
1819
+ if (tile.parentElement) {
1820
+ tile.parentElement.removeChild(tile);
1821
+ }
1822
+ }
1823
+ }
1824
+
1825
+ /*
1826
+ this.column_header_tiles.forEach((tile) => {
1827
+ if (tile.parentElement) {
1828
+ tile.parentElement.removeChild(tile);
1829
+ }
1830
+ });
1831
+
1832
+ this.row_header_tiles.forEach((tile) => {
1833
+ if (tile.parentElement) {
1834
+ tile.parentElement.removeChild(tile);
1835
+ }
1836
+ });
1837
+ */
1838
+
1839
+ this.frozen_row_tiles = [];
1840
+ this.frozen_column_tiles = [];
1841
+ this.row_header_tiles = [];
1842
+ this.column_header_tiles = [];
1843
+ this.grid_tiles = [];
1844
+
1845
+ // update local references (scaled). this has to be done before layout.
1846
+
1847
+ const sheet = this.view.active_sheet;
1848
+
1849
+ this.default_row_height = Math.round(sheet.default_row_height * this.scale);
1850
+ this.default_column_width = Math.round(sheet.default_column_width * this.scale);
1851
+
1852
+ this.header_offset = {
1853
+ x: Math.round(sheet.header_offset.x * this.scale),
1854
+ y: Math.round(sheet.header_offset.y * this.scale),
1855
+ };
1856
+
1857
+ this.UpdateContainingGrid();
1858
+
1859
+ let rows = this.view.active_sheet.rows;
1860
+ let columns = this.view.active_sheet.columns;
1861
+
1862
+ if (!rows) rows = 100;
1863
+ if (!columns) columns = 40;
1864
+
1865
+ // get total size of the grid from sheet
1866
+
1867
+ let total_height = 0;
1868
+ let total_width = 0;
1869
+
1870
+ for (let i = 0; i < rows; i++) {
1871
+ total_height += this.RowHeight(i);
1872
+ }
1873
+
1874
+ for (let i = 0; i < columns; i++) {
1875
+ total_width += this.ColumnWidth(i);
1876
+ }
1877
+
1878
+ if (!total_width || !total_height) {
1879
+ throw ('unexpected missing total size');
1880
+ }
1881
+
1882
+ // console.info(`${rows} rows; total size: ${total_width} x ${total_height}`);
1883
+
1884
+ if (!total_height) total_height = this.default_row_height * rows;
1885
+ if (!total_width) total_width = this.default_column_width * columns;
1886
+
1887
+ if (this.container.clientWidth > total_width + this.header_size.width) {
1888
+ const add_columns = Math.ceil((this.container.offsetWidth - total_width) /
1889
+ this.default_column_width);
1890
+
1891
+ total_width += add_columns * this.default_column_width;
1892
+ columns += add_columns;
1893
+ }
1894
+
1895
+ this.last_column = columns;
1896
+
1897
+ // FIXME: header size should be scaled?
1898
+
1899
+ if (this.container.clientHeight > total_height + this.header_size.height) {
1900
+ const add_rows = Math.ceil((this.container.offsetHeight - total_height) /
1901
+ this.default_row_height);
1902
+
1903
+ total_height += add_rows * this.default_row_height;
1904
+ rows += add_rows;
1905
+ }
1906
+
1907
+ // console.info(this.container.offsetWidth, this.container.offsetHeight)
1908
+ // console.info('total size:', total_width, ', ', total_height);
1909
+
1910
+ // update node sizes to match
1911
+
1912
+ this.column_header.style.width = this.contents.style.width = `${total_width}px`;
1913
+ this.row_header.style.height = this.contents.style.height = `${total_height}px`;
1914
+
1915
+ // generate a set of tiles given an approximate target size.
1916
+ // keep track of the tile width/height and the starting column/row.
1917
+
1918
+ // rows:
1919
+
1920
+ const tile_widths: number[] = [];
1921
+ const tile_columns: number[] = [];
1922
+
1923
+ let tile_width = 0;
1924
+ let tile_column = 0;
1925
+
1926
+ for (let c = 0; c < columns; c++) {
1927
+ const column_width = this.ColumnWidth(c);
1928
+ if (tile_width && tile_width + column_width > this.default_tile_size.width) {
1929
+
1930
+ // push and move to the next tile, starting with this column
1931
+ tile_widths.push(tile_width);
1932
+ tile_columns.push(tile_column);
1933
+
1934
+ // for the next one, start here
1935
+ tile_column = c;
1936
+ tile_width = 0;
1937
+ }
1938
+ tile_width += column_width;
1939
+ }
1940
+
1941
+ // last one
1942
+ tile_widths.push(tile_width);
1943
+ tile_columns.push(tile_column);
1944
+
1945
+ // columns:
1946
+
1947
+ const tile_heights: number[] = [];
1948
+ const tile_rows: number[] = [];
1949
+
1950
+ let tile_height = 0;
1951
+ let tile_row = 0;
1952
+
1953
+ for (let r = 0; r < rows; r++) {
1954
+ const row_height = this.RowHeight(r);
1955
+ if (tile_height && tile_height + row_height > this.default_tile_size.height) {
1956
+ tile_heights.push(tile_height);
1957
+ tile_rows.push(tile_row);
1958
+
1959
+ tile_height = 0;
1960
+ tile_row = r;
1961
+ }
1962
+ tile_height += row_height;
1963
+ }
1964
+
1965
+ tile_heights.push(tile_height);
1966
+ tile_rows.push(tile_row);
1967
+
1968
+ // now create tiles and set metadata
1969
+
1970
+ const column_tile_count = tile_widths.length;
1971
+ const row_tile_count = tile_heights.length;
1972
+
1973
+ let pixel_y = 0;
1974
+ let pixel_x = 0;
1975
+
1976
+ let header_height = 0;
1977
+ let header_width = 0;
1978
+
1979
+ for (let i = 0; i < this.view.active_sheet.freeze.rows; i++) {
1980
+ header_height += this.RowHeight(i);
1981
+ }
1982
+ for (let i = 0; i < this.view.active_sheet.freeze.columns; i++) {
1983
+ header_width += this.ColumnWidth(i);
1984
+ }
1985
+
1986
+ for (let c = 0; c < column_tile_count; c++) {
1987
+ const column: Tile[] = [];
1988
+
1989
+ pixel_y = 0; // reset
1990
+
1991
+ const column_extent = (c === column_tile_count - 1) ?
1992
+ columns - tile_columns[c] :
1993
+ tile_columns[c + 1] - tile_columns[c];
1994
+
1995
+ // create a column header tile for this column
1996
+ this.column_header_tiles.push(this.CreateTile('column-header-tile',
1997
+ { height: this.header_offset.y, width: tile_widths[c] },
1998
+ { row: 0, column: c },
1999
+ { row: 0, column: tile_columns[c] },
2000
+ { rows: 0, columns: column_extent },
2001
+ { x: pixel_x, y: 0 },
2002
+ this.column_header));
2003
+
2004
+ // also frozen
2005
+ if (this.view.active_sheet.freeze.rows) {
2006
+ this.frozen_row_tiles.push(this.CreateTile('frozen-row-tile',
2007
+ { height: header_height, width: tile_widths[c] },
2008
+ { row: 1, column: c },
2009
+ { row: 0, column: tile_columns[c] },
2010
+ { rows: 0, columns: column_extent },
2011
+ { x: pixel_x, y: 0 },
2012
+ this.column_header));
2013
+ }
2014
+
2015
+ // loop over rows
2016
+ for (let r = 0; r < row_tile_count; r++) {
2017
+
2018
+ const row_extent = (r === row_tile_count - 1) ?
2019
+ rows - tile_rows[r] :
2020
+ tile_rows[r + 1] - tile_rows[r];
2021
+
2022
+ // first column, create header
2023
+ if (!c) {
2024
+ this.row_header_tiles.push(this.CreateTile('row-header-tile',
2025
+ { height: tile_heights[r], width: this.header_offset.x },
2026
+ { row: r, column: 0 },
2027
+ { row: tile_rows[r], column: 0 }, // first cell
2028
+ { rows: row_extent, columns: 1 },
2029
+ { x: 0, y: pixel_y },
2030
+ this.row_header));
2031
+
2032
+ // also frozen
2033
+ if (this.view.active_sheet.freeze.columns) {
2034
+ this.frozen_column_tiles.push(this.CreateTile('frozen-column-tile',
2035
+ { height: tile_heights[r], width: header_width },
2036
+ { row: r, column: 1 },
2037
+ { row: tile_rows[r], column: 0 },
2038
+ { rows: row_extent, columns: 1 },
2039
+ { x: 0, y: pixel_y },
2040
+ this.row_header));
2041
+ }
2042
+ }
2043
+
2044
+ column.push(this.CreateTile('grid-tile',
2045
+ { height: tile_heights[r], width: tile_widths[c] }, // tile size in pixels
2046
+ { row: r, column: c }, // grid position
2047
+ { row: tile_rows[r], column: tile_columns[c] }, // first cell
2048
+ { rows: row_extent, columns: column_extent }, // extent
2049
+ { x: pixel_x, y: pixel_y },
2050
+ this.contents));
2051
+
2052
+ pixel_y += tile_heights[r];
2053
+
2054
+ }
2055
+ this.grid_tiles.push(column);
2056
+
2057
+ pixel_x += tile_widths[c];
2058
+
2059
+ }
2060
+
2061
+ this.total_height = total_height;
2062
+ this.total_width = total_width;
2063
+
2064
+ this.ClearLayoutCaches();
2065
+
2066
+ this.UpdateGridTemplates(true, true);
2067
+
2068
+ }
2069
+
2070
+ public ClearLayoutCaches(): void {
2071
+ this.row_cache = [];
2072
+ this.column_cache = [];
2073
+ }
2074
+
2075
+ /**
2076
+ * returns the tile index for a given column. this is used to map
2077
+ * a column to a tile in either the header or the grid.
2078
+ * FIXME: speed up w/ lookup cache
2079
+ */
2080
+ public TileIndexForColumn(column: number): number {
2081
+ for (const tile of this.column_header_tiles) {
2082
+ if (tile.first_cell.column <= column && tile.last_cell.column >= column) {
2083
+ return tile.tile_position.column;
2084
+ }
2085
+ }
2086
+ return -1;
2087
+ }
2088
+
2089
+ /**
2090
+ * returns the tile index for a given row. this is used to map
2091
+ * a column to a tile in either the header or the grid.
2092
+ * FIXME: speed up w/ lookup cache
2093
+ */
2094
+ public TileIndexForRow(row: number): number {
2095
+ for (const tile of this.row_header_tiles) {
2096
+ if (tile.first_cell.row <= row && tile.last_cell.row >= row) {
2097
+ return tile.tile_position.row;
2098
+ }
2099
+ }
2100
+ return -1;
2101
+ }
2102
+
2103
+ /**
2104
+ * marks header tiles as dirty, for next repaint call
2105
+ *
2106
+ * UPDATE fix for entire column/row/sheet (the Intersects() method
2107
+ * doesn't support infinities, for some reason: FIX THAT)
2108
+ */
2109
+ public DirtyHeaders(area?: Area): void {
2110
+
2111
+ if (!area) return;
2112
+
2113
+ // FIXME: visible only?
2114
+ // why, who cares? render should be based on visible, not dirty
2115
+
2116
+ for (const tile of this.column_header_tiles) {
2117
+ if (tile.dirty) continue;
2118
+ const test: Area = new Area(
2119
+ { row: area.start.row, column: tile.first_cell.column },
2120
+ { row: area.start.row, column: tile.last_cell.column },
2121
+ );
2122
+ if (area.entire_row || test.Intersects(area)) {
2123
+ tile.dirty = true;
2124
+ }
2125
+ }
2126
+
2127
+ for (const tile of this.row_header_tiles) {
2128
+ if (tile.dirty) continue;
2129
+ const test: Area = new Area(
2130
+ { column: area.start.column, row: tile.first_cell.row },
2131
+ { column: area.start.column, row: tile.last_cell.row },
2132
+ );
2133
+ if (area.entire_column || test.Intersects(area)) {
2134
+ tile.dirty = true;
2135
+ }
2136
+ }
2137
+
2138
+ }
2139
+
2140
+ public DirtyAll(): void {
2141
+ for (const column of this.grid_tiles) {
2142
+ for (const tile of column) {
2143
+ tile.dirty = true;
2144
+ }
2145
+ }
2146
+ }
2147
+
2148
+ public DirtyArea(area: Area): void {
2149
+
2150
+ if (!this.initialized) return;
2151
+
2152
+ const start = { row: 0, column: 0 };
2153
+ const end = { row: this.grid_tiles[0].length - 1, column: this.grid_tiles.length - 1 };
2154
+
2155
+ if (area.start.column !== Infinity) {
2156
+ start.column = end.column = this.TileIndexForColumn(area.start.column);
2157
+ if (area.end.column !== area.start.column) end.column = this.TileIndexForColumn(area.end.column);
2158
+ }
2159
+ if (area.start.row !== Infinity) {
2160
+ start.row = end.row = this.TileIndexForRow(area.start.row);
2161
+ if (area.end.row !== area.start.row) end.row = this.TileIndexForRow(area.end.row);
2162
+ }
2163
+ for (let column = start.column; column <= end.column; column++) {
2164
+ for (let row = start.row; row <= end.row; row++) {
2165
+ this.grid_tiles[column][row].dirty = true;
2166
+ }
2167
+ }
2168
+
2169
+ }
2170
+
2171
+ /** calculate first visible tile based on scroll position */
2172
+ public VisibleTiles(): TileRange {
2173
+
2174
+ const tiles: Position[] = [{ row: 0, column: 0 }, { row: 0, column: 0 }];
2175
+ if (!this.container || !this.grid_tiles.length || !this.grid_tiles[0].length) {
2176
+ return new TileRange(tiles[0], tiles[1]); // too much?
2177
+ }
2178
+
2179
+ const left = this.scroll_reference_node.scrollLeft;
2180
+ const right = left + this.scroll_reference_node.offsetWidth;
2181
+ const top = this.scroll_reference_node.scrollTop;
2182
+ const bottom = top + this.scroll_reference_node.offsetHeight;
2183
+
2184
+ for (const column of this.grid_tiles) {
2185
+ let cell = column[0];
2186
+ if (cell.pixel_start.x <= left && cell.pixel_end.x >= left) {
2187
+ for (cell of column) {
2188
+ if (cell.pixel_start.y <= top && cell.pixel_end.y >= top) {
2189
+ tiles[0] = cell.tile_position;
2190
+ break;
2191
+ }
2192
+ }
2193
+ }
2194
+ if (column === this.grid_tiles[this.grid_tiles.length - 1] ||
2195
+ cell.pixel_start.x <= right && cell.pixel_end.x >= right) {
2196
+ for (cell of column) {
2197
+ if (cell === column[column.length - 1] ||
2198
+ cell.pixel_start.y <= bottom && cell.pixel_end.y >= bottom) {
2199
+ tiles[1] = cell.tile_position;
2200
+ // return tiles;
2201
+ return new TileRange(tiles[0], tiles[1]); // too much?
2202
+ }
2203
+ }
2204
+ }
2205
+ }
2206
+
2207
+ // return tiles;
2208
+ return new TileRange(tiles[0], tiles[1]); // too much?
2209
+
2210
+ }
2211
+
2212
+ public UpdateTileHeights(mark_dirty = true, start_row = -1): void {
2213
+
2214
+ let y = 0;
2215
+
2216
+ for (let i = 0; i < this.row_header_tiles.length; i++) {
2217
+ const tile = this.row_header_tiles[i];
2218
+ // const column = this.grid_tiles[i];
2219
+
2220
+ if (start_row > tile.last_cell.row) {
2221
+ y += tile.logical_size.height;
2222
+ continue;
2223
+ }
2224
+
2225
+ let height = 0;
2226
+
2227
+ for (let r = tile.first_cell.row; r <= tile.last_cell.row; r++) {
2228
+ height += this.RowHeight(r);
2229
+ }
2230
+
2231
+ const height_match = (tile.logical_size.height === height);
2232
+
2233
+ tile.pixel_start.y = y;
2234
+ y += height;
2235
+ tile.pixel_end.y = y;
2236
+
2237
+ if (!height_match) {
2238
+
2239
+ tile.logical_size.height = height;
2240
+ tile.style.height = `${height}px`;
2241
+ tile.height = this.dpr * height;
2242
+
2243
+ if (this.view.active_sheet.freeze.columns) {
2244
+ const frozen_tile = this.frozen_column_tiles[i];
2245
+ frozen_tile.logical_size.height = height;
2246
+ frozen_tile.style.height = `${height}px`;
2247
+ frozen_tile.height = this.dpr * height;
2248
+ }
2249
+
2250
+ if (mark_dirty) {
2251
+ tile.dirty = true;
2252
+ tile.needs_full_repaint = true;
2253
+ }
2254
+ }
2255
+
2256
+ for (const column of this.grid_tiles) {
2257
+ const grid_tile = column[i];
2258
+
2259
+ grid_tile.pixel_start.y = tile.pixel_start.y;
2260
+ grid_tile.pixel_end.y = tile.pixel_end.y;
2261
+
2262
+ if (!height_match) {
2263
+
2264
+ grid_tile.logical_size.height = height;
2265
+ grid_tile.style.height = `${height}px`;
2266
+ grid_tile.height = this.dpr * height;
2267
+
2268
+ if (mark_dirty) {
2269
+ grid_tile.dirty = true;
2270
+ grid_tile.needs_full_repaint = true;
2271
+ }
2272
+ }
2273
+ }
2274
+
2275
+ }
2276
+
2277
+ if (this.view.active_sheet.freeze.rows) {
2278
+ let freeze_height = 0;
2279
+ for (let i = 0; i < this.view.active_sheet.freeze.rows; i++) freeze_height += this.RowHeight(i);
2280
+ for (const tile of this.frozen_row_tiles) {
2281
+ tile.style.height = `${freeze_height}px`;
2282
+ tile.height = freeze_height * this.dpr;
2283
+ }
2284
+
2285
+ // corner includes header size
2286
+ freeze_height += this.header_offset.y;
2287
+ this.corner_canvas.style.height = `${freeze_height}px`;
2288
+ this.corner_canvas.height = freeze_height * this.dpr;
2289
+
2290
+ // mark these as dirty so we get painted
2291
+ for (const column of this.grid_tiles) {
2292
+ column[0].dirty = true;
2293
+ }
2294
+
2295
+ }
2296
+
2297
+ this.UpdateGridTemplates(false, true);
2298
+
2299
+ this.row_header.style.height = this.contents.style.height = `${y}px`;
2300
+
2301
+ this.ClearLayoutCaches();
2302
+
2303
+
2304
+ }
2305
+
2306
+ /**
2307
+ * update all widths. start_column is a hint that can save some work
2308
+ * by skipping unaffected tiles
2309
+ */
2310
+ public UpdateTileWidths(mark_dirty = true, start_column = -1): void {
2311
+
2312
+ let x = 0;
2313
+
2314
+ for (let i = 0; i < this.column_header_tiles.length; i++) {
2315
+ const tile = this.column_header_tiles[i];
2316
+ const column = this.grid_tiles[i];
2317
+
2318
+ if (start_column > tile.last_cell.column) {
2319
+ x += tile.logical_size.width;
2320
+ continue;
2321
+ }
2322
+
2323
+ let width = 0;
2324
+
2325
+ for (let c = tile.first_cell.column; c <= tile.last_cell.column; c++) {
2326
+ width += this.ColumnWidth(c);
2327
+ }
2328
+
2329
+ const width_match = (tile.logical_size.width === width);
2330
+
2331
+ tile.pixel_start.x = x;
2332
+ x += width;
2333
+ tile.pixel_end.x = x;
2334
+
2335
+ if (!width_match) {
2336
+
2337
+ tile.logical_size.width = width;
2338
+ tile.style.width = `${width}px`;
2339
+ tile.width = this.dpr * width;
2340
+
2341
+ if (this.view.active_sheet.freeze.rows) {
2342
+ const frozen_tile = this.frozen_row_tiles[i];
2343
+ frozen_tile.logical_size.width = width;
2344
+ frozen_tile.style.width = `${width}px`;
2345
+ frozen_tile.width = this.dpr * width;
2346
+ }
2347
+
2348
+ if (mark_dirty) {
2349
+ tile.dirty = true;
2350
+ tile.needs_full_repaint = true;
2351
+ }
2352
+ }
2353
+
2354
+ for (const grid_tile of column) {
2355
+
2356
+ grid_tile.pixel_start.x = tile.pixel_start.x;
2357
+ grid_tile.pixel_end.x = tile.pixel_end.x;
2358
+
2359
+ if (!width_match) {
2360
+
2361
+ grid_tile.logical_size.width = width;
2362
+ grid_tile.style.width = `${width}px`;
2363
+ grid_tile.width = this.dpr * width;
2364
+
2365
+ if (mark_dirty) {
2366
+ grid_tile.dirty = true;
2367
+ grid_tile.needs_full_repaint = true;
2368
+ }
2369
+ }
2370
+
2371
+ }
2372
+
2373
+ }
2374
+
2375
+ if (this.view.active_sheet.freeze.columns) {
2376
+ let freeze_width = 0;
2377
+ for (let i = 0; i < this.view.active_sheet.freeze.columns; i++) freeze_width += this.ColumnWidth(i);
2378
+ for (const tile of this.frozen_column_tiles) {
2379
+ tile.style.width = `${freeze_width}px`;
2380
+ tile.width = freeze_width * this.dpr;
2381
+ }
2382
+
2383
+ // corner includes header size
2384
+ freeze_width += this.header_offset.x;
2385
+ this.corner_canvas.style.width = `${freeze_width}px`;
2386
+ this.corner_canvas.width = freeze_width * this.dpr;
2387
+
2388
+ // mark these as dirty so we get painted
2389
+ for (const tile of this.grid_tiles[0]) {
2390
+ tile.dirty = true;
2391
+ }
2392
+
2393
+ }
2394
+
2395
+ this.UpdateGridTemplates(true, false);
2396
+
2397
+ this.column_header.style.width = this.contents.style.width = `${x}px`;
2398
+
2399
+ this.ClearLayoutCaches();
2400
+
2401
+
2402
+ }
2403
+
2404
+ public ClampToGrid(point: Point): Point {
2405
+ const address = this.PointToAddress_Grid(point);
2406
+ const rect = this.OffsetCellAddressToRectangle(address);
2407
+
2408
+ if (point.x > rect.left + rect.width / 2) {
2409
+ point.x = rect.left + rect.width - 1;
2410
+ }
2411
+ else {
2412
+ point.x = rect.left - 1;
2413
+ }
2414
+
2415
+ if (point.y > rect.top + rect.height / 2) {
2416
+ point.y = rect.top + rect.height - 1;
2417
+ }
2418
+ else {
2419
+ point.y = rect.top - 1;
2420
+ }
2421
+
2422
+ return point;
2423
+ }
2424
+
2425
+ /**
2426
+ * wrapper method for CellAddressToRectangle allows us to offset for
2427
+ * frozen rows/columns. in some cases we may not want to do this, so
2428
+ * the underlying method is still visible (and cache contains the raw
2429
+ * rectangles, not offset).
2430
+ */
2431
+ public OffsetCellAddressToRectangle(address: ICellAddress): Rectangle {
2432
+
2433
+ let rect = this.CellAddressToRectangle(address);
2434
+
2435
+ if (address.column >= 0 && address.column < this.view.active_sheet.freeze.columns) {
2436
+ rect = rect.Shift(this.scroll_reference_node.scrollLeft, 0);
2437
+ }
2438
+ if (address.row >= 0 && address.row < this.view.active_sheet.freeze.rows) {
2439
+ rect = rect.Shift(0, this.scroll_reference_node.scrollTop);
2440
+ }
2441
+
2442
+ return rect;
2443
+
2444
+ }
2445
+
2446
+ /**
2447
+ * finds the rectangle, in the coordinate space of the grid node,
2448
+ * of the cell with the given address. uses a cache since we wind
2449
+ * up looking up the same rectangles a lot.
2450
+ *
2451
+ * UPDATE dropping rectangle cache in favor of holding row and
2452
+ * column edges. I realized we were holding a lot of redundant
2453
+ * information, and this should be resonably fast.
2454
+ *
2455
+ * TODO could probably be slightly more efficient by holding the
2456
+ * left edge of the column/row at the index; then we don't have to
2457
+ * have special behavior for column/row 0.
2458
+ */
2459
+ public CellAddressToRectangle(address: ICellAddress): Rectangle {
2460
+
2461
+ // limit
2462
+
2463
+ const row = address.row === Infinity || address.row < 0 ? 0 : address.row;
2464
+ const column = address.column === Infinity || address.column < 0 ? 0 : address.column;
2465
+
2466
+ // build out the caches if necessary
2467
+
2468
+ if (this.column_cache.length <= column + 1) {
2469
+
2470
+ if (!this.column_cache.length) {
2471
+ this.column_cache[0] = 0;
2472
+ }
2473
+
2474
+ for (let i = this.column_cache.length - 1; i <= column; i++) {
2475
+ this.column_cache[i + 1] = this.column_cache[i] + this.ColumnWidth(i);
2476
+ }
2477
+
2478
+ }
2479
+
2480
+ if (this.row_cache.length <= row + 1) {
2481
+
2482
+ if (!this.row_cache.length) {
2483
+ this.row_cache[0] = 0;
2484
+ }
2485
+
2486
+ for (let i = this.row_cache.length - 1; i <= row; i++) {
2487
+ this.row_cache[i + 1] = this.row_cache[i] + this.RowHeight(i);
2488
+ }
2489
+
2490
+ }
2491
+
2492
+ // now we can construct the rectangle
2493
+
2494
+ const left = this.column_cache[column];
2495
+ const top = this.row_cache[row];
2496
+
2497
+ return new Rectangle(left, top,
2498
+ this.column_cache[column + 1] - left,
2499
+ this.row_cache[row + 1] - top);
2500
+
2501
+ }
2502
+
2503
+ /**
2504
+ * resizes the tile at this index, rebuilds structure of subsequent tiles.
2505
+ * this is necessary because tiles keep track of pixel position: so if a
2506
+ * tile is resized, subsequent tiles have to change.
2507
+ */
2508
+ public ResizeTileWidth(index: number, width: number, mark_dirty = true): void {
2509
+
2510
+ // start with headers...
2511
+
2512
+ let tile = this.column_header_tiles[index];
2513
+ const delta = width - tile.logical_size.width;
2514
+
2515
+ tile.logical_size.width = width;
2516
+ tile.style.width = `${width}px`;
2517
+ tile.width = this.dpr * width;
2518
+ tile.pixel_end.x += delta;
2519
+
2520
+ if (mark_dirty) {
2521
+ tile.dirty = true;
2522
+ tile.needs_full_repaint = true;
2523
+ }
2524
+
2525
+ for (let i = index + 1; i < this.column_header_tiles.length; i++) {
2526
+ this.column_header_tiles[i].pixel_start.x += delta;
2527
+ this.column_header_tiles[i].pixel_end.x += delta;
2528
+
2529
+ for (const cell of this.grid_tiles[i]) {
2530
+ cell.pixel_start.x += delta;
2531
+ cell.pixel_end.x += delta;
2532
+ }
2533
+
2534
+ }
2535
+
2536
+ const column = this.grid_tiles[index];
2537
+ for (tile of column) {
2538
+ tile.logical_size.width = width;
2539
+ tile.style.width = `${width}px`;
2540
+ tile.width = this.dpr * width;
2541
+ tile.pixel_end.x += delta;
2542
+ if (mark_dirty) {
2543
+ tile.dirty = true;
2544
+ tile.needs_full_repaint = true;
2545
+ }
2546
+ }
2547
+
2548
+ this.UpdateTotalSize();
2549
+ this.UpdateGridTemplates(true, false);
2550
+ this.UpdateContentsSize();
2551
+
2552
+ }
2553
+
2554
+ /**
2555
+ * resizes the tile at this index, rebuilds structure of subsequent tiles
2556
+ */
2557
+ public ResizeTileHeight(index: number, height: number, mark_dirty = true): void {
2558
+
2559
+ // start with headers...
2560
+
2561
+ let tile = this.row_header_tiles[index];
2562
+ const delta = height - tile.logical_size.height;
2563
+
2564
+ tile.logical_size.height = height;
2565
+ tile.style.height = `${height}px`;
2566
+ tile.height = this.dpr * height;
2567
+ tile.pixel_end.y += delta;
2568
+
2569
+ if (mark_dirty) {
2570
+ tile.dirty = true;
2571
+ tile.needs_full_repaint = true;
2572
+ }
2573
+
2574
+ for (let r = index + 1; r < this.row_header_tiles.length; r++) {
2575
+ tile = this.row_header_tiles[r];
2576
+ tile.pixel_start.y += delta;
2577
+ tile.pixel_end.y += delta;
2578
+ }
2579
+
2580
+ for (const column of this.grid_tiles) {
2581
+ tile = column[index];
2582
+ tile.logical_size.height = height;
2583
+ tile.style.height = `${height}px`;
2584
+ tile.height = this.dpr * height;
2585
+ tile.pixel_end.y += delta;
2586
+ if (mark_dirty) {
2587
+ tile.dirty = true;
2588
+ tile.needs_full_repaint = true;
2589
+ }
2590
+
2591
+ for (let i = index + 1; i < column.length; i++) {
2592
+ column[i].pixel_start.y += delta;
2593
+ column[i].pixel_end.y += delta;
2594
+ }
2595
+ }
2596
+
2597
+ this.UpdateTotalSize();
2598
+ this.UpdateGridTemplates(false, true);
2599
+ this.UpdateContentsSize();
2600
+
2601
+ }
2602
+
2603
+ /**
2604
+ * do subclass-specific initialization
2605
+ */
2606
+ public abstract InitializeInternal(container: HTMLElement, scroll_callback: () => void): void;
2607
+
2608
+ /**
2609
+ * set resize cursor. this is subclass-specific because it's set on
2610
+ * different nodes depending on layout.
2611
+ */
2612
+ public abstract ResizeCursor(resize?: 'row' | 'column'): void;
2613
+
2614
+ protected abstract UpdateTileGridPosition(tile: Tile): void;
2615
+ protected abstract UpdateContainingGrid(): void;
2616
+ protected abstract UpdateGridTemplates(columns: boolean, rows: boolean): void;
2617
+
2618
+ }