%PDF- %PDF-
Direktori : /backups/router/usr/local/opnsense/www/js/widgets/ |
Current File : //backups/router/usr/local/opnsense/www/js/widgets/BaseTableWidget.js |
/* * Copyright (C) 2024 Deciso B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ class BaseTableWidget extends BaseWidget { constructor(config) { super(config); this.tables = {}; this.curSize = null; } _calculateColumnWidth() { for (const [id, tableObj] of Object.entries(this.tables)) { if (tableObj.options.headerPosition === 'left') { return `calc(100% / 2)`; } if (tableObj.options.headerPosition === 'none') { let first = $(`#${tableObj.table.attr('id')} > .flextable-row`).first(); let count = first.children().filter(function() { return $(this).css('display') !== 'none'; }).length; return `calc(100% / ${count})`; } } return ''; } createTable(id, options) { /** * Options: * * headerPosition: top, left or none. * top: headers are on top of the table. Headers are defined in the options. Data layout: * [ * ['x', 'y', 'z'], * ['x', 'y', 'z'] * ] * * left: headers are on the left of the table (key-value). Data layout: * [ * ['x', 'x1'], * ['y', 'y1'], * ['z', ['z1', 'z2']] <-- supports nested columns * ] * * none: no headers, same data layout as 'top', without headers set as an option. * * rotation: limit table entries to a certain amount and rotate them. Only applicable for headerPosition: top. * headers: list of headers to display. Only applicable for headerPosition: top. * sortIndex: index of the column to sort on. Only applicable for headerPosition: top. * sortOrder: 'asc' or 'desc'. Only applicable for headerPosition: top. * headerBreakpoint: width in pixels beforing switching to a column layout. Only applicable for headerPosition: left. * */ if (this.options === null) { console.error('No table options set'); return null; } let mergedOpts = { headerPosition: 'top', rotation: false, sortIndex: null, sortOrder: 'desc', headerBreakpoint: 450, ...options } let $table = null; let $headerContainer = null; this.headerBreakpoint = mergedOpts.headerBreakpoint; this.sizeStates = { 0: { '.flextable-row': {'padding': ''}, '.flextable-header .flex-cell': {'border-bottom': 'solid 1px'}, '.flex-cell': {'width': '100%'}, '.column': {'width': '100%'}, '.flex-subcell': {'width': '100%'}, }, [this.headerBreakpoint]: { '.flextable-row': {'padding': '0.5em 0.5em'}, '.flextable-header .flex-cell': {'border-bottom': ''}, '.flex-cell': {'width': this._calculateColumnWidth.bind(this)}, '.column .flex-cell': {'width': '100%'}, '.column': {'width': ''}, '.flex-subcell': {'width': ''}, } } this.widths = Object.keys(this.sizeStates).sort(); if (mergedOpts.headerPosition === 'top') { /* CSS grid implementation */ $table = $(`<div class="grid-table" id="${id}" role="table"></div>`); $headerContainer = $(`<div id="header_${id}" class="grid-header-container"></div>`); for (const h of mergedOpts.headers) { $headerContainer.append($(` <div class="grid-item grid-header">${h}</div> `)); } $table.append($headerContainer); } else { /* flextable implementation */ $table = $(`<div class="flextable-container" id="${id}" role="table"></div>`); } this.tables[id] = { 'table': $table, 'options': mergedOpts, 'headerContainer': $headerContainer, 'data': [], }; return $table; } updateTable(id, data = [], rowIdentifier = null) { /** * id: table id * data: array of rows * rowIdentifier: if set, upsert row with this identifier */ let $table = $(`#${id}`); let options = this.tables[id].options; if (!options.rotation && rowIdentifier == null) { $table.children('.grid-row').remove(); $table.children('.flextable-row').remove(); this.tables[id].data = data; } if (rowIdentifier !== null) { rowIdentifier = this.sanitizeSelector(rowIdentifier); } data.forEach(row => { let $gridRow = options.headerPosition === 'top' ? $(`<div class="grid-row"></div>`) : $(`<div class="flextable-row"></div>`); let newElement = true; if (rowIdentifier !== null) { let $existingRow = $(`#${this.id}_${rowIdentifier}`); if ($existingRow.length === 0) { $gridRow.attr('id', `${this.id}_${rowIdentifier}`); } else { $gridRow = $existingRow.empty(); newElement = false; } } this.populateRow($gridRow, row, options, id); if (newElement) { if (options.headerPosition === 'top') { $(`#header_${id}`).after($gridRow); } else { $table.append($gridRow); } } else { $(`#${this.id}_${rowIdentifier}`).replaceWith($gridRow); } if (options.headerPosition === 'top' && options.sortIndex !== null) { this.sortTable($table, options); } if (options.rotation) { $gridRow.animate({ from: 0, to: 255, opacity: 1, }, { duration: 500, easing: 'linear', step: function() { $gridRow.css('background-color', 'initial'); } }); this.rotate(id, row); } else { $gridRow.css({ opacity: 1, 'background-color': 'initial' }); } }); for (const [selector, styles] of Object.entries(this.sizeStates[this.curSize ?? 0])) { $table.find(selector).css(styles); } } populateRow($gridRow, row, options, id) { switch (options.headerPosition) { case "none": row.forEach(item => { $gridRow.append(`<div class="flex-cell" role="cell">${item}</div>`); }); break; case "top": row.forEach((item, i) => { $gridRow.append(` <div class="grid-item ${options.sortIndex !== null && options.sortIndex == i ? 'sort' : ''}"> ${item} </div> `); }); break; case "left": if (row.length !== 2) return; const [h, c] = row; if (Array.isArray(c)) { $gridRow.append(`<div class="flex-cell rowspan first"><b>${h}</b></div>`); let $column = $('<div class="column"></div>'); c.forEach(item => { $column.append(` <div class="flex-cell"> <div class="flex-subcell">${item}</div> </div> `); }); $gridRow.append($column); } else { $gridRow.append(` <div class="flex-cell first"><b>${h}</b></div> <div class="flex-cell">${c}</div> `); } break; } } rotate(id, newElement) { let opts = this.tables[id].options; let data = this.tables[id].data; data.unshift(newElement); if (data.length > opts.rotation) { data.splice(opts.rotation); } const divs = document.querySelectorAll(`#${id} .grid-row`); if (divs.length > opts.rotation) { for (let i = opts.rotation; i < divs.length; i++) { $(divs[i]).remove(); } } } sortTable($table, options) { let items = $table.children('.grid-row').toArray().sort((a, b) => { let vA = parseInt($(a).children('.sort').first().text()); let vB = parseInt($(b).children('.sort').first().text()); return options.sortOrder === 'asc' ? (vA - vB) : (vB - vA); }); $table.append(items); } onWidgetResize(elem, width, height) { if (this.widths == null || this.sizeStates == null) { return false; } let lowIndex = 0; for (let i = 0; i < this.widths.length; i++) { if (this.widths[i] <= width) { lowIndex = i; } else { break; } } const lowIndexWidth = this.widths[lowIndex]; if (lowIndexWidth !== this.curSize) { for (const [selector, styles] of Object.entries(this.sizeStates[lowIndexWidth])) { $(elem).find(selector).css(styles); } this.curSize = lowIndexWidth; return true; } return false; } }