UNPKG

14.2 kBJavaScriptView Raw
1import {Feature} from '../feature';
2import {createElm, removeElm, elm, tag} from '../dom';
3import {addEvt, targetEvt} from '../event';
4import {contains} from '../string';
5import {NONE} from '../const';
6import {
7 defaultsBool, defaultsStr, defaultsNb, defaultsArr
8} from '../settings';
9
10/**
11 * Grid layout, table with fixed headers
12 */
13export 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}