1 | import {Feature} from '../feature';
|
2 | import {createElm, removeElm, elm, tag} from '../dom';
|
3 | import {addEvt, targetEvt} from '../event';
|
4 | import {contains} from '../string';
|
5 | import {NONE} from '../const';
|
6 | import {
|
7 | defaultsBool, defaultsStr, defaultsNb, defaultsArr
|
8 | } from '../settings';
|
9 |
|
10 | /**
|
11 | * Grid layout, table with fixed headers
|
12 | */
|
13 | export class GridLayout extends Feature {
|
14 |
|
15 | /**
|
16 | * Creates an instance of GridLayout
|
17 | * @param {TableFilter} tf TableFilter instance
|
18 | */
|
19 | constructor(tf) {
|
20 | super(tf, GridLayout);
|
21 |
|
22 | let f = this.config.grid_layout || {};
|
23 |
|
24 | /**
|
25 | * Grid-layout container width as CSS string
|
26 | * @type {String}
|
27 | */
|
28 | this.width = defaultsStr(f.width, null);
|
29 |
|
30 | /**
|
31 | * Grid-layout container height as CSS string
|
32 | * @type {String}
|
33 | */
|
34 | this.height = defaultsStr(f.height, null);
|
35 |
|
36 | /**
|
37 | * Css class for main container element
|
38 | * @type {String}
|
39 | */
|
40 | this.mainContCssClass = defaultsStr(f.cont_css_class, 'grd_Cont');
|
41 |
|
42 | /**
|
43 | * Css class for body table container element
|
44 | * @type {String}
|
45 | */
|
46 | this.contCssClass = defaultsStr(f.tbl_cont_css_class, 'grd_tblCont');
|
47 |
|
48 | /**
|
49 | * Css class for headers table container element
|
50 | * @type {String}
|
51 | */
|
52 | this.headContCssClass = defaultsStr(f.tbl_head_css_class,
|
53 | 'grd_headTblCont');
|
54 |
|
55 | /**
|
56 | * Css class for toolbar container element (rows counter, paging etc.)
|
57 | * @type {String}
|
58 | */
|
59 | this.infDivCssClass = defaultsStr(f.inf_grid_css_class, 'grd_inf');
|
60 |
|
61 | /**
|
62 | * Index of the headers row, default: 0
|
63 | * @type {Number}
|
64 | */
|
65 | this.headRowIndex = defaultsNb(f.headers_row_index, 0);
|
66 |
|
67 | /**
|
68 | * Collection of the header row indexes to be moved into headers table
|
69 | * @type {Array}
|
70 | */
|
71 | this.headRows = defaultsArr(f.headers_rows, [0]);
|
72 |
|
73 | /**
|
74 | * Enable or disable column filters generation, default: true
|
75 | * @type {Boolean}
|
76 | */
|
77 | this.filters = defaultsBool(f.filters, true);
|
78 |
|
79 | /**
|
80 | * Enable or disable column headers, default: false
|
81 | * @type {Boolean}
|
82 | */
|
83 | this.noHeaders = Boolean(f.no_headers);
|
84 |
|
85 | /**
|
86 | * Grid-layout default column widht as CSS string
|
87 | * @type {String}
|
88 | */
|
89 | this.defaultColWidth = defaultsStr(f.default_col_width, '100px');
|
90 |
|
91 | /**
|
92 | * List of column elements
|
93 | * @type {Array}
|
94 | * @private
|
95 | */
|
96 | this.colElms = [];
|
97 |
|
98 | /**
|
99 | * Prefix for grid-layout filter's cell ID
|
100 | * @type {String}
|
101 | * @private
|
102 | */
|
103 | this.prfxGridFltTd = '_td_';
|
104 |
|
105 | /**
|
106 | * Prefix for grid-layout header's cell ID
|
107 | * @type {String}
|
108 | * @private
|
109 | */
|
110 | this.prfxGridTh = 'tblHeadTh_';
|
111 |
|
112 | /**
|
113 | * Mark-up of original HTML table
|
114 | * @type {String}
|
115 | * @private
|
116 | */
|
117 | this.sourceTblHtml = tf.dom().outerHTML;
|
118 |
|
119 | /**
|
120 | * Indicates if working table has column elements
|
121 | * @type {Boolean}
|
122 | * @private
|
123 | */
|
124 | this.tblHasColTag = tag(tf.dom(), 'col').length > 0 ? true : false;
|
125 |
|
126 | /**
|
127 | * Main container element
|
128 | * @private
|
129 | */
|
130 | this.tblMainCont = null;
|
131 |
|
132 | /**
|
133 | * Table container element
|
134 | * @private
|
135 | */
|
136 | this.tblCont = null;
|
137 |
|
138 | /**
|
139 | * Headers' table container element
|
140 | * @private
|
141 | */
|
142 | this.headTblCont = null;
|
143 |
|
144 | /**
|
145 | * Headers' table element
|
146 | * @private
|
147 | */
|
148 | this.headTbl = null;
|
149 |
|
150 | // filters flag at TF level
|
151 | tf.fltGrid = this.filters;
|
152 | }
|
153 |
|
154 | /**
|
155 | * Generates a grid with fixed headers
|
156 | * TODO: reduce size of init by extracting single purposed methods
|
157 | */
|
158 | init() {
|
159 | let tf = this.tf;
|
160 | let tbl = tf.dom();
|
161 |
|
162 | if (this.initialized) {
|
163 | return;
|
164 | }
|
165 |
|
166 | // Override relevant TableFilter properties
|
167 | this.setOverrides();
|
168 |
|
169 | // Assign default column widths
|
170 | this.setDefaultColWidths();
|
171 |
|
172 | //Main container: it will contain all the elements
|
173 | this.tblMainCont = this.createContainer(
|
174 | 'div', this.mainContCssClass);
|
175 | if (this.width) {
|
176 | this.tblMainCont.style.width = this.width;
|
177 | }
|
178 | tbl.parentNode.insertBefore(this.tblMainCont, tbl);
|
179 |
|
180 | //Table container: div wrapping content table
|
181 | this.tblCont = this.createContainer('div', this.contCssClass);
|
182 | this.setConfigWidth(this.tblCont);
|
183 | if (this.height) {
|
184 | this.tblCont.style.height = this.height;
|
185 | }
|
186 | tbl.parentNode.insertBefore(this.tblCont, tbl);
|
187 | let t = removeElm(tbl);
|
188 | this.tblCont.appendChild(t);
|
189 |
|
190 | //In case table width is expressed in %
|
191 | if (tbl.style.width === '') {
|
192 | let tblW = this.initialTableWidth();
|
193 | tbl.style.width = (contains('%', tblW) ?
|
194 | tbl.clientWidth : tblW) + 'px';
|
195 | }
|
196 |
|
197 | let d = removeElm(this.tblCont);
|
198 | this.tblMainCont.appendChild(d);
|
199 |
|
200 | //Headers table container: div wrapping headers table
|
201 | this.headTblCont = this.createContainer(
|
202 | 'div', this.headContCssClass);
|
203 |
|
204 | //Headers table
|
205 | this.headTbl = createElm('table');
|
206 | let tH = createElm('tHead');
|
207 |
|
208 | //1st row should be headers row, ids are added if not set
|
209 | //Those ids are used by the sort feature
|
210 | let hRow = tbl.rows[this.headRowIndex];
|
211 | let sortTriggers = this.getSortTriggerIds(hRow);
|
212 |
|
213 | //Filters row is created
|
214 | let filtersRow = this.createFiltersRow();
|
215 |
|
216 | //Headers row are moved from content table to headers table
|
217 | this.setHeadersRow(tH);
|
218 |
|
219 | this.headTbl.appendChild(tH);
|
220 | if (tf.filtersRowIndex === 0) {
|
221 | tH.insertBefore(filtersRow, hRow);
|
222 | } else {
|
223 | tH.appendChild(filtersRow);
|
224 | }
|
225 |
|
226 | this.headTblCont.appendChild(this.headTbl);
|
227 | this.tblCont.parentNode.insertBefore(this.headTblCont, this.tblCont);
|
228 |
|
229 | //THead needs to be removed in content table for sort feature
|
230 | let thead = tag(tbl, 'thead');
|
231 | if (thead.length > 0) {
|
232 | tbl.removeChild(thead[0]);
|
233 | }
|
234 |
|
235 | // ensure table layout is always set even if already set in css
|
236 | // definitions, potentially with custom css class this could be lost
|
237 | this.headTbl.style.tableLayout = 'fixed';
|
238 | tbl.style.tableLayout = 'fixed';
|
239 |
|
240 | //content table without headers needs col widths to be reset
|
241 | tf.setColWidths(this.headTbl);
|
242 |
|
243 | //Headers container width
|
244 | this.headTbl.style.width = tbl.style.width;
|
245 | //
|
246 |
|
247 | //scroll synchronisation
|
248 | addEvt(this.tblCont, 'scroll', (evt) => {
|
249 | let elm = targetEvt(evt);
|
250 | let scrollLeft = elm.scrollLeft;
|
251 | this.headTblCont.scrollLeft = scrollLeft;
|
252 | //New pointerX calc taking into account scrollLeft
|
253 | // if(!o.isPointerXOverwritten){
|
254 | // try{
|
255 | // o.Evt.pointerX = function(evt){
|
256 | // let e = evt || global.event;
|
257 | // let bdScrollLeft = tf_StandardBody().scrollLeft +
|
258 | // scrollLeft;
|
259 | // return (e.pageX + scrollLeft) ||
|
260 | // (e.clientX + bdScrollLeft);
|
261 | // };
|
262 | // o.isPointerXOverwritten = true;
|
263 | // } catch(err) {
|
264 | // o.isPointerXOverwritten = false;
|
265 | // }
|
266 | // }
|
267 | });
|
268 |
|
269 | // TODO: Trigger a custom event handled by sort extension
|
270 | let sort = tf.extension('sort');
|
271 | if (sort) {
|
272 | sort.asyncSort = true;
|
273 | sort.triggerIds = sortTriggers;
|
274 | }
|
275 |
|
276 | //Col elements are enough to keep column widths after sorting and
|
277 | //filtering
|
278 | this.setColumnElements();
|
279 |
|
280 | if (tf.popupFilters) {
|
281 | filtersRow.style.display = NONE;
|
282 | }
|
283 |
|
284 | /** @inherited */
|
285 | this.initialized = true;
|
286 | }
|
287 |
|
288 | /**
|
289 | * Overrides TableFilter instance properties to adjust to grid layout mode
|
290 | * @private
|
291 | */
|
292 | setOverrides() {
|
293 | let tf = this.tf;
|
294 | tf.refRow = 0;
|
295 | tf.headersRow = 0;
|
296 | tf.filtersRowIndex = 1;
|
297 | }
|
298 |
|
299 | /**
|
300 | * Set grid-layout default column widths if column widths are not defined
|
301 | * @private
|
302 | */
|
303 | setDefaultColWidths() {
|
304 | let tf = this.tf;
|
305 | if (tf.colWidths.length > 0) {
|
306 | return;
|
307 | }
|
308 |
|
309 | tf.eachCol((k) => {
|
310 | let colW;
|
311 | let cell = tf.dom().rows[tf.getHeadersRowIndex()].cells[k];
|
312 | if (cell.width !== '') {
|
313 | colW = cell.width;
|
314 | } else if (cell.style.width !== '') {
|
315 | colW = parseInt(cell.style.width, 10);
|
316 | } else {
|
317 | colW = this.defaultColWidth;
|
318 | }
|
319 | tf.colWidths[k] = colW;
|
320 | });
|
321 |
|
322 | tf.setColWidths();
|
323 | }
|
324 |
|
325 | /**
|
326 | * Initial table width
|
327 | * @returns {Number}
|
328 | * @private
|
329 | */
|
330 | initialTableWidth() {
|
331 | let tbl = this.tf.dom();
|
332 | let width; //initial table width
|
333 |
|
334 | if (tbl.width !== '') {
|
335 | width = tbl.width;
|
336 | }
|
337 | else if (tbl.style.width !== '') {
|
338 | width = tbl.style.width;
|
339 | } else {
|
340 | width = tbl.clientWidth;
|
341 | }
|
342 | return parseInt(width, 10);
|
343 | }
|
344 |
|
345 | /**
|
346 | * Creates container element
|
347 | * @param {String} tag Tag name
|
348 | * @param {String} className Css class to assign to element
|
349 | * @returns {DOMElement}
|
350 | * @private
|
351 | */
|
352 | createContainer(tag, className) {
|
353 | let element = createElm(tag);
|
354 | element.className = className;
|
355 | return element;
|
356 | }
|
357 |
|
358 | /**
|
359 | * Creates filters row with cells
|
360 | * @returns {HTMLTableRowElement}
|
361 | * @private
|
362 | */
|
363 | createFiltersRow() {
|
364 | let tf = this.tf;
|
365 | let filtersRow = createElm('tr');
|
366 | if (this.filters && tf.fltGrid) {
|
367 | tf.externalFltIds = [];
|
368 | tf.eachCol((j) => {
|
369 | let fltTdId = `${tf.prfxFlt + j + this.prfxGridFltTd + tf.id}`;
|
370 | let cl = createElm(tf.fltCellTag, ['id', fltTdId]);
|
371 | filtersRow.appendChild(cl);
|
372 | tf.externalFltIds[j] = fltTdId;
|
373 | });
|
374 | }
|
375 | return filtersRow;
|
376 | }
|
377 |
|
378 | /**
|
379 | * Generates column elements if necessary and assigns their widths
|
380 | * @private
|
381 | */
|
382 | setColumnElements() {
|
383 | let tf = this.tf;
|
384 | let cols = tag(tf.dom(), 'col');
|
385 | this.tblHasColTag = cols.length > 0;
|
386 |
|
387 | for (let k = (tf.getCellsNb() - 1); k >= 0; k--) {
|
388 | let col;
|
389 |
|
390 | if (!this.tblHasColTag) {
|
391 | col = createElm('col');
|
392 | tf.dom().insertBefore(col, tf.dom().firstChild);
|
393 | } else {
|
394 | col = cols[k];
|
395 | }
|
396 | col.style.width = tf.colWidths[k];
|
397 | this.colElms[k] = col;
|
398 | }
|
399 | this.tblHasColTag = true;
|
400 | }
|
401 |
|
402 | /**
|
403 | * Sets headers row in headers table
|
404 | * @param {HTMLHeadElement} tableHead Table head element
|
405 | * @private
|
406 | */
|
407 | setHeadersRow(tableHead) {
|
408 | if (this.noHeaders) {
|
409 | // Handle table with no headers, assuming here headers do not
|
410 | // exist
|
411 | tableHead.appendChild(createElm('tr'));
|
412 | } else {
|
413 | // Headers row are moved from content table to headers table
|
414 | for (let i = 0; i < this.headRows.length; i++) {
|
415 | let row = this.tf.dom().rows[this.headRows[i]];
|
416 | tableHead.appendChild(row);
|
417 | }
|
418 | }
|
419 | }
|
420 |
|
421 | /**
|
422 | * Sets width defined in configuration to passed element
|
423 | * @param {DOMElement} element DOM element
|
424 | * @private
|
425 | */
|
426 | setConfigWidth(element) {
|
427 | if (!this.width) {
|
428 | return;
|
429 | }
|
430 | if (this.width.indexOf('%') !== -1) {
|
431 | element.style.width = '100%';
|
432 | } else {
|
433 | element.style.width = this.width;
|
434 | }
|
435 | }
|
436 |
|
437 | /**
|
438 | * Returns a list of header IDs used for specifing external sort triggers
|
439 | * @param {HTMLTableRowElement} row DOM row element
|
440 | * @returns {Array} List of IDs
|
441 | * @private
|
442 | */
|
443 | getSortTriggerIds(row) {
|
444 | let tf = this.tf;
|
445 | let sortTriggers = [];
|
446 | tf.eachCol((n) => {
|
447 | let c = row.cells[n];
|
448 | let thId = c.getAttribute('id');
|
449 | if (!thId || thId === '') {
|
450 | thId = `${this.prfxGridTh + n}_${tf.id}`;
|
451 | c.setAttribute('id', thId);
|
452 | }
|
453 | sortTriggers.push(thId);
|
454 | });
|
455 | return sortTriggers;
|
456 | }
|
457 |
|
458 | /**
|
459 | * Removes the grid layout
|
460 | */
|
461 | destroy() {
|
462 | let tf = this.tf;
|
463 | let tbl = tf.dom();
|
464 |
|
465 | if (!this.initialized) {
|
466 | return;
|
467 | }
|
468 | let t = removeElm(tbl);
|
469 | this.tblMainCont.parentNode.insertBefore(t, this.tblMainCont);
|
470 | removeElm(this.tblMainCont);
|
471 |
|
472 | this.tblMainCont = null;
|
473 | this.headTblCont = null;
|
474 | this.headTbl = null;
|
475 | this.tblCont = null;
|
476 |
|
477 | tbl.outerHTML = this.sourceTblHtml;
|
478 | //needed to keep reference of table element for future usage
|
479 | this.tf.tbl = elm(tf.id);
|
480 |
|
481 | this.initialized = false;
|
482 | }
|
483 | }
|