@trebco/treb 23.6.5 → 25.0.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +8 -0
- package/.eslintrc.js +164 -0
- package/README-shadow-DOM.md +88 -0
- package/README.md +37 -130
- package/api-config.json +29 -0
- package/api-generator/api-generator-types.ts +82 -0
- package/api-generator/api-generator.ts +1172 -0
- package/api-generator/package.json +3 -0
- package/build/treb-spreadsheet.mjs +14 -0
- package/{treb.d.ts → build/treb.d.ts} +285 -269
- package/esbuild-custom-element.mjs +336 -0
- package/esbuild.js +305 -0
- package/package.json +43 -14
- package/treb-base-types/package.json +5 -0
- package/treb-base-types/src/api_types.ts +36 -0
- package/treb-base-types/src/area.ts +583 -0
- package/treb-base-types/src/basic_types.ts +45 -0
- package/treb-base-types/src/cell.ts +612 -0
- package/treb-base-types/src/cells.ts +1066 -0
- package/treb-base-types/src/color.ts +124 -0
- package/treb-base-types/src/import.ts +71 -0
- package/treb-base-types/src/index-standalone.ts +29 -0
- package/treb-base-types/src/index.ts +42 -0
- package/treb-base-types/src/layout.ts +47 -0
- package/treb-base-types/src/localization.ts +187 -0
- package/treb-base-types/src/rectangle.ts +145 -0
- package/treb-base-types/src/render_text.ts +72 -0
- package/treb-base-types/src/style.ts +545 -0
- package/treb-base-types/src/table.ts +109 -0
- package/treb-base-types/src/text_part.ts +54 -0
- package/treb-base-types/src/theme.ts +608 -0
- package/treb-base-types/src/union.ts +152 -0
- package/treb-base-types/src/value-type.ts +164 -0
- package/treb-base-types/style/resizable.css +59 -0
- package/treb-calculator/modern.tsconfig.json +11 -0
- package/treb-calculator/package.json +5 -0
- package/treb-calculator/src/calculator.ts +2546 -0
- package/treb-calculator/src/complex-math.ts +558 -0
- package/treb-calculator/src/dag/array-vertex.ts +198 -0
- package/treb-calculator/src/dag/graph.ts +951 -0
- package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
- package/treb-calculator/src/dag/vertex.ts +352 -0
- package/treb-calculator/src/descriptors.ts +162 -0
- package/treb-calculator/src/expression-calculator.ts +1069 -0
- package/treb-calculator/src/function-error.ts +103 -0
- package/treb-calculator/src/function-library.ts +103 -0
- package/treb-calculator/src/functions/base-functions.ts +1214 -0
- package/treb-calculator/src/functions/checkbox.ts +164 -0
- package/treb-calculator/src/functions/complex-functions.ts +253 -0
- package/treb-calculator/src/functions/finance-functions.ts +399 -0
- package/treb-calculator/src/functions/information-functions.ts +102 -0
- package/treb-calculator/src/functions/matrix-functions.ts +182 -0
- package/treb-calculator/src/functions/sparkline.ts +335 -0
- package/treb-calculator/src/functions/statistics-functions.ts +350 -0
- package/treb-calculator/src/functions/text-functions.ts +298 -0
- package/treb-calculator/src/index.ts +27 -0
- package/treb-calculator/src/notifier-types.ts +59 -0
- package/treb-calculator/src/primitives.ts +428 -0
- package/treb-calculator/src/utilities.ts +305 -0
- package/treb-charts/package.json +5 -0
- package/treb-charts/src/chart-functions.ts +156 -0
- package/treb-charts/src/chart-types.ts +230 -0
- package/treb-charts/src/chart.ts +1288 -0
- package/treb-charts/src/index.ts +24 -0
- package/treb-charts/src/main.ts +37 -0
- package/treb-charts/src/rectangle.ts +52 -0
- package/treb-charts/src/renderer.ts +1841 -0
- package/treb-charts/src/util.ts +122 -0
- package/treb-charts/style/charts.scss +221 -0
- package/treb-charts/style/old-charts.scss +250 -0
- package/treb-embed/markup/layout.html +137 -0
- package/treb-embed/markup/toolbar.html +175 -0
- package/treb-embed/modern.tsconfig.json +25 -0
- package/treb-embed/src/custom-element/content-types.d.ts +18 -0
- package/treb-embed/src/custom-element/global.d.ts +11 -0
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
- package/treb-embed/src/custom-element/treb-global.ts +44 -0
- package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
- package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
- package/treb-embed/src/index.ts +16 -0
- package/treb-embed/src/language-model.ts +41 -0
- package/treb-embed/src/options.ts +320 -0
- package/treb-embed/src/progress-dialog.ts +228 -0
- package/treb-embed/src/selection-state.ts +16 -0
- package/treb-embed/src/spinner.ts +42 -0
- package/treb-embed/src/toolbar-message.ts +96 -0
- package/treb-embed/src/types.ts +167 -0
- package/treb-embed/style/autocomplete.scss +103 -0
- package/treb-embed/style/dark-theme.scss +114 -0
- package/treb-embed/style/defaults.scss +36 -0
- package/treb-embed/style/dialog.scss +181 -0
- package/treb-embed/style/dropdown-select.scss +101 -0
- package/treb-embed/style/formula-bar.scss +193 -0
- package/treb-embed/style/grid.scss +374 -0
- package/treb-embed/style/layout.scss +424 -0
- package/treb-embed/style/mouse-mask.scss +67 -0
- package/treb-embed/style/note.scss +92 -0
- package/treb-embed/style/overlay-editor.scss +102 -0
- package/treb-embed/style/spinner.scss +92 -0
- package/treb-embed/style/tab-bar.scss +228 -0
- package/treb-embed/style/table.scss +80 -0
- package/treb-embed/style/theme-defaults.scss +444 -0
- package/treb-embed/style/toolbar.scss +416 -0
- package/treb-embed/style/tooltip.scss +68 -0
- package/treb-embed/style/treb-icons.scss +130 -0
- package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
- package/treb-embed/style/z-index.scss +43 -0
- package/treb-export/docs/charts.md +68 -0
- package/treb-export/modern.tsconfig.json +19 -0
- package/treb-export/package.json +4 -0
- package/treb-export/src/address-type.ts +77 -0
- package/treb-export/src/base-template.ts +22 -0
- package/treb-export/src/column-width.ts +85 -0
- package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
- package/treb-export/src/drawing2/chart2.ts +282 -0
- package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
- package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
- package/treb-export/src/drawing2/drawing2.ts +355 -0
- package/treb-export/src/drawing2/embedded-image.ts +71 -0
- package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
- package/treb-export/src/export-worker/export-worker.ts +99 -0
- package/treb-export/src/export-worker/index-modern.ts +22 -0
- package/treb-export/src/export2.ts +2204 -0
- package/treb-export/src/import2.ts +882 -0
- package/treb-export/src/relationship.ts +36 -0
- package/treb-export/src/shared-strings2.ts +128 -0
- package/treb-export/src/template-2.ts +22 -0
- package/treb-export/src/unescape_xml.ts +47 -0
- package/treb-export/src/workbook-sheet2.ts +182 -0
- package/treb-export/src/workbook-style2.ts +1285 -0
- package/treb-export/src/workbook-theme2.ts +88 -0
- package/treb-export/src/workbook2.ts +491 -0
- package/treb-export/src/xml-utils.ts +201 -0
- package/treb-export/template/base/[Content_Types].xml +2 -0
- package/treb-export/template/base/_rels/.rels +2 -0
- package/treb-export/template/base/docProps/app.xml +2 -0
- package/treb-export/template/base/docProps/core.xml +12 -0
- package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
- package/treb-export/template/base/xl/sharedStrings.xml +2 -0
- package/treb-export/template/base/xl/styles.xml +2 -0
- package/treb-export/template/base/xl/theme/theme1.xml +2 -0
- package/treb-export/template/base/xl/workbook.xml +2 -0
- package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
- package/treb-export/template/base.xlsx +0 -0
- package/treb-format/package.json +8 -0
- package/treb-format/src/format.test.ts +213 -0
- package/treb-format/src/format.ts +942 -0
- package/treb-format/src/format_cache.ts +199 -0
- package/treb-format/src/format_parser.ts +723 -0
- package/treb-format/src/index.ts +25 -0
- package/treb-format/src/number_format_section.ts +100 -0
- package/treb-format/src/value_parser.ts +337 -0
- package/treb-grid/package.json +5 -0
- package/treb-grid/src/editors/autocomplete.ts +394 -0
- package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
- package/treb-grid/src/editors/formula_bar.ts +473 -0
- package/treb-grid/src/editors/formula_editor_base.ts +910 -0
- package/treb-grid/src/editors/overlay_editor.ts +511 -0
- package/treb-grid/src/index.ts +37 -0
- package/treb-grid/src/layout/base_layout.ts +2618 -0
- package/treb-grid/src/layout/grid_layout.ts +299 -0
- package/treb-grid/src/layout/rectangle_cache.ts +86 -0
- package/treb-grid/src/render/selection-renderer.ts +414 -0
- package/treb-grid/src/render/svg_header_overlay.ts +93 -0
- package/treb-grid/src/render/svg_selection_block.ts +187 -0
- package/treb-grid/src/render/tile_renderer.ts +2122 -0
- package/treb-grid/src/types/annotation.ts +216 -0
- package/treb-grid/src/types/border_constants.ts +34 -0
- package/treb-grid/src/types/clipboard_data.ts +31 -0
- package/treb-grid/src/types/data_model.ts +334 -0
- package/treb-grid/src/types/drag_mask.ts +81 -0
- package/treb-grid/src/types/grid.ts +7743 -0
- package/treb-grid/src/types/grid_base.ts +3644 -0
- package/treb-grid/src/types/grid_command.ts +470 -0
- package/treb-grid/src/types/grid_events.ts +124 -0
- package/treb-grid/src/types/grid_options.ts +97 -0
- package/treb-grid/src/types/grid_selection.ts +60 -0
- package/treb-grid/src/types/named_range.ts +369 -0
- package/treb-grid/src/types/scale-control.ts +202 -0
- package/treb-grid/src/types/serialize_options.ts +72 -0
- package/treb-grid/src/types/set_range_options.ts +52 -0
- package/treb-grid/src/types/sheet.ts +3099 -0
- package/treb-grid/src/types/sheet_types.ts +95 -0
- package/treb-grid/src/types/tab_bar.ts +464 -0
- package/treb-grid/src/types/tile.ts +59 -0
- package/treb-grid/src/types/update_flags.ts +75 -0
- package/treb-grid/src/util/dom_utilities.ts +44 -0
- package/treb-grid/src/util/fontmetrics2.ts +179 -0
- package/treb-grid/src/util/ua.ts +104 -0
- package/treb-logo.svg +18 -0
- package/treb-parser/package.json +5 -0
- package/treb-parser/src/csv-parser.ts +122 -0
- package/treb-parser/src/index.ts +25 -0
- package/treb-parser/src/md-parser.ts +526 -0
- package/treb-parser/src/parser-types.ts +397 -0
- package/treb-parser/src/parser.test.ts +298 -0
- package/treb-parser/src/parser.ts +2673 -0
- package/treb-utils/package.json +5 -0
- package/treb-utils/src/dispatch.ts +57 -0
- package/treb-utils/src/event_source.ts +147 -0
- package/treb-utils/src/ievent_source.ts +33 -0
- package/treb-utils/src/index.ts +31 -0
- package/treb-utils/src/measurement.ts +174 -0
- package/treb-utils/src/resizable.ts +160 -0
- package/treb-utils/src/scale.ts +137 -0
- package/treb-utils/src/serialize_html.ts +124 -0
- package/treb-utils/src/template.ts +70 -0
- package/treb-utils/src/validate_uri.ts +61 -0
- package/tsconfig.json +10 -0
- package/tsproject.json +30 -0
- package/util/license-plugin-esbuild.js +86 -0
- package/util/list-css-vars.sh +46 -0
- package/README-esm.md +0 -37
- package/treb-bundle.css +0 -2
- package/treb-bundle.mjs +0 -15
|
@@ -0,0 +1,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 = ' ';
|
|
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
|
+
}
|