import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './hide-columns.sass'; import { TableIndices } from '../../lib/table/table'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const HIDE_COLUMNS_HIDER_LABEL = 'uw-hide-columns--hider-label'; const HIDE_COLUMNS_NO_HIDE = 'uw-hide-columns--no-hide'; const HIDE_COLUMNS_DEFAULT_HIDDEN = 'uw-hide-column-default-hidden'; const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; 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'; const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan'; const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized'; @Utility({ selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, }) export class HideColumns { _storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL }); _eventManager; _element; _elementWrapper; _tableUtilContainer; _autoHide; _tableIndices; headerToHider = new Map(); hiderToHeader = new Map(); addHeaderHider(th, hider) { this.headerToHider.set(th, hider); this.hiderToHeader.set(hider, th); } constructor(element) { this._autoHide = this._storageManager.load('autoHide', {}) || false; if (!element) throw new Error('Hide Columns utility cannot be setup without an element!'); // do not provide hide-column ability in tables inside modals, async forms with response or tail.datetime instances if (element.closest('[uw-modal], .async-form__response, .tail-datetime-calendar')) return false; if (element.classList.contains(HIDE_COLUMNS_INITIALIZED)) return false; this._element = element; this._eventManager = new EventManager(); this._tableIndices = new TableIndices(this._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); } [...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th)); this._eventManager.registerNewMutationObserver(this._tableMutated.bind(this), this._element, { childList: true, subtree: true }); this._element.classList.add(HIDE_COLUMNS_INITIALIZED); } destroy() { this._eventManager.cleanUp(); this._tableUtilContainer.remove(); this._element.classList.remove(HIDE_COLUMNS_INITIALIZED); } setupHideButton(th) { const preHidden = this.isHiddenTH(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.getAttribute(HIDE_COLUMNS_HIDER_LABEL) || th.innerText; hider.appendChild(hiderContent); this.addHeaderHider(th, hider); const mouseOverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); }).bind(this), th); this._eventManager.registerNewListener(mouseOverEvent); const mouseOutEvent = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } }).bind(this), th); this._eventManager.registerNewListener(mouseOutEvent); const hideClickEvent = new EventWrapper(EVENT_TYPE.CLICK, ((event) => { event.preventDefault(); event.stopPropagation(); this.switchColumnDisplay(th); // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); }).bind(this), hider); this._eventManager.registerNewListener(hideClickEvent); const mouseOverHider = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, !currentlyHidden); }).bind(this), hider); this._eventManager.registerNewListener(mouseOverHider); const mouseOutHider = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, currentlyHidden); }).bind(this), hider); this._eventManager.registerNewListener(mouseOutHider); new ResizeObserver(() => { this.repositionHider(hider); }).observe(th); // reposition hider on each window resize event // window.addEventListener('resize', () => this.repositionHider(hider)); this.switchColumnDisplay(th, preHidden); } switchColumnDisplay(th, hidden) { hidden = typeof(hidden) === 'undefined' ? !this.isHiddenTH(th) : !!hidden; Array.from(this.cellColumns(th)).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden)); } updateColumnDisplay(columnIndex, hidden) { // console.debug('updateColumnDisplay', { columnIndex, hidden }); Array.from(this._element.rows).forEach(row => { const cell = this.getCol(row, columnIndex); if (cell) { const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); const colspan = Math.max(1, cell.colSpan) || 1; const visibleColumns = this.cellColumns(cell).reduce((count, cColumnIndex) => (cColumnIndex === columnIndex ? hidden : this.isHiddenColumn(cColumnIndex)) ? count : count + 1, 0); // if (cell.tagName === 'TH') { // console.debug({cell, originalColspan, colspan, visibleColumns, isHidden: cell.classList.contains(CELL_HIDDEN_CLASS)}); // } if (visibleColumns <= 0) { cell.classList.add(CELL_HIDDEN_CLASS); } else { cell.classList.remove(CELL_HIDDEN_CLASS); if (colspan !== visibleColumns) { if (!originalColspan) { cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan); } cell.colSpan = visibleColumns; } } } }); const touchedColumns = new Array(); this.columnTHs(columnIndex) .forEach(th => { touchedColumns.push(...this.cellColumns(th)); if (!this._element.classList.contains(HIDE_COLUMNS_INITIALIZED)) return; const thHidden = this.cellColumns(th).every(cColumnIndex => cColumnIndex === columnIndex ? hidden : this.isHiddenColumn(cColumnIndex)); // persist new hidden setting for column if (thHidden == this.isDefaultHiddenTH(th)) { this._storageManager.remove(this.getStorageKey(th)); } else { this._storageManager.save(this.getStorageKey(th), thHidden); } }); touchedColumns.flatMap(cColumnIndex => this.columnTHs(cColumnIndex)) .forEach(th => { const hider = this.headerToHider.get(th); if (!hider) return; this.updateHider(hider, this.hiderStatus(th)); }); } updateHider(hider, hidden) { // console.debug({hider, hidden, columnIndex: this.colIndex(this.hiderToHeader.get(hider)), colSpan: this.colSpan(this.hiderToHeader.get(hider))}); 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) { Array.from(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.hiderToHeader.get(hider).contains(hider)) { this.hiderToHeader.get(hider).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(); hider.style.left = (thR.width/2 - hR.width/2) + 'px'; hider.style.top = thR.height + 'px'; } _tableMutated(mutationList) { if (!Array.from(mutationList).some(mutation => mutation.type === 'childList')) return; if (Array.from(mutationList).every(mRecord => mRecord.type === 'childList' && [...mRecord.addedNodes, ...mRecord.removedNodes].every(isTableHider))) return; // console.debug('_tableMutated', { mutationList }); this._tableIndices = new TableIndices(this._element, { colSpan: this.colSpan.bind(this) }); Array.from(this._element.rows) .flatMap(row => Array.from(row.cells)) .filter(th => th.tagName === 'TH' && !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)) .forEach(th => this.updateColumnDisplay(this.colIndex(th), this.isHiddenTH(th))); } 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 = this.colIndex(th); } return `${handlerIdent}__${tIdent}__${thIdent}`; } isEmptyColumn(columnIndex) { for (let row of this._element.rows) { const cell = this.getCol(row, columnIndex); if (!cell || cell.tagName == 'TH') continue; if (cell.querySelector('.table__td-content')) { for (let child of cell.children) { if (!isEmptyElement(child)) return false; } return true; } else { return isEmptyElement(cell); } } } columnTHs(columnIndex) { return Array.from(this._element.rows) .map(row => this.getCol(row, columnIndex)) .filter(cell => cell && cell.tagName === 'TH'); } cellColumns(cell) { const columnIndex = this.colIndex(cell); return Array.from(new Array(this.colSpan(cell)), (_x, i) => columnIndex + i); } isHiddenTH(th) { return this.cellColumns(th).every(columnIndex => this.isHiddenColumn(columnIndex)); } hiderStatus(th) { const columnsHidden = this.isHiddenTH(th); const shadowed = this.cellColumns(th).every(columnIndex => this.isHiddenColumn(columnIndex, this.columnTHs(columnIndex).filter(oTH => oTH !== th))); const isFirst = this.cellColumns(th).some(columnIndex => this.columnTHs(columnIndex)[0] === th); // console.debug("hiderStatus", { th, columnsHidden, shadowed, isFirst }); return columnsHidden && (!shadowed || isFirst); } isDefaultHiddenTH(th) { return this.cellColumns(th).every(columnIndex => this.isDefaultHiddenColumn(columnIndex, Array.of(th))); } isHiddenColumn(columnIndex, ths) { ths = ths === undefined ? this.columnTHs(columnIndex) : ths; const hidden = ths.map(th => this._storageManager.load(this.getStorageKey(th))); return hidden.every(h => h === undefined) ? this.isDefaultHiddenColumn(columnIndex, ths) : hidden.some(h => h); } isDefaultHiddenColumn(columnIndex, ths) { ths = ths === undefined ? this.columnTHs(columnIndex) : ths; return this.isEmptyColumn(columnIndex) && this._autoHide || ths.some(th => th.hasAttribute(HIDE_COLUMNS_DEFAULT_HIDDEN)); } colSpan(cell) { if (!cell) return 1; const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); const colspan = Math.max(1, cell.colSpan) || 1; return originalColspan ? Math.max(colspan, originalColspan) : colspan; } colIndex(cell) { return this._tableIndices.colIndex(cell); } getCol(row, col) { return this._tableIndices.getCell(row.rowIndex, col); } } function isEmptyElement(element) { for (let child of element.childNodes) { if (child.nodeName !== '#comment') return false; } return true; } function isTableHider(element) { return element && element.classList && ( element.classList.contains(TABLE_HIDER_CLASS) || element.classList.contains(TABLE_HIDER_VISIBLE_CLASS) || element.classList.contains(TABLE_PILL_CLASS) ); }