-
Notifications
You must be signed in to change notification settings - Fork 0
/
GridStyleManager.ts
203 lines (173 loc) · 6.25 KB
/
GridStyleManager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import {addScroll} from '../internal';
import {cssClass, CSS_CLASS_BODY, CSS_CLASS_FOOTER, CSS_CLASS_HEADER, CSS_CLASS_SCROLLBAR_TESTER, CSS_CLASS_SHIFTED, CSS_CLASS_TBODY, CSS_CLASS_THEAD} from '../styles';
import StyleManager from './StyleManager';
export function setTemplate(root: HTMLElement, id: string) {
id = id.startsWith('#') ? id.slice(1) : id;
root.innerHTML = `
<header id="header-${id}" class="${CSS_CLASS_HEADER} ${cssClass(`header-${id}`)}">
<article class="${CSS_CLASS_THEAD} ${cssClass(`thead-${id}`)}"></article>
</header>
<main id="body-${id}" class="${CSS_CLASS_BODY} ${cssClass(`body-${id}`)}">
<footer class="${CSS_CLASS_FOOTER}"> </footer>
<article class="${CSS_CLASS_TBODY} ${cssClass(`tbody-${id}`)}"></article>
</main>`;
return root;
}
/**
* column base interface
*/
export interface IColumn {
readonly index: number;
readonly id: string;
readonly width: number;
/**
* boolean flag whether when scrolling the column should be sticky
*/
readonly frozen: boolean;
}
interface ISelectors {
thead: string;
tbody: string;
tr: string;
th: string;
td: string;
}
/**
* generates the HTML Ids used for the header and body article of a table
* @param {string} tableId base table id
* @param {boolean} asSelector flag whether to prepend with # for CSS selector
* @return {ISelectors} the table ids used for header and body
*/
export function tableIds(tableId: string) {
return {
thead: `thead-${tableId}`,
tbody: `tbody-${tableId}`,
tr: `tr-${tableId}`,
th: `th-${tableId}`,
td: `td-${tableId}`
};
}
export function tableCSSClasses(tableId: string) {
const ids = tableIds(tableId);
return {
thead: cssClass(ids.thead),
tbody: cssClass(ids.tbody),
tr: cssClass(ids.tr),
th: cssClass(ids.th),
td: cssClass(ids.td)
};
}
/**
* utility for custom generated CSS rules with a focus on dynamically generated grid layouts
*/
export default class GridStyleManager extends StyleManager {
readonly id: string;
readonly ids: ISelectors;
readonly cssClasses: ISelectors;
constructor(root: HTMLElement, id: string) {
super(root);
this.id = id.startsWith('#') ? id.slice(1) : id;
this.ids = tableIds(this.id);
this.cssClasses = tableCSSClasses(this.id);
const headerScroller = <HTMLElement>root.getElementsByTagName('header')[0];
const bodyScroller = <HTMLElement>root.getElementsByTagName('main')[0];
// async since style needs to be added to dom first
self.setTimeout(() => {
const {width} = measureScrollbar(root);
this.updateRule('__scollBarFix2', `#header-${this.id} > article:last-of-type`, {
borderRight: `${width}px solid transparent`
});
}, 20);
let old = headerScroller.scrollLeft;
// update frozen and sync header with body
addScroll(bodyScroller, 'animation', (act) => {
const newValue = act.left;
if (old !== newValue) {
old = headerScroller.scrollLeft = newValue;
}
root.classList.toggle(CSS_CLASS_SHIFTED, act.left > 0);
});
}
/**
* updates the column widths and default row height for a table
* @param {number} defaultRowHeight
* @param {IColumn[]} columns
* @param {number} frozenShift shift frozen colums
* @param {string} tableId optional tableId in case of multiple tables within the same engine
* @param {string} unit
*/
update(defaultRowHeight: number, columns: IColumn[], padding: (index: number) => number, frozenShift: number, tableId: string, unit: string = 'px') {
const ids = tableIds(tableId);
const selectors = tableCSSClasses(tableId);
const total = `${columns.reduce((a, b, i) => a + b.width + padding(i), 0)}${unit}`;
this.updateRule(`__heightsRule${selectors.tr}`, `.${selectors.tr}`, {
height: `${defaultRowHeight}px`,
width: total
});
this.updateRule(`__heightsRule${selectors.tbody}`, `#${ids.tbody}`, {
width: total
});
this.updateColumns(columns, padding, selectors, frozenShift, unit);
}
/**
* removes a given tableId if not needed anymore
* @param {string} tableId tableId to remove
*/
remove(tableId: string) {
const selectors = tableCSSClasses(tableId);
this.deleteRule(`__heightsRule${selectors.tr}`);
this.deleteRule(`__heightsRule${selectors.tbody}`);
const prefix = `__col${selectors.td}_`;
const rules = this.ruleNames.reduce((a, b) => a + (b.startsWith(prefix) ? 1 : 0), 0);
// reset
for (let i = 0; i < rules; ++i) {
this.deleteRule(`${prefix}${i}`);
}
}
private updateColumns(columns: IColumn[], padding: (index: number) => number, cssSelectors: ISelectors, frozenShift: number, unit: string = 'px') {
const prefix = `__col${cssSelectors.td}_`;
const rules = new Set(this.ruleNames.filter((d) => d.startsWith(prefix)));
let acc = 0;
columns.forEach((c, i) => {
const th = `.${cssSelectors.th}[data-id="${c.id}"]`;
const thStyles: Partial<CSSStyleDeclaration> = {
width: `${c.width}${unit}`
};
const td = `.${cssSelectors.td}[data-id="${c.id}"]`;
const tdStyles: Partial<CSSStyleDeclaration> = {
transform: `translateX(${acc}${unit})`,
width: `${c.width}${unit}`
};
if (c.frozen) {
thStyles.left = `${acc}px`;
this.updateRule(`${prefix}${td}F`, `.${cssSelectors.td}.${CSS_CLASS_SHIFTED}[data-id="${c.id}"]`, {
transform: `translateX(0)`,
left: `${acc + frozenShift}${unit}`
});
rules.delete(`${prefix}${td}F`);
}
this.updateRule(`${prefix}${th}`, th, thStyles);
rules.delete(`${prefix}${th}`);
this.updateRule(`${prefix}${td}`, td, tdStyles);
rules.delete(`${prefix}${td}`);
acc += c.width + padding(i);
});
rules.forEach((d) => this.deleteRule(d));
}
}
/**
* measure the width and height of the scrollbars
* based on Slick grid implementation
* @param root
*/
function measureScrollbar(root: HTMLElement) {
const body = root.ownerDocument!.body;
body.insertAdjacentHTML('beforeend', `
<div class="${CSS_CLASS_SCROLLBAR_TESTER}"><div></div></div>
`);
const elem = <HTMLElement>body.lastElementChild!;
const width = elem.offsetWidth - elem.clientWidth;
const height = elem.offsetHeight - elem.clientHeight;
elem.remove();
return {width, height};
}