From c4ce98159c829bca6302939929ace75b385d6c9f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 Jul 2022 08:25:45 +0200 Subject: [PATCH] collapsible.js: Use ES6's class syntax --- public/js/icinga/behavior/collapsible.js | 686 +++++++++++------------ 1 file changed, 343 insertions(+), 343 deletions(-) diff --git a/public/js/icinga/behavior/collapsible.js b/public/js/icinga/behavior/collapsible.js index 4bb34d3e3..b70a12932 100644 --- a/public/js/icinga/behavior/collapsible.js +++ b/public/js/icinga/behavior/collapsible.js @@ -11,398 +11,398 @@ * * @param icinga Icinga The current Icinga Object */ - var Collapsible = function(icinga) { - Icinga.EventListener.call(this, icinga); + class Collapsible extends Icinga.EventListener { + constructor(icinga) { + super(icinga); - this.on('layout-change', this.onLayoutChange, this); - this.on('rendered', '#main > .container, #modal-content', this.onRendered, this); - this.on('click', '.collapsible + .collapsible-control, .collapsible > .collapsible-control', - this.onControlClicked, this); + this.on('layout-change', this.onLayoutChange, this); + this.on('rendered', '#main > .container, #modal-content', this.onRendered, this); + this.on('click', '.collapsible + .collapsible-control, .collapsible > .collapsible-control', + this.onControlClicked, this); - this.icinga = icinga; - this.defaultVisibleRows = 2; - this.defaultVisibleHeight = 36; + this.icinga = icinga; + this.defaultVisibleRows = 2; + this.defaultVisibleHeight = 36; - this.state = new Icinga.Storage.StorageAwareMap.withStorage( - Icinga.Storage.BehaviorStorage('collapsible'), - 'expanded' - ) - .on('add', this.onExpand, this) - .on('delete', this.onCollapse, this); - }; - - Collapsible.prototype = new Icinga.EventListener(); - - /** - * Initializes all collapsibles. Triggered on rendering of a container. - * - * @param event Event The `onRender` event triggered by the rendered container - */ - Collapsible.prototype.onRendered = function(event) { - let _this = event.data.self, - toCollapse = [], - toExpand = []; - - event.target.querySelectorAll('.collapsible').forEach(collapsible => { - // Assumes that any newly rendered elements are expanded - if (! ('canCollapse' in collapsible.dataset) && _this.canCollapse(collapsible)) { - if (_this.setupCollapsible(collapsible)) { - toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]); - } else if (_this.isDetails(collapsible)) { - // Except if it's a
element, which may not be expanded by default - toExpand.push(collapsible); - } - } - }); - - // Elements are all collapsed in a row now, after height calculations are done. - // This avoids reflows since instantly collapsing an element will cause one if - // the height of the next element is being calculated. - for (const collapseInfo of toCollapse) { - _this.collapse(collapseInfo[0], collapseInfo[1]); + this.state = new Icinga.Storage.StorageAwareMap.withStorage( + Icinga.Storage.BehaviorStorage('collapsible'), + 'expanded' + ) + .on('add', this.onExpand, this) + .on('delete', this.onCollapse, this); } - for (const collapsible of toExpand) { - _this.expand(collapsible); - } - }; + /** + * Initializes all collapsibles. Triggered on rendering of a container. + * + * @param event Event The `onRender` event triggered by the rendered container + */ + onRendered(event) { + let _this = event.data.self, + toCollapse = [], + toExpand = []; - /** - * Updates all collapsibles. - * - * @param event Event The `layout-change` event triggered by window resizing or column changes - */ - Collapsible.prototype.onLayoutChange = function(event) { - let _this = event.data.self; - let toCollapse = []; - - document.querySelectorAll('.collapsible').forEach(collapsible => { - if ('canCollapse' in collapsible.dataset) { - if (! _this.canCollapse(collapsible)) { - let toggleSelector = collapsible.dataset.toggleElement; - if (! toggleSelector && ! this.isDetails(collapsible)) { - collapsible.nextElementSibling.remove(); + event.target.querySelectorAll('.collapsible').forEach(collapsible => { + // Assumes that any newly rendered elements are expanded + if (! ('canCollapse' in collapsible.dataset) && _this.canCollapse(collapsible)) { + if (_this.setupCollapsible(collapsible)) { + toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]); + } else if (_this.isDetails(collapsible)) { + // Except if it's a
element, which may not be expanded by default + toExpand.push(collapsible); } - - delete collapsible.dataset.canCollapse; - _this.expand(collapsible); } - } else if (_this.canCollapse(collapsible) && _this.setupCollapsible(collapsible)) { - // It's expanded but shouldn't - toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]); - } - }); + }); - setTimeout(function () { + // Elements are all collapsed in a row now, after height calculations are done. + // This avoids reflows since instantly collapsing an element will cause one if + // the height of the next element is being calculated. for (const collapseInfo of toCollapse) { _this.collapse(collapseInfo[0], collapseInfo[1]); } - }, 0); - }; - /** - * A collapsible got expanded in another window, try to apply this here as well - * - * @param {string} collapsiblePath - */ - Collapsible.prototype.onExpand = function(collapsiblePath) { - let collapsible = document.querySelector(collapsiblePath); - - if (collapsible && 'canCollapse' in collapsible.dataset) { - if ('stateCollapses' in collapsible.dataset) { - this.collapse(collapsible, this.calculateCollapsedHeight(collapsible)); - } else { - this.expand(collapsible); - } - } - }; - - /** - * A collapsible got collapsed in another window, try to apply this here as well - * - * @param {string} collapsiblePath - */ - Collapsible.prototype.onCollapse = function(collapsiblePath) { - let collapsible = document.querySelector(collapsiblePath); - - if (collapsible && this.canCollapse(collapsible)) { - if ('stateCollapses' in collapsible.dataset) { - this.expand(collapsible); - } else { - this.collapse(collapsible, this.calculateCollapsedHeight(collapsible)); - } - } - }; - - /** - * Event handler for toggling collapsibles. Switches the collapsed state of the respective container. - * - * @param event Event The `onClick` event triggered by the clicked collapsible-control element - */ - Collapsible.prototype.onControlClicked = function(event) { - let _this = event.data.self, - target = event.currentTarget; - - let collapsible = target.previousElementSibling; - if (! collapsible) { - collapsible = target.closest('.collapsible'); - } - - if (! collapsible) { - _this.icinga.logger.error( - '[Collapsible] Collapsible control has no associated .collapsible: ', target); - - return; - } else if ('noPersistence' in collapsible.dataset) { - if (collapsible.classList.contains('collapsed')) { + for (const collapsible of toExpand) { _this.expand(collapsible); - } else { - _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible)); - } - } else { - let collapsiblePath = _this.icinga.utils.getCSSPath(collapsible), - stateCollapses = 'stateCollapses' in collapsible.dataset; - - if (_this.state.has(collapsiblePath)) { - _this.state.delete(collapsiblePath); - - if (stateCollapses) { - _this.expand(collapsible); - } else { - _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible)); - } - } else { - _this.state.set(collapsiblePath); - - if (stateCollapses) { - _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible)); - } else { - _this.expand(collapsible); - } } } - if (_this.isDetails(collapsible)) { - // The browser handles these clicks as well, and would toggle the state again - event.preventDefault(); - } - }; + /** + * Updates all collapsibles. + * + * @param event Event The `layout-change` event triggered by window resizing or column changes + */ + onLayoutChange(event) { + let _this = event.data.self; + let toCollapse = []; - /** - * Setup the given collapsible - * - * @param collapsible The given collapsible container element - * - * @returns {boolean} Whether it needs to collapse or not - */ - Collapsible.prototype.setupCollapsible = function (collapsible) { - if (this.isDetails(collapsible)) { - let summary = collapsible.querySelector(':scope > summary'); - if (! summary.classList.contains('collapsible-control')) { - summary.classList.add('collapsible-control'); - } + document.querySelectorAll('.collapsible').forEach(collapsible => { + if ('canCollapse' in collapsible.dataset) { + if (! _this.canCollapse(collapsible)) { + let toggleSelector = collapsible.dataset.toggleElement; + if (! toggleSelector && ! this.isDetails(collapsible)) { + collapsible.nextElementSibling.remove(); + } - if (collapsible.open) { - collapsible.dataset.stateCollapses = ''; - } - } else if (!! collapsible.dataset.toggleElement) { - let toggleSelector = collapsible.dataset.toggleElement, - toggle = collapsible.querySelector(toggleSelector); - if (! toggle && collapsible.nextElementSibling && collapsible.nextElementSibling.matches(toggleSelector)) { - toggle = collapsible.nextElementSibling; - } + delete collapsible.dataset.canCollapse; + _this.expand(collapsible); + } + } else if (_this.canCollapse(collapsible) && _this.setupCollapsible(collapsible)) { + // It's expanded but shouldn't + toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]); + } + }); - if (! toggle) { - this.icinga.logger.error( - '[Collapsible] Control `' + toggleSelector + '` not found in .collapsible', collapsible); - } else if (! toggle.classList.contains('collapsible-control')) { - toggle.classList.add('collapsible-control'); - } - } else { setTimeout(function () { - let collapsibleControl = document - .getElementById('collapsible-control-ghost') - .cloneNode(true); - collapsibleControl.removeAttribute('id'); - collapsible.parentNode.insertBefore(collapsibleControl, collapsible.nextElementSibling); + for (const collapseInfo of toCollapse) { + _this.collapse(collapseInfo[0], collapseInfo[1]); + } }, 0); } - collapsible.dataset.canCollapse = ''; + /** + * A collapsible got expanded in another window, try to apply this here as well + * + * @param {string} collapsiblePath + */ + onExpand(collapsiblePath) { + let collapsible = document.querySelector(collapsiblePath); - if ('noPersistence' in collapsible.dataset) { - return ! ('stateCollapses' in collapsible.dataset); + if (collapsible && 'canCollapse' in collapsible.dataset) { + if ('stateCollapses' in collapsible.dataset) { + this.collapse(collapsible, this.calculateCollapsedHeight(collapsible)); + } else { + this.expand(collapsible); + } + } } - if ('stateCollapses' in collapsible.dataset) { - return this.state.has(this.icinga.utils.getCSSPath(collapsible)); - } else { - return ! this.state.has(this.icinga.utils.getCSSPath(collapsible)); - } - }; + /** + * A collapsible got collapsed in another window, try to apply this here as well + * + * @param {string} collapsiblePath + */ + onCollapse(collapsiblePath) { + let collapsible = document.querySelector(collapsiblePath); + + if (collapsible && this.canCollapse(collapsible)) { + if ('stateCollapses' in collapsible.dataset) { + this.expand(collapsible); + } else { + this.collapse(collapsible, this.calculateCollapsedHeight(collapsible)); + } + } + } + + /** + * Event handler for toggling collapsibles. Switches the collapsed state of the respective container. + * + * @param event Event The `onClick` event triggered by the clicked collapsible-control element + */ + onControlClicked(event) { + let _this = event.data.self, + target = event.currentTarget; + + let collapsible = target.previousElementSibling; + if (! collapsible) { + collapsible = target.closest('.collapsible'); + } + + if (! collapsible) { + _this.icinga.logger.error( + '[Collapsible] Collapsible control has no associated .collapsible: ', target); + + return; + } else if ('noPersistence' in collapsible.dataset) { + if (collapsible.classList.contains('collapsed')) { + _this.expand(collapsible); + } else { + _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible)); + } + } else { + let collapsiblePath = _this.icinga.utils.getCSSPath(collapsible), + stateCollapses = 'stateCollapses' in collapsible.dataset; + + if (_this.state.has(collapsiblePath)) { + _this.state.delete(collapsiblePath); + + if (stateCollapses) { + _this.expand(collapsible); + } else { + _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible)); + } + } else { + _this.state.set(collapsiblePath); + + if (stateCollapses) { + _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible)); + } else { + _this.expand(collapsible); + } + } + } + + if (_this.isDetails(collapsible)) { + // The browser handles these clicks as well, and would toggle the state again + event.preventDefault(); + } + } + + /** + * Setup the given collapsible + * + * @param collapsible The given collapsible container element + * + * @returns {boolean} Whether it needs to collapse or not + */ + setupCollapsible(collapsible) { + if (this.isDetails(collapsible)) { + let summary = collapsible.querySelector(':scope > summary'); + if (! summary.classList.contains('collapsible-control')) { + summary.classList.add('collapsible-control'); + } + + if (collapsible.open) { + collapsible.dataset.stateCollapses = ''; + } + } else if (!! collapsible.dataset.toggleElement) { + let toggleSelector = collapsible.dataset.toggleElement, + toggle = collapsible.querySelector(toggleSelector); + if (! toggle && collapsible.nextElementSibling && collapsible.nextElementSibling.matches(toggleSelector)) { + toggle = collapsible.nextElementSibling; + } + + if (! toggle) { + this.icinga.logger.error( + '[Collapsible] Control `' + toggleSelector + '` not found in .collapsible', collapsible); + } else if (! toggle.classList.contains('collapsible-control')) { + toggle.classList.add('collapsible-control'); + } + } else { + setTimeout(function () { + let collapsibleControl = document + .getElementById('collapsible-control-ghost') + .cloneNode(true); + collapsibleControl.removeAttribute('id'); + collapsible.parentNode.insertBefore(collapsibleControl, collapsible.nextElementSibling); + }, 0); + } + + collapsible.dataset.canCollapse = ''; + + if ('noPersistence' in collapsible.dataset) { + return ! ('stateCollapses' in collapsible.dataset); + } + + if ('stateCollapses' in collapsible.dataset) { + return this.state.has(this.icinga.utils.getCSSPath(collapsible)); + } else { + return ! this.state.has(this.icinga.utils.getCSSPath(collapsible)); + } + } + + /** + * Return an appropriate row element selector + * + * @param collapsible The given collapsible container element + * + * @returns {string} + */ + getRowSelector(collapsible) { + if (!! collapsible.dataset.visibleHeight) { + return ''; + } + + if (collapsible.tagName === 'TABLE') { + return '> tbody > tr'; + } else if (collapsible.tagName === 'UL' || collapsible.tagName === 'OL') { + return '> li:not(.collapsible-control)'; + } - /** - * Return an appropriate row element selector - * - * @param collapsible The given collapsible container element - * - * @returns {string} - */ - Collapsible.prototype.getRowSelector = function(collapsible) { - if (!! collapsible.dataset.visibleHeight) { return ''; } - if (collapsible.tagName === 'TABLE') { - return '> tbody > tr'; - } else if (collapsible.tagName === 'UL' || collapsible.tagName === 'OL') { - return '> li:not(.collapsible-control)'; - } - - return ''; - }; - - /** - * Check whether the given collapsible needs to collapse - * - * @param collapsible The given collapsible container element - * - * @returns {boolean} - */ - Collapsible.prototype.canCollapse = function(collapsible) { - if (this.isDetails(collapsible)) { - return collapsible.querySelector(':scope > summary') !== null; - } - - let rowSelector = this.getRowSelector(collapsible); - if (!! rowSelector) { - let visibleRows = Number(collapsible.dataset.visibleRows); - if (isNaN(visibleRows)) { - visibleRows = this.defaultVisibleRows; - } else if (visibleRows === 0) { - return true; + /** + * Check whether the given collapsible needs to collapse + * + * @param collapsible The given collapsible container element + * + * @returns {boolean} + */ + canCollapse(collapsible) { + if (this.isDetails(collapsible)) { + return collapsible.querySelector(':scope > summary') !== null; } - return collapsible.querySelectorAll(rowSelector).length > visibleRows * 2; - } else { - let maxHeight = Number(collapsible.dataset.visibleHeight); - if (isNaN(maxHeight)) { - maxHeight = this.defaultVisibleHeight; - } else if (maxHeight === 0) { - return true; + let rowSelector = this.getRowSelector(collapsible); + if (!! rowSelector) { + let visibleRows = Number(collapsible.dataset.visibleRows); + if (isNaN(visibleRows)) { + visibleRows = this.defaultVisibleRows; + } else if (visibleRows === 0) { + return true; + } + + return collapsible.querySelectorAll(rowSelector).length > visibleRows * 2; + } else { + let maxHeight = Number(collapsible.dataset.visibleHeight); + if (isNaN(maxHeight)) { + maxHeight = this.defaultVisibleHeight; + } else if (maxHeight === 0) { + return true; + } + + let actualHeight = collapsible.scrollHeight - parseFloat( + window.getComputedStyle(collapsible).getPropertyValue('padding-top') + ); + + return actualHeight >= maxHeight * 2; } - - let actualHeight = collapsible.scrollHeight - parseFloat( - window.getComputedStyle(collapsible).getPropertyValue('padding-top') - ); - - return actualHeight >= maxHeight * 2; - } - }; - - /** - * Calculate the height the given collapsible should have when collapsed - * - * @param collapsible - */ - Collapsible.prototype.calculateCollapsedHeight = function (collapsible) { - let height; - - if (this.isDetails(collapsible)) { - return -1; } - let rowSelector = this.getRowSelector(collapsible); - if (!! rowSelector) { - height = collapsible.scrollHeight; - height -= parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-bottom')); + /** + * Calculate the height the given collapsible should have when collapsed + * + * @param collapsible + */ + calculateCollapsedHeight(collapsible) { + let height; - let visibleRows = Number(collapsible.dataset.visibleRows); - if (isNaN(visibleRows)) { - visibleRows = this.defaultVisibleRows; + if (this.isDetails(collapsible)) { + return -1; } - let rows = Array.from(collapsible.querySelectorAll(rowSelector)).slice(visibleRows); - for (let i = 0; i < rows.length; i++) { - let row = rows[i]; + let rowSelector = this.getRowSelector(collapsible); + if (!! rowSelector) { + height = collapsible.scrollHeight; + height -= parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-bottom')); - if (row.previousElementSibling === null) { // very first element - height -= row.offsetHeight; - height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top')); - } else if (i < rows.length - 1) { // every element but the last one - let prevBottomBorderAt = row.previousElementSibling.offsetTop; - prevBottomBorderAt += row.previousElementSibling.offsetHeight; - height -= row.offsetTop - prevBottomBorderAt + row.offsetHeight; - } else { // the last element - height -= row.offsetHeight; - height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top')); - height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-bottom')); + let visibleRows = Number(collapsible.dataset.visibleRows); + if (isNaN(visibleRows)) { + visibleRows = this.defaultVisibleRows; + } + + let rows = Array.from(collapsible.querySelectorAll(rowSelector)).slice(visibleRows); + for (let i = 0; i < rows.length; i++) { + let row = rows[i]; + + if (row.previousElementSibling === null) { // very first element + height -= row.offsetHeight; + height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top')); + } else if (i < rows.length - 1) { // every element but the last one + let prevBottomBorderAt = row.previousElementSibling.offsetTop; + prevBottomBorderAt += row.previousElementSibling.offsetHeight; + height -= row.offsetTop - prevBottomBorderAt + row.offsetHeight; + } else { // the last element + height -= row.offsetHeight; + height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top')); + height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-bottom')); + } + } + } else { + height = Number(collapsible.dataset.visibleHeight); + if (isNaN(height)) { + height = this.defaultVisibleHeight; + } + + height += parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-top')); + + if ( + !! collapsible.dataset.toggleElement + && (! collapsible.nextElementSibling + || ! collapsible.nextElementSibling.matches(collapsible.dataset.toggleElement)) + ) { + let toggle = collapsible.querySelector(collapsible.dataset.toggleElement); + height += toggle.offsetHeight; // TODO: Very expensive at times. (50ms+) Check why! + height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-top')); + height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-bottom')); } } - } else { - height = Number(collapsible.dataset.visibleHeight); - if (isNaN(height)) { - height = this.defaultVisibleHeight; + + return height; + } + + /** + * Collapse the given collapsible + * + * @param collapsible The given collapsible container element + * @param toHeight {int} The height in pixels to collapse to + */ + collapse(collapsible, toHeight) { + if (this.isDetails(collapsible)) { + collapsible.open = false; + } else { + collapsible.style.cssText = 'display: block; height: ' + toHeight + 'px; padding-bottom: 0'; } - height += parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-top')); + collapsible.classList.add('collapsed'); + } - if ( - !! collapsible.dataset.toggleElement - && (! collapsible.nextElementSibling - || ! collapsible.nextElementSibling.matches(collapsible.dataset.toggleElement)) - ) { - let toggle = collapsible.querySelector(collapsible.dataset.toggleElement); - height += toggle.offsetHeight; // TODO: Very expensive at times. (50ms+) Check why! - height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-top')); - height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-bottom')); + /** + * Expand the given collapsible + * + * @param collapsible The given collapsible container element + */ + expand(collapsible) { + collapsible.classList.remove('collapsed'); + + if (this.isDetails(collapsible)) { + collapsible.open = true; + } else { + collapsible.style.cssText = ''; } } - return height; - }; - - /** - * Collapse the given collapsible - * - * @param collapsible The given collapsible container element - * @param toHeight {int} The height in pixels to collapse to - */ - Collapsible.prototype.collapse = function(collapsible, toHeight) { - if (this.isDetails(collapsible)) { - collapsible.open = false; - } else { - collapsible.style.cssText = 'display: block; height: ' + toHeight + 'px; padding-bottom: 0'; + /** + * Get whether the given collapsible is a
element + * + * @param collapsible + * + * @return {Boolean} + */ + isDetails(collapsible) { + return collapsible.tagName === 'DETAILS'; } - - collapsible.classList.add('collapsed'); - }; - - /** - * Expand the given collapsible - * - * @param collapsible The given collapsible container element - */ - Collapsible.prototype.expand = function(collapsible) { - collapsible.classList.remove('collapsed'); - - if (this.isDetails(collapsible)) { - collapsible.open = true; - } else { - collapsible.style.cssText = ''; - } - }; - - /** - * Get whether the given collapsible is a
element - * - * @param collapsible - * - * @return {Boolean} - */ - Collapsible.prototype.isDetails = function (collapsible) { - return collapsible.tagName === 'DETAILS'; - }; + } Icinga.Behaviors.Collapsible = Collapsible;