UNPKG

14.6 kBJavaScriptView Raw
1/**
2 * Copyright IBM Corp. 2016, 2018
3 *
4 * This source code is licensed under the Apache-2.0 license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import settings from '../../globals/js/settings';
9import mixin from '../../globals/js/misc/mixin';
10import createComponent from '../../globals/js/mixins/create-component';
11import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
12import eventedState from '../../globals/js/mixins/evented-state';
13import handles from '../../globals/js/mixins/handles';
14import eventMatches from '../../globals/js/misc/event-matches';
15import on from '../../globals/js/misc/on';
16
17const toArray = (arrayLike) => Array.prototype.slice.call(arrayLike);
18
19class DataTable extends mixin(
20 createComponent,
21 initComponentBySearch,
22 eventedState,
23 handles
24) {
25 /**
26 * Data Table
27 * @extends CreateComponent
28 * @extends InitComponentBySearch
29 * @extends EventedState
30 * @param {HTMLElement} element The root element of tables
31 * @param {object} [options] the... options
32 * @param {string} [options.selectorInit] selector initialization
33 * @param {string} [options.selectorExpandCells] css selector for expand
34 * @param {string} [options.expandableRow] css selector for expand
35 * @param {string} [options.selectorParentRows] css selector for rows housing expansion
36 * @param {string} [options.selectorTableBody] root css for table body
37 * @param {string} [options.eventTrigger] selector for event bubble capture points
38 * @param {string} [options.eventParentContainer] used find the bubble container
39 */
40 constructor(element, options) {
41 super(element, options);
42
43 this.container = element.parentNode;
44 this.toolbarEl = this.element.querySelector(this.options.selectorToolbar);
45 this.batchActionEl = this.element.querySelector(
46 this.options.selectorActions
47 );
48 this.countEl = this.element.querySelector(this.options.selectorCount);
49 this.cancelEl = this.element.querySelector(
50 this.options.selectorActionCancel
51 );
52 this.tableHeaders = this.element.querySelectorAll('th');
53 this.tableBody = this.element.querySelector(this.options.selectorTableBody);
54 this.expandCells = [];
55 this.expandableRows = [];
56 this.parentRows = [];
57
58 this.refreshRows();
59
60 this.manage(on(this.element, 'mouseover', this._expandableHoverToggle));
61 this.manage(on(this.element, 'mouseout', this._expandableHoverToggle));
62
63 this.manage(
64 on(this.element, 'click', (evt) => {
65 const eventElement = eventMatches(evt, this.options.eventTrigger);
66 const searchContainer = this.element.querySelector(
67 this.options.selectorToolbarSearchContainer
68 );
69
70 if (eventElement) {
71 this._toggleState(eventElement, evt);
72 }
73
74 if (searchContainer) {
75 this._handleDocumentClick(evt);
76 }
77 })
78 );
79
80 this.manage(on(this.element, 'keydown', this._keydownHandler));
81
82 this.state = {
83 checkboxCount: 0,
84 };
85 }
86
87 _handleDocumentClick(evt) {
88 const searchContainer = this.element.querySelector(
89 this.options.selectorToolbarSearchContainer
90 );
91 const searchEvent = eventMatches(evt, this.options.selectorSearchMagnifier);
92 const activeSearch = searchContainer.classList.contains(
93 this.options.classToolbarSearchActive
94 );
95
96 if (searchContainer && searchEvent) {
97 this.activateSearch(searchContainer);
98 }
99
100 if (activeSearch) {
101 this.deactivateSearch(searchContainer, evt);
102 }
103 }
104
105 activateSearch(container) {
106 const input = container.querySelector(this.options.selectorSearchInput);
107 container.classList.add(this.options.classToolbarSearchActive);
108 input.focus();
109 }
110
111 deactivateSearch(container, evt) {
112 const trigger = container.querySelector(
113 this.options.selectorSearchMagnifier
114 );
115 const input = container.querySelector(this.options.selectorSearchInput);
116 const svg = trigger.querySelector('svg');
117 if (
118 input.value.length === 0 &&
119 evt.target !== input &&
120 evt.target !== trigger &&
121 evt.target !== svg
122 ) {
123 container.classList.remove(this.options.classToolbarSearchActive);
124 trigger.focus();
125 }
126
127 if (evt.which === 27 && evt.target === input) {
128 container.classList.remove(this.options.classToolbarSearchActive);
129 trigger.focus();
130 }
131 }
132
133 _sortToggle = (detail) => {
134 const { element, previousValue } = detail;
135
136 toArray(this.tableHeaders).forEach((header) => {
137 const sortEl = header.querySelector(this.options.selectorTableSort);
138
139 if (sortEl !== null && sortEl !== element) {
140 sortEl.classList.remove(this.options.classTableSortActive);
141 sortEl.classList.remove(this.options.classTableSortAscending);
142 }
143 });
144
145 if (!previousValue) {
146 element.dataset.previousValue = 'ascending';
147 element.classList.add(this.options.classTableSortActive);
148 element.classList.add(this.options.classTableSortAscending);
149 } else if (previousValue === 'ascending') {
150 element.dataset.previousValue = 'descending';
151 element.classList.add(this.options.classTableSortActive);
152 element.classList.remove(this.options.classTableSortAscending);
153 } else if (previousValue === 'descending') {
154 element.removeAttribute('data-previous-value');
155 element.classList.remove(this.options.classTableSortActive);
156 element.classList.remove(this.options.classTableSortAscending);
157 }
158 };
159
160 _selectToggle = (detail) => {
161 const { element } = detail;
162 const { checked } = element;
163
164 // increment the count
165 this.state.checkboxCount += checked ? 1 : -1;
166 this.countEl.textContent = this.state.checkboxCount;
167
168 const row = element.parentNode.parentNode;
169
170 row.classList.toggle(this.options.classTableSelected);
171
172 // toggle on/off batch action bar
173 this._actionBarToggle(this.state.checkboxCount > 0);
174 };
175
176 _selectAllToggle = ({ element }) => {
177 const { checked } = element;
178
179 const inputs = toArray(
180 this.element.querySelectorAll(this.options.selectorCheckbox)
181 );
182
183 this.state.checkboxCount = checked ? inputs.length - 1 : 0;
184
185 inputs.forEach((item) => {
186 item.checked = checked;
187
188 const row = item.parentNode.parentNode;
189 if (checked && row) {
190 row.classList.add(this.options.classTableSelected);
191 } else {
192 row.classList.remove(this.options.classTableSelected);
193 }
194 });
195
196 this._actionBarToggle(this.state.checkboxCount > 0);
197
198 if (this.batchActionEl) {
199 this.countEl.textContent = this.state.checkboxCount;
200 }
201 };
202
203 _actionBarCancel = () => {
204 const inputs = toArray(
205 this.element.querySelectorAll(this.options.selectorCheckbox)
206 );
207 const row = toArray(
208 this.element.querySelectorAll(this.options.selectorTableSelected)
209 );
210
211 row.forEach((item) => {
212 item.classList.remove(this.options.classTableSelected);
213 });
214
215 inputs.forEach((item) => {
216 item.checked = false;
217 });
218
219 this.state.checkboxCount = 0;
220 this._actionBarToggle(false);
221
222 if (this.batchActionEl) {
223 this.countEl.textContent = this.state.checkboxCount;
224 }
225 };
226
227 _actionBarToggle = (toggleOn) => {
228 let handleTransitionEnd;
229 const transition = (evt) => {
230 if (handleTransitionEnd) {
231 handleTransitionEnd = this.unmanage(handleTransitionEnd).release();
232 }
233
234 if (evt.target.matches(this.options.selectorActions)) {
235 if (this.batchActionEl.dataset.active === 'false') {
236 this.batchActionEl.setAttribute('tabIndex', -1);
237 } else {
238 this.batchActionEl.setAttribute('tabIndex', 0);
239 }
240 }
241 };
242
243 if (toggleOn) {
244 this.batchActionEl.dataset.active = true;
245 this.batchActionEl.classList.add(this.options.classActionBarActive);
246 } else if (this.batchActionEl) {
247 this.batchActionEl.dataset.active = false;
248 this.batchActionEl.classList.remove(this.options.classActionBarActive);
249 }
250 if (this.batchActionEl) {
251 handleTransitionEnd = this.manage(
252 on(this.batchActionEl, 'transitionend', transition)
253 );
254 }
255 };
256
257 _rowExpandToggle = ({ element, forceExpand }) => {
258 const parent = element.closest(this.options.eventParentContainer);
259 // NOTE: `data-previous-value` keeps UI state before this method makes change in style
260 // eslint-disable-next-line eqeqeq
261 const shouldExpand =
262 forceExpand != null
263 ? forceExpand
264 : element.dataset.previousValue === undefined ||
265 element.dataset.previousValue === 'expanded';
266
267 if (shouldExpand) {
268 element.dataset.previousValue = 'collapsed';
269 parent.classList.add(this.options.classExpandableRow);
270 } else {
271 parent.classList.remove(this.options.classExpandableRow);
272 element.dataset.previousValue = 'expanded';
273 const expandHeader = this.element.querySelector(
274 this.options.selectorExpandHeader
275 );
276 if (expandHeader) {
277 expandHeader.dataset.previousValue = 'expanded';
278 }
279 }
280 };
281
282 _rowExpandToggleAll = ({ element }) => {
283 // NOTE: `data-previous-value` keeps UI state before this method makes change in style
284 const shouldExpand =
285 element.dataset.previousValue === undefined ||
286 element.dataset.previousValue === 'expanded';
287 element.dataset.previousValue = shouldExpand ? 'collapsed' : 'expanded';
288 const expandCells = this.element.querySelectorAll(
289 this.options.selectorExpandCells
290 );
291 Array.prototype.forEach.call(expandCells, (cell) => {
292 this._rowExpandToggle({ element: cell, forceExpand: shouldExpand });
293 });
294 };
295
296 _expandableHoverToggle = (evt) => {
297 const element = eventMatches(evt, this.options.selectorChildRow);
298 if (element) {
299 element.previousElementSibling.classList.toggle(
300 this.options.classExpandableRowHover,
301 evt.type === 'mouseover'
302 );
303 }
304 };
305
306 _toggleState = (element, evt) => {
307 const data = element.dataset;
308 const label = data.label ? data.label : '';
309 const previousValue = data.previousValue ? data.previousValue : '';
310 const initialEvt = evt;
311
312 this.changeState({
313 group: data.event,
314 element,
315 label,
316 previousValue,
317 initialEvt,
318 });
319 };
320
321 _keydownHandler = (evt) => {
322 const searchContainer = this.element.querySelector(
323 this.options.selectorToolbarSearchContainer
324 );
325 const searchEvent = eventMatches(evt, this.options.selectorSearchMagnifier);
326 const activeSearch = searchContainer.classList.contains(
327 this.options.classToolbarSearchActive
328 );
329
330 if (evt.which === 27) {
331 this._actionBarCancel();
332 }
333
334 if (searchContainer && searchEvent && evt.which === 13) {
335 this.activateSearch(searchContainer);
336 }
337
338 if (activeSearch && evt.which === 27) {
339 this.deactivateSearch(searchContainer, evt);
340 }
341 };
342
343 _changeState(detail, callback) {
344 this[this.constructor.eventHandlers[detail.group]](detail);
345 callback();
346 }
347
348 refreshRows = () => {
349 const newExpandCells = toArray(
350 this.element.querySelectorAll(this.options.selectorExpandCells)
351 );
352 const newExpandableRows = toArray(
353 this.element.querySelectorAll(this.options.selectorExpandableRows)
354 );
355 const newParentRows = toArray(
356 this.element.querySelectorAll(this.options.selectorParentRows)
357 );
358
359 // check if this is a refresh or the first time
360 if (this.parentRows.length > 0) {
361 const diffParentRows = newParentRows.filter(
362 (newRow) => !this.parentRows.some((oldRow) => oldRow === newRow)
363 );
364
365 // check if there are expandable rows
366 if (newExpandableRows.length > 0) {
367 const diffExpandableRows = diffParentRows.map(
368 (newRow) => newRow.nextElementSibling
369 );
370 const mergedExpandableRows = [
371 ...toArray(this.expandableRows),
372 ...toArray(diffExpandableRows),
373 ];
374 this.expandableRows = mergedExpandableRows;
375 }
376 } else if (newExpandableRows.length > 0) {
377 this.expandableRows = newExpandableRows;
378 }
379
380 this.expandCells = newExpandCells;
381 this.parentRows = newParentRows;
382 };
383
384 static components /* #__PURE_CLASS_PROPERTY__ */ = new WeakMap();
385
386 // UI Events
387 static eventHandlers /* #__PURE_CLASS_PROPERTY__ */ = {
388 expand: '_rowExpandToggle',
389 expandAll: '_rowExpandToggleAll',
390 sort: '_sortToggle',
391 select: '_selectToggle',
392 'select-all': '_selectAllToggle',
393 'action-bar-cancel': '_actionBarCancel',
394 };
395
396 static get options() {
397 const { prefix } = settings;
398 return {
399 selectorInit: `[data-table]`,
400 selectorToolbar: `.${prefix}--table--toolbar`,
401 selectorActions: `.${prefix}--batch-actions`,
402 selectorCount: '[data-items-selected]',
403 selectorActionCancel: `.${prefix}--batch-summary__cancel`,
404 selectorCheckbox: `.${prefix}--checkbox`,
405 selectorExpandHeader: `th.${prefix}--table-expand`,
406 selectorExpandCells: `td.${prefix}--table-expand`,
407 selectorExpandableRows: `.${prefix}--expandable-row`,
408 selectorParentRows: `.${prefix}--parent-row`,
409 selectorChildRow: '[data-child-row]',
410 selectorTableBody: 'tbody',
411 selectorTableSort: `.${prefix}--table-sort`,
412 selectorTableSelected: `.${prefix}--data-table--selected`,
413 selectorToolbarSearchContainer: `.${prefix}--toolbar-search-container-expandable`,
414 selectorSearchMagnifier: `.${prefix}--search-magnifier`,
415 selectorSearchInput: `.${prefix}--search-input`,
416 classExpandableRow: `${prefix}--expandable-row`,
417 classExpandableRowHidden: `${prefix}--expandable-row--hidden`,
418 classExpandableRowHover: `${prefix}--expandable-row--hover`,
419 classTableSortAscending: `${prefix}--table-sort--ascending`,
420 classTableSortActive: `${prefix}--table-sort--active`,
421 classToolbarSearchActive: `${prefix}--toolbar-search-container-active`,
422 classActionBarActive: `${prefix}--batch-actions--active`,
423 classTableSelected: `${prefix}--data-table--selected`,
424 eventBeforeExpand: `data-table-beforetoggleexpand`,
425 eventAfterExpand: `data-table-aftertoggleexpand`,
426 eventBeforeExpandAll: `data-table-beforetoggleexpandall`,
427 eventAfterExpandAll: `data-table-aftertoggleexpandall`,
428 eventBeforeSort: `data-table-beforetogglesort`,
429 eventAfterSort: `data-table-aftertogglesort`,
430 eventTrigger: '[data-event]',
431 eventParentContainer: '[data-parent-row]',
432 };
433 }
434}
435
436export default DataTable;