329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
import { Utility } from '../../core/utility';
|
|
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
|
|
import './hide-columns.sass';
|
|
|
|
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 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';
|
|
|
|
@Utility({
|
|
selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`,
|
|
})
|
|
export class HideColumns {
|
|
|
|
_storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL });
|
|
|
|
_element;
|
|
_elementWrapper;
|
|
_tableUtilContainer;
|
|
|
|
_autoHide;
|
|
|
|
_mutationObserver;
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
[...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th));
|
|
|
|
this._mutationObserver = new MutationObserver(this._tableMutated.bind(this));
|
|
this._mutationObserver.observe(this._element, { childList: true, subtree: true });
|
|
}
|
|
|
|
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.getAttribute(HIDE_COLUMNS_HIDER_LABEL) || 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();
|
|
event.stopPropagation();
|
|
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);
|
|
});
|
|
|
|
new ResizeObserver(() => { this.repositionHider(hider); }).observe(th);
|
|
|
|
// reposition hider on each window resize event
|
|
// window.addEventListener('resize', () => this.repositionHider(hider));
|
|
|
|
this.updateColumnDisplay(this.colIndex(th), preHidden);
|
|
this.updateHider(hider, preHidden);
|
|
|
|
if (preHidden) {
|
|
this._tableUtilContainer.appendChild(hider);
|
|
} else {
|
|
this.hideHiderBehindHeader(hider);
|
|
}
|
|
}
|
|
|
|
switchColumnDisplay(th, hider) {
|
|
const hidden = !this.isHiddenColumn(th);
|
|
const originalColspan = Math.max(1, th.getAttribute(CELL_ORIGINAL_COLSPAN)) || 1;
|
|
const colspan = Math.max(1, th.colSpan) || 1;
|
|
const columnIndex = this.colIndex(th);
|
|
|
|
for (let i = 0; i < Math.max(colspan, originalColspan); i++) {
|
|
this.updateColumnDisplay(columnIndex + i, hidden);
|
|
}
|
|
this.updateHider(hider, hidden);
|
|
|
|
// persist new hidden setting for column
|
|
if ((hidden && this.isEmptyColumn(columnIndex) && this._autoHide) || (!hidden && (!this.isEmptyColumn(columnIndex) || !this._autoHide))) {
|
|
this._storageManager.remove(this.getStorageKey(th));
|
|
} else {
|
|
this._storageManager.save(this.getStorageKey(th), hidden);
|
|
}
|
|
}
|
|
|
|
updateColumnDisplay(columnIndex, hidden) {
|
|
this._element.getElementsByTagName('tr').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;
|
|
|
|
if (hidden) {
|
|
if (colspan > 1) {
|
|
if (!originalColspan) {
|
|
cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan);
|
|
}
|
|
cell.colSpan--;
|
|
} else {
|
|
cell.classList.add(CELL_HIDDEN_CLASS);
|
|
}
|
|
} else {
|
|
if (cell.classList.contains(CELL_HIDDEN_CLASS)) {
|
|
cell.classList.remove(CELL_HIDDEN_CLASS);
|
|
} else if (originalColspan && colspan < originalColspan) {
|
|
cell.colSpan++;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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.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) {
|
|
// console.log('_tableMutated', mutationList, observer);
|
|
|
|
if (!Array.from(mutationList).some(mutation => mutation.type === 'childList'))
|
|
return;
|
|
|
|
[...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.updateColumnDisplay(this.colIndex(th), this.isHiddenColumn(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.getElementsByTagName('tr')) {
|
|
const cell = this.getCol(row, columnIndex);
|
|
if (cell.matches('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);
|
|
}
|
|
}
|
|
}
|
|
|
|
isHiddenColumn(th) {
|
|
const hidden = this._storageManager.load(this.getStorageKey(th)),
|
|
emptyColumn = this.isEmptyColumn(this.colIndex(th));
|
|
return hidden === true || hidden === undefined && emptyColumn && this._autoHide;
|
|
}
|
|
|
|
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) {
|
|
if (!cell)
|
|
return 0;
|
|
|
|
const rowParent = cell.closest('tr');
|
|
|
|
if (!rowParent)
|
|
return 0;
|
|
|
|
let i = 0;
|
|
for (const sibling of Array.from(rowParent.cells).slice(0, cell.cellIndex)) {
|
|
i += this.colSpan(sibling);
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
getCol(row, columnIndex) {
|
|
let c = 0;
|
|
|
|
for (const cell of row.cells) {
|
|
c += cell ? this.colSpan(cell) : 1;
|
|
|
|
if (columnIndex < c)
|
|
return cell;
|
|
}
|
|
}
|
|
}
|
|
|
|
function isEmptyElement(element) {
|
|
for (let child of element.childNodes) {
|
|
if (child.nodeName !== '#comment')
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|