import { Utility } from '../../core/utility'; import { StorageManager } from '../../lib/storage-manager/storage-manager'; import './hide-columns.scss'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const ASYNC_TABLE_IDENT = 'uw-async-table'; const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; const TABLE_HIDER_CONTAINER_CLASS = 'table-hiders'; const TABLE_HIDER_CLASS = 'table-hider'; const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible'; const TABLE_PILL_CLASS = 'table-pill'; const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell'; @Utility({ selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, }) export class HideColumns { _storageManager = new StorageManager('HIDE_COLUMNS'); _element; _elementWrapper; _tableUtilContainer; _tableHiderContainer; headerToHider = new Map(); hiderToHeader = new Map(); addHeaderHider(th, hider) { this.headerToHider.set(th, hider); this.hiderToHeader.set(hider, th); } constructor(element) { if (!element) { throw new Error('Hide Columns utility cannot be setup without an element!'); } // do not provide hide-column ability in tables inside modals or async forms with response if (element.closest('[uw-modal], .async-form__response')) { return false; } this._element = element; const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`); if (!hideColumnsContainer) { throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!'); } this._elementWrapper = hideColumnsContainer; // get or create table utils container this._tableUtilContainer = hideColumnsContainer.querySelector(TABLE_UTILS_CONTAINER_SELECTOR); if (!this._tableUtilContainer) { this._tableUtilContainer = document.createElement('div'); this._tableUtilContainer.setAttribute(TABLE_UTILS_ATTR, ''); const tableContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}] > *`); hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer); } // get or create table hider container before the table this._tableHiderContainer = this._element.previousSibling; if (!this._tableHiderContainer || !this._tableHiderContainer.classList.contains(TABLE_HIDER_CONTAINER_CLASS)) { this._tableHiderContainer = document.createElement('div'); this._tableHiderContainer.classList.add(TABLE_HIDER_CONTAINER_CLASS); this._element.parentElement.insertBefore(this._tableHiderContainer, this._element); } this._element.querySelectorAll('th').forEach(th => this.setupHideButton(th)); } setupHideButton(th) { const preHidden = this.isHiddenColumn(th); const hider = document.createElement('span'); const hiderIcon = document.createElement('i'); hiderIcon.classList.add('fas', 'fa-fw'); hider.appendChild(hiderIcon); const hiderContent = document.createElement('span'); hiderContent.classList.add('table-hider__label'); hiderContent.innerHTML = th.innerText; hider.appendChild(hiderContent); this.addHeaderHider(th, hider); th.addEventListener('mouseover', () => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); }); th.addEventListener('mouseout', () => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } }); hider.addEventListener('click', (event) => { event.preventDefault(); this.switchColumnDisplay(th, hider); this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); }); hider.addEventListener('mouseover', () => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); const currentlyHidden = this.isHiddenColumn(th); this.updateHiderIcon(hider, !currentlyHidden); }); hider.addEventListener('mouseout', () => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } const currentlyHidden = this.isHiddenColumn(th); this.updateHiderIcon(hider, currentlyHidden); }); // reposition hider on each window resize event window.addEventListener('resize', () => this.repositionHider(hider)); if (preHidden) { this._tableUtilContainer.appendChild(hider); } else { this.hideHiderBehindHeader(hider); } this.updateColumnDisplay(th.cellIndex, preHidden); this.updateHider(hider, preHidden); this._tableHiderContainer.children.forEach(hider => this.repositionHider(hider)); } switchColumnDisplay(th, hider) { const hidden = !this.isHiddenColumn(th); this.updateColumnDisplay(th.cellIndex, hidden); this.updateHider(hider, hidden); // persist new hidden setting for column this._storageManager.save(this.getStorageKey(th), hidden); } updateColumnDisplay(columnIndex, hidden) { this._element.getElementsByTagName('tr').forEach(row => { const cell = row.cells[columnIndex]; if (cell) { if (hidden) { cell.classList.add(CELL_HIDDEN_CLASS); } else { cell.classList.remove(CELL_HIDDEN_CLASS); } } }); } updateHider(hider, hidden) { if (hidden) { hider.classList.remove(TABLE_HIDER_CLASS); hider.classList.add(TABLE_PILL_CLASS); this._tableUtilContainer.appendChild(hider); } else { hider.classList.remove(TABLE_PILL_CLASS); hider.classList.add(TABLE_HIDER_CLASS); this.hideHiderBehindHeader(hider); } this.updateHiderIcon(hider, hidden); } updateHiderIcon(hider, hidden) { hider.getElementsByClassName('fas').forEach(hiderIcon => { hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash'); hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye'); }); } hideHiderBehindHeader(hider) { if (!this._tableHiderContainer.contains(hider)) { this._tableHiderContainer.appendChild(hider); } this.repositionHider(hider); // remove visible class if necessary hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } repositionHider(hider) { const thR = this.hiderToHeader.get(hider).getBoundingClientRect(), hR = hider.getBoundingClientRect(), pR = this._tableHiderContainer.getBoundingClientRect(); hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; hider.style.top = (thR.top - pR.top + thR.height) + 'px'; } getStorageKey(th) { // get handler name const handlerIdent = document.querySelector('[uw-handler]').getAttribute('uw-handler'); // get hide-columns container ident (if not present, use table index in document as fallback) let tIdent = this._elementWrapper.getAttribute(HIDE_COLUMNS_CONTAINER_IDENT); if (!tIdent) { const tablesInDocument = document.getElementsByTagName('TABLE'); for (let i = 0; i < tablesInDocument.length; i++) { if (tablesInDocument[i] === this._element) { tIdent = i; break; } } } // check for unique table header ident from backend (if not present, use cell index as fallback) let thIdent = th.getAttribute(TABLE_HEADER_IDENT); if (!thIdent) { thIdent = th.cellIndex; } return `${handlerIdent}__${tIdent}__${thIdent}`; } isEmptyColumn(columnIndex) { for (let row of this._element.getElementsByTagName('TR')) { if (row.children.length <= columnIndex) { throw new Error('Invalid column index for table'); } const cell = row.children[columnIndex]; if (cell.tagName === 'TH') continue; if (this._element.closest(`[${ASYNC_TABLE_IDENT}]`)) { for (let child of cell.children) { if (!isEmptyElement(child)) return false; } return true; } else { return isEmptyElement(cell); } } } isHiddenColumn(th) { const hidden = this._storageManager.load(this.getStorageKey(th)), emptyColumn = this.isEmptyColumn(th.cellIndex); return hidden === true || hidden === undefined && emptyColumn; } } function isEmptyElement(element) { for (let child of element.childNodes) { if (child.nodeName !== '#comment') return false; } return true; }