This repository has been archived on 2024-10-24. You can view files and clone it, but cannot push or open issues or pull requests.
fradrive-old/frontend/src/utils/hide-columns/hide-columns.js

399 lines
14 KiB
JavaScript

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)
);
}