SSL TableWidget.js
Interaktion und PortierbarkeitJAVA
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict";
// Different types of events emitted by the Various components of the // TableWidget. const EVENTS = {
CELL_EDIT: "cell-edit",
COLUMN_SORTED: "column-sorted",
COLUMN_TOGGLED: "column-toggled",
FIELDS_EDITABLE: "fields-editable",
HEADER_CONTEXT_MENU: "header-context-menu",
ROW_EDIT: "row-edit",
ROW_CONTEXT_MENU: "row-context-menu",
ROW_REMOVED: "row-removed",
ROW_SELECTED: "row-selected",
ROW_UPDATED: "row-updated",
TABLE_CLEARED: "table-cleared",
TABLE_FILTERED: "table-filtered",
SCROLL_END: "scroll-end",
};
Object.defineProperty(this, "EVENTS", {
value: EVENTS,
enumerable: true,
writable: false,
});
/** * A table widget with various features like resizble/toggleable columns, * sorting, keyboard navigation etc. * * @param {Node} node * The container element for the table widget. * @param {object} options * - initialColumns: map of key vs display name for initial columns of * the table. See @setupColumns for more info. * - uniqueId: the column which will be the unique identifier of each * entry in the table. Default: name. * - wrapTextInElements: Don't ever use 'value' attribute on labels. * Default: false. * - emptyText: Localization ID for the text to display when there are * no entries in the table to display. * - highlightUpdated: true to highlight the changed/added row. * - removableColumns: Whether columns are removeable. If set to false, * the context menu in the headers will not appear. * - firstColumn: key of the first column that should appear. * - cellContextMenuId: ID of a <menupopup> element to be set as a * context menu of every cell.
*/ function TableWidget(node, options = {}) {
EventEmitter.decorate(this);
// Setup the column headers context menu to allow users to hide columns at // will. if (this.removableColumns) { this.onPopupCommand = this.onPopupCommand.bind(this); this.setupHeadersContextMenu();
}
if (initialColumns) { this.setColumns(initialColumns, uniqueId);
}
/** * Return true if the table body has a scrollbar.
*/
get hasScrollbar() { returnthis.tbody.scrollHeight > this.tbody.clientHeight;
},
/** * Getter for the headers context menu popup id.
*/
get headersContextMenu() { if (this.menupopup) { returnthis.menupopup.id;
} returnnull;
},
/** * Select the row corresponding to the json object `id`
*/
set selectedRow(id) { for (const column of this.columns.values()) { if (id || id === "") {
column.selectRow(id[this.uniqueId] || id);
} else {
column.selectedRow = null;
column.selectRow(null);
}
}
},
/** * Is a row currently selected? * * @return {Boolean} * true or false.
*/
get hasSelectedRow() { return ( this.columns.get(this.uniqueId) && this.columns.get(this.uniqueId).selectedRow
);
},
/** * Returns the json object corresponding to the selected row.
*/
get selectedRow() { returnthis.items.get(this.columns.get(this.uniqueId).selectedRow);
},
/** * Selects the row at index `index`.
*/
set selectedIndex(index) { for (const column of this.columns.values()) {
column.selectRowAt(index);
}
},
/** * Returns the index of the selected row.
*/
get selectedIndex() { returnthis.columns.get(this.uniqueId).selectedIndex;
},
/** * Returns the index of the selected row disregarding hidden rows.
*/
get visibleSelectedIndex() { const column = this.firstVisibleColumn; const cells = column.visibleCellNodes;
for (let i = 0; i < cells.length; i++) { if (cells[i].classList.contains("theme-selected")) { return i;
}
}
return -1;
},
/** * Returns the first visible column.
*/
get firstVisibleColumn() { for (const column of this.columns.values()) { if (column._private) { continue;
}
if (column.column.clientHeight > 0) { return column;
}
}
returnnull;
},
/** * returns all editable columns.
*/
get editableColumns() { const filter = columns => {
columns = [...columns].filter(col => { if (col.clientWidth === 0) { returnfalse;
}
// A rows position in the table can change as the result of an edit. In // order to ensure that the correct row is highlighted after an edit we // save the uniqueId in editBookmark. this.editBookmark =
colName === uniqueId ? change.newValue : items[uniqueId]; this.emit(EVENTS.CELL_EDIT, change);
},
/** * Called by the inplace editor when Tab / Shift-Tab is pressed in edit-mode. * Because tables are live any row, column, cell or table can be added, * deleted or moved by deleting and adding e.g. a row again. * * This presents various challenges when navigating via the keyboard so please * keep this in mind whenever editing this method. * * @param {Event} event * Keydown event
*/
onEditorTab(event) { const textbox = event.target; const editor = this._editableFieldsEngine;
if (textbox.id !== editor.INPUT_ID) { return;
}
const column = textbox.parentNode;
// Changing any value can change the position of the row depending on which // column it is currently sorted on. In addition to this, the table cell may // have been edited and had to be recreated when the user has pressed tab or // shift+tab. Both of these situations require us to recover our target, // select the appropriate row and move the textbox on to the next cell. if (editor.changePending) { // We need to apply a change, which can mean that the position of cells // within the table can change. Because of this we need to wait for // EVENTS.ROW_EDIT and then move the textbox. this.once(EVENTS.ROW_EDIT, uniqueId => {
let columnObj; const cols = this.editableColumns;
let rowIndex = this.visibleSelectedIndex; const colIndex = cols.indexOf(column);
let newIndex;
// If the row has been deleted we should bail out. if (!uniqueId) { return;
}
// Find the column we need to move to. if (event.shiftKey) { // Navigate backwards on shift tab. if (colIndex === 0) { if (rowIndex === 0) { return;
}
newIndex = cols.length - 1;
} else {
newIndex = colIndex - 1;
}
} elseif (colIndex === cols.length - 1) { const id = cols[0].id;
columnObj = this.columns.get(id); const maxRowIndex = columnObj.visibleCellNodes.length - 1; if (rowIndex === maxRowIndex) { return;
}
newIndex = 0;
} else {
newIndex = colIndex + 1;
}
// Select the correct row even if it has moved due to sorting. const dataId = editor.currentTarget.getAttribute("data-id"); if (this.items.get(dataId)) { this.emit(EVENTS.ROW_SELECTED, dataId);
} else { this.emit(EVENTS.ROW_SELECTED, uniqueId);
}
// EVENTS.ROW_SELECTED may have changed the selected row so let's save // the result in rowIndex.
rowIndex = this.visibleSelectedIndex;
// Remove flash-out class... it won't have been auto-removed because the // cell was hidden for editing.
cell.classList.remove("flash-out");
});
}
// Begin cell edit. We always do this so that we can begin editing even in // the case that the previous edit will cause the row to move. const cell = this.getEditedCellOnTab(event, column);
editor.edit(cell);
/** * Get the cell that will be edited next on tab / shift tab and highlight the * appropriate row. Edits etc. are not taken into account. * * This is used to tab from one field to another without editing and makes the * editor much more responsive. * * @param {Event} event * Keydown event
*/
getEditedCellOnTab(event, column) {
let cell = null; const cols = this.editableColumns; const rowIndex = this.visibleSelectedIndex; const colIndex = cols.indexOf(column); const maxCol = cols.length - 1; const maxRow = this.columns.get(column.id).visibleCellNodes.length - 1;
if (event.shiftKey) { // Navigate backwards on shift tab. if (colIndex === 0) { if (rowIndex === 0) { this._editableFieldsEngine.completeEdit(); returnnull;
}
const rowId = cell.getAttribute("data-id"); this.emit(EVENTS.ROW_SELECTED, rowId);
} else {
column = cols[colIndex - 1]; const cells = this.columns.get(column.id).visibleCellNodes;
cell = cells[rowIndex];
}
} elseif (colIndex === maxCol) { // If in the rightmost column on the last row stop editing. if (rowIndex === maxRow) { this._editableFieldsEngine.completeEdit(); returnnull;
}
// If in the rightmost column of a row then move to the first column of // the next row.
column = cols[0]; const cells = this.columns.get(column.id).visibleCellNodes;
cell = cells[rowIndex + 1];
/** * Reset the editable fields engine if the currently edited row is removed. * * @param {String} event * The event name "event-removed." * @param {Object} row * The values from the removed row.
*/
onRowRemoved(row) { if (!this._editableFieldsEngine || !this._editableFieldsEngine.isEditing) { return;
}
// The target is lost so we need to hide the remove the textbox from the DOM // and reset the target nodes. this.onEditorTargetLost();
},
/** * Cancel an edit because the edit target has been lost.
*/
onEditorTargetLost() { const editor = this._editableFieldsEngine;
if (!editor || !editor.isEditing) { return;
}
editor.cancelEdit();
},
/** * Keydown event handler for the table. Used for keyboard navigation amongst * rows.
*/
onKeydown(event) { // If we are in edit mode bail out. if (this._editableFieldsEngine && this._editableFieldsEngine.isEditing) { return;
}
// We need to get the first *visible* selected cell. Some columns are hidden // e.g. because they contain a unique compound key for cookies that is never // displayed in the UI. To do this we get all selected cells and filter out // any that are hidden. const selectedCells = [
...this.tbody.querySelectorAll(".theme-selected"),
].filter(cell => cell.clientWidth > 0); // Select the first visible selected cell. const selectedCell = selectedCells[0]; if (!selectedCell) { return;
}
let colName;
let column;
let visibleCells;
let index;
let cell;
switch (event.keyCode) { case KeyCodes.DOM_VK_UP:
event.preventDefault();
/** * Close any editors if the area "outside the table" is clicked. In reality, * the table covers the whole area but there are labels filling the top few * rows. This method clears any inline editors if an area outside a textbox or * label is clicked.
*/
onMousedown({ target }) { const localName = target.localName;
if (localName === "input" || !this._editableFieldsEngine) { return;
}
// Force any editor fields to hide due to XUL focus quirks. this._editableFieldsEngine.blur();
},
/** * Make table fields editable. * * @param {String|Array} editableColumns * An array or comma separated list of editable column names.
*/
makeFieldsEditable(editableColumns) { const selectors = [];
if (typeof editableColumns === "string") {
editableColumns = [editableColumns];
}
for (const id of editableColumns) {
selectors.push("#" + id + " .table-widget-cell");
}
for (const [name, column] of this.columns) { if (!editableColumns.includes(name)) {
column.column.setAttribute("readonly", "");
}
}
if (this.menupopup) { this.menupopup.removeEventListener("command", this.onPopupCommand); this.menupopup.remove();
}
},
/** * Sets the localization ID of the description to be shown when the table is empty. * * @param {String} l10nID * The ID of the localization string. * @param {String} learnMoreURL * A URL referring to a website with further information related to * the data shown in the table widget.
*/
setPlaceholder(l10nID, learnMoreURL) { if (learnMoreURL) {
let placeholderLink = this.placeholder.firstElementChild; if (!placeholderLink) {
placeholderLink = this.document.createElement("a");
placeholderLink.setAttribute("target", "_blank");
placeholderLink.setAttribute("data-l10n-name", "learn-more-link"); this.placeholder.appendChild(placeholderLink);
}
placeholderLink.setAttribute("href", learnMoreURL);
} else { // Remove link element if no learn more URL is given this.placeholder.firstElementChild?.remove();
}
/** * Prepares the context menu for the headers of the table columns. This * context menu allows users to toggle various columns, only with an exception * of the unique columns and when only two columns are visible in the table.
*/
setupHeadersContextMenu() {
let popupset = this.document.getElementsByTagName("popupset")[0]; if (!popupset) {
popupset = this.document.createXULElement("popupset"); this.document.documentElement.appendChild(popupset);
}
/** * Populates the header context menu with the names of the columns along with * displaying which columns are hidden or visible. * * @param {Array} privateColumns=[] * An array of column names that should never appear in the table. This * allows us to e.g. have an invisible compound primary key for a * table's rows.
*/
populateMenuPopup(privateColumns = []) { if (!this.menupopup) { return;
}
while (this.menupopup.firstChild) { this.menupopup.firstChild.remove();
}
for (const column of this.columns.values()) { if (privateColumns.includes(column.id)) { continue;
}
/** * Event handler for the `command` event on the column headers context menu
*/
onPopupCommand(event) { const item = event.originalTarget;
let checked = !!item.getAttribute("checked"); const id = item.getAttribute("data-id"); this.emit(EVENTS.HEADER_CONTEXT_MENU, id, checked);
checked = this.menupopup.querySelectorAll("menuitem[checked]"); const disabled = this.menupopup.querySelectorAll("menuitem[disabled]"); if (checked.length == 2) {
checked[checked.length - 1].setAttribute("disabled", "true");
} elseif (disabled.length > 1) {
disabled[disabled.length - 1].removeAttribute("disabled");
}
},
/** * Creates the columns in the table. Without calling this method, data cannot * be inserted into the table unless `initialColumns` was supplied. * * @param {Object} columns * A key value pair representing the columns of the table. Where the * key represents the id of the column and the value is the displayed * label in the header of the column. * @param {String} sortOn * The id of the column on which the table will be initially sorted on. * @param {Array} hiddenColumns * Ids of all the columns that are hidden by default. * @param {Array} privateColumns=[] * An array of column names that should never appear in the table. This * allows us to e.g. have an invisible compound primary key for a * table's rows.
*/
setColumns(
columns,
sortOn = this.sortedOn,
hiddenColumns = [],
privateColumns = []
) { for (const column of this.columns.values()) {
column.destroy();
}
this.columns.clear();
if (!(sortOn in columns)) {
sortOn = null;
}
if (!(this.firstColumn in columns)) { this.firstColumn = null;
}
if (this.firstColumn) { this.columns.set( this.firstColumn, new Column(this, this.firstColumn, columns[this.firstColumn])
);
}
for (const id in columns) { if (!sortOn) {
sortOn = id;
}
if (this.firstColumn && id == this.firstColumn) { continue;
}
this.columns.set(id, new Column(this, id, columns[id])); if (hiddenColumns.includes(id) || privateColumns.includes(id)) { // Hide the column. this.columns.get(id).toggleColumn();
/** * Returns true if the passed string or the row json object corresponds to the * selected item in the table.
*/
isSelected(item) { if (typeof item == "object") {
item = item[this.uniqueId];
}
/** * Selects the row corresponding to the `id` json.
*/
selectRow(id) { this.selectedRow = id;
},
/** * Selects the next row. Cycles over to the first row if last row is selected
*/
selectNextRow() { for (const column of this.columns.values()) {
column.selectNextRow();
}
},
/** * Selects the previous row. Cycles over to the last row if first row is * selected.
*/
selectPreviousRow() { for (const column of this.columns.values()) {
column.selectPreviousRow();
}
},
/** * Adds a row into the table. * * @param {object} item * The object from which the key-value pairs will be taken and added * into the row. This object can have any arbitarary key value pairs, * but only those will be used whose keys match to the ids of the * columns. * @param {boolean} suppressFlash * true to not flash the row while inserting the row.
*/
push(item, suppressFlash) { if (!this.sortedOn || !this.columns) {
console.error("Can't insert item without defining columns first"); return;
}
if (this.items.has(item[this.uniqueId])) { this.update(item); return;
}
if (this.editBookmark && !this.items.has(this.editBookmark)) { // Key has been updated... update bookmark. this.editBookmark = item[this.uniqueId];
}
const index = this.columns.get(this.sortedOn).push(item); for (const [key, column] of this.columns) { if (key != this.sortedOn) {
column.insertAt(item, index);
}
column.updateZebra();
} this.items.set(item[this.uniqueId], item); this.tbody.removeAttribute("empty");
if (!suppressFlash) { this.emit(EVENTS.ROW_UPDATED, item[this.uniqueId]);
}
/** * Removes the row associated with the `item` object.
*/
remove(item) { if (typeof item != "object") {
item = this.items.get(item);
} if (!item) { return;
} const removed = this.items.delete(item[this.uniqueId]);
if (!removed) { return;
} for (const column of this.columns.values()) {
column.remove(item);
column.updateZebra();
} if (this.items.size === 0) { this.selectedRow = null; this.tbody.setAttribute("empty", "empty");
}
this.emit(EVENTS.ROW_REMOVED, item);
},
/** * Updates the items in the row corresponding to the `item` object previously * used to insert the row using `push` method. The linking is done via the * `uniqueId` key's value.
*/
update(item) { const oldItem = this.items.get(item[this.uniqueId]); if (!oldItem) { return;
} this.items.set(item[this.uniqueId], item);
let changed = false; for (const column of this.columns.values()) { if (item[column.id] != oldItem[column.id]) {
column.update(item);
changed = true;
}
} if (changed) { this.emit(EVENTS.ROW_UPDATED, item[this.uniqueId]); this.emit(EVENTS.ROW_EDIT, item[this.uniqueId]);
}
},
/** * Removes all of the rows from the table.
*/
clear() { this.items.clear(); for (const column of this.columns.values()) {
column.clear();
} this.tbody.setAttribute("empty", "empty"); this.setPlaceholder(this.emptyText);
this.selectedRow = null;
this.emit(EVENTS.TABLE_CLEARED, this);
},
/** * Sorts the table by a given column. * * @param {string} column * The id of the column on which the table should be sorted.
*/
sortBy(column) { this.emit(EVENTS.COLUMN_SORTED, column); this.sortedOn = column;
if (!this.items.size) { return;
}
// First sort the column to "sort by" explicitly. const sortedItems = this.columns.get(column).sort([...this.items.values()]);
// Then, sort all the other columns (id !== column) only based on the // sortedItems provided by the first sort. // Each column keeps track of the fact that it is the "sort by" column or // not, so this will not shuffle the items and will just make sure each // column displays the correct value. for (const [id, col] of this.columns) { if (id !== column) {
col.sort(sortedItems);
}
}
},
/** * Filters the table based on a specific value * * @param {String} value: The filter value * @param {Array} ignoreProps: Props to ignore while filtering
*/
filterItems(value, ignoreProps = []) { if (this.filteredValue == value) { return;
} if (this._editableFieldsEngine) { this._editableFieldsEngine.completeEdit();
}
this.filteredValue = value; if (!value) { this.emit(EVENTS.TABLE_FILTERED, []); return;
} // Shouldn't be case-sensitive
value = value.toLowerCase();
const itemsToHide = [...this.items.keys()]; // Loop through all items and hide unmatched items for (const [id, val] of this.items) { for (const prop in val) { const column = this.columns.get(prop); if (ignoreProps.includes(prop) || column.hidden) { continue;
}
/** * Calls the afterScroll function when the user has stopped scrolling
*/
onScroll() {
clearNamedTimeout("table-scroll");
setNamedTimeout("table-scroll", AFTER_SCROLL_DELAY, this.afterScroll);
},
/** * Emits the "scroll-end" event when the whole table is scrolled
*/
afterScroll() { const maxScrollTop = this.tbody.scrollHeight - this.tbody.clientHeight; // Emit scroll-end event when 9/10 of the table is scrolled if (this.tbody.scrollTop >= 0.9 * maxScrollTop) { this.emit("scroll-end");
}
},
};
TableWidget.EVENTS = EVENTS;
module.exports.TableWidget = TableWidget;
/** * A single column object in the table. * * @param {TableWidget} table * The table object to which the column belongs. * @param {string} id * Id of the column. * @param {String} header * The displayed string on the column's header.
*/ function Column(table, id, header) { // By default cells are visible in the UI. this._private = false;
Column.prototype = { // items is a cell-id to cell-index map. It is basically a reverse map of the // this.cells object and is used to quickly reverse lookup a cell by its id // instead of looping through the cells array. This reverse map is not kept // upto date in sync with the cells array as updating it is in itself a loop // through all the cells of the columns. Thus update it on demand when it goes // out of sync with this.cells.
items: null,
// _itemsDirty is a flag which becomes true when this.items goes out of sync // with this.cells
_itemsDirty: null,
selectedRow: null,
cells: null,
/** * Gets whether the table is sorted on this column or not. * 0 - not sorted. * 1 - ascending order * 2 - descending order
*/
get sorted() { returnthis._sortState || 0;
},
/** * Returns a boolean indicating whether the column is hidden.
*/
get hidden() { returnthis.column.hidden;
},
/** * Get the private state of the column (visibility in the UI).
*/
get private() { returnthis._private;
},
/** * Set the private state of the column (visibility in the UI). * * @param {Boolean} state * Private (true or false)
*/
set private(state) { this._private = state;
},
/** * Sets the sorted value
*/
set sorted(value) { if (!value) { this.header.removeAttribute("sorted");
} else { this.header.setAttribute( "sorted",
value == 1 ? "ascending" : "descending"
);
} this._sortState = value;
},
/** * Gets the selected row in the column.
*/
get selectedIndex() { if (!this.selectedRow) { return -1;
} returnthis.items[this.selectedRow];
},
get cellNodes() { return [...this.column.querySelectorAll(".table-widget-cell")];
},
get visibleCellNodes() { const editor = this.table._editableFieldsEngine; const nodes = this.cellNodes.filter(node => { // If the cell is currently being edited we should class it as visible. if (editor && editor.currentTarget === node) { returntrue;
} return node.clientWidth !== 0;
});
return nodes;
},
/** * Called when the column is sorted by. * * @param {string} column * The id of the column being sorted by.
*/
onColumnSorted(column) { if (column != this.id) { this.sorted = 0; return;
} elseif (this.sorted == 0 || this.sorted == 2) { this.sorted = 1;
} else { this.sorted = 2;
} this.updateZebra();
},
onTableFiltered(itemsToHide) { this._updateItems(); if (!this.cells) { return;
} for (const cell of this.cells) {
cell.hidden = false;
} for (const id of itemsToHide) { this.cells[this.items[id]].hidden = true;
} this.updateZebra();
},
/** * Called when a row is updated e.g. a cell is changed. This means that * for a new row this method will be called once for each column. If a single * cell is changed this method will be called just once. * * @param {string} event * The event name of the event. i.e. EVENTS.ROW_UPDATED * @param {string} id * The unique id of the object associated with the row.
*/
onRowUpdated(id) { this._updateItems();
if (this.highlightUpdated && this.items[id] != null) { if (this.table.scrollIntoViewOnUpdate) { const cell = this.cells[this.items[id]];
// When a new row is created this method is called once for each column // as each cell is updated. We can only scroll to cells if they are // visible. We check for visibility and once we find the first visible // cell in a row we scroll it into view and reset the // scrollIntoViewOnUpdate flag. if (cell.label.clientHeight > 0) {
cell.scrollIntoView();
this.table.scrollIntoViewOnUpdate = null;
}
}
if (this.table.editBookmark) { // A rows position in the table can change as the result of an edit. In // order to ensure that the correct row is highlighted after an edit we // save the uniqueId in editBookmark. Here we send the signal that the // row has been edited and that the row needs to be selected again. this.table.emit(EVENTS.ROW_SELECTED, this.table.editBookmark); this.table.editBookmark = null;
}
/** * Selects the row at the `index` index
*/
selectRowAt(index) { if (this.selectedRow != null) { this.cells[this.items[this.selectedRow]].classList.remove( "theme-selected"
);
}
/** * Selects the row with the object having the `uniqueId` value as `id`
*/
selectRow(id) { this._updateItems(); this.selectRowAt(this.items[id]);
},
/** * Selects the next row. Cycles to first if last row is selected.
*/
selectNextRow() { this._updateItems();
let index = this.items[this.selectedRow] + 1; if (index == this.cells.length) {
index = 0;
} this.selectRowAt(index);
},
/** * Selects the previous row. Cycles to last if first row is selected.
*/
selectPreviousRow() { this._updateItems();
let index = this.items[this.selectedRow] - 1; if (index == -1) {
index = this.cells.length - 1;
} this.selectRowAt(index);
},
/** * Pushes the `item` object into the column. If this column is sorted on, * then inserts the object at the right position based on the column's id * key's value. * * @returns {number} * The index of the currently pushed item.
*/
push(item) { const value = item[this.id];
if (this.sorted) {
let index; if (this.sorted == 1) {
index = this.cells.findIndex(element => { return (
naturalSortCaseInsensitive(
value,
element.value,
standardSessionString
) === -1
);
});
} else {
index = this.cells.findIndex(element => { return (
naturalSortCaseInsensitive(
value,
element.value,
standardSessionString
) === 1
);
});
}
index = index >= 0 ? index : this.cells.length; if (index < this.cells.length) { this._itemsDirty = true;
} this.items[item[this.uniqueId]] = index; this.cells.splice(index, 0, new Cell(this, item, this.cells[index])); return index;
}
/** * Inserts the `item` object at the given `index` index in the table.
*/
insertAt(item, index) { if (index < this.cells.length) { this._itemsDirty = true;
} this.items[item[this.uniqueId]] = index; this.cells.splice(index, 0, new Cell(this, item, this.cells[index])); this.updateZebra();
},
/** * Event handler for the command event coming from the header context menu. * Toggles the column if it was requested by the user. * When called explicitly without parameters, it toggles the corresponding * column. * * @param {string} event * The name of the event. i.e. EVENTS.HEADER_CONTEXT_MENU * @param {string} id * Id of the column to be toggled * @param {string} checked * true if the column is visible
*/
toggleColumn(id, checked) { if (!arguments.length) { // Act like a toggling method when called with no params
id = this.id;
checked = this.column.hidden;
} if (id != this.id) { return;
} if (checked) { this.column.hidden = false; this.tbody.insertBefore(this.splitter, this.column.nextSibling);
} else { this.column.hidden = true; this.splitter.remove();
}
},
/** * Removes the corresponding item from the column and hide the last visible * splitter with CSS, so we do not add splitter elements for hidden columns.
*/
remove(item) { this._updateItems(); const index = this.items[item[this.uniqueId]]; if (index == null) { return;
}
/** * Updates the corresponding item from the column.
*/
update(item) { this._updateItems();
const index = this.items[item[this.uniqueId]]; if (index == null) { return;
}
this.cells[index].value = item[this.id];
},
/** * Updates the `this.items` cell-id vs cell-index map to be in sync with * `this.cells`.
*/
_updateItems() { if (!this._itemsDirty) { return;
} for (let i = 0; i < this.cells.length; i++) { this.items[this.cells[i].id] = i;
} this._itemsDirty = false;
},
/** * Clears the current column
*/
clear() { this.cells = []; this.items = {}; this._itemsDirty = false; while (this.header.nextSibling) { this.header.nextSibling.remove();
}
},
/** * Sorts the given items and returns the sorted list if the table was sorted * by this column.
*/
sort(items) { // Only sort the array if we are sorting based on this column if (this.sorted == 1) {
items.sort((a, b) => { const val1 = Node.isInstance(a[this.id])
? a[this.id].textContent
: a[this.id]; const val2 = Node.isInstance(b[this.id])
? b[this.id].textContent
: b[this.id]; return naturalSortCaseInsensitive(val1, val2, standardSessionString);
});
} elseif (this.sorted > 1) {
items.sort((a, b) => { const val1 = Node.isInstance(a[this.id])
? a[this.id].textContent
: a[this.id]; const val2 = Node.isInstance(b[this.id])
? b[this.id].textContent
: b[this.id]; return naturalSortCaseInsensitive(val2, val1, standardSessionString);
});
}
if (this.selectedRow) { this.cells[this.items[this.selectedRow]].classList.remove( "theme-selected"
);
} this.items = {}; // Otherwise, just use the sorted array passed to update the cells value. for (const [i, item] of items.entries()) { // See Bug 1706679 (Intermittent) // Sometimes we would reach the situation in which we were trying to sort // and item that was no longer available in the TableWidget. // We should find exactly what is triggering it. if (!this.cells[i]) { continue;
} this.items[item[this.uniqueId]] = i; this.cells[i].value = item[this.id]; this.cells[i].id = item[this.uniqueId];
} if (this.selectedRow) { this.cells[this.items[this.selectedRow]].classList.add("theme-selected");
} this._itemsDirty = false; this.updateZebra(); return items;
},
updateZebra() { this._updateItems();
let i = 0; for (const cell of this.cells) { if (!cell.hidden) {
i++;
}
const even = !(i % 2);
cell.classList.toggle("even", even);
}
},
/** * Click event handler for the column. Used to detect click on header for * for sorting.
*/
onClick(event) { const target = event.originalTarget;
/** * A single cell in a column * * @param {Column} column * The column object to which the cell belongs. * @param {object} item * The object representing the row. It contains a key value pair * representing the column id and its associated value. The value * can be a DOMNode that is appended or a string value. * @param {Cell} nextCell * The cell object which is next to this cell. null if this cell is last * cell of the column
*/ function Cell(column, item, nextCell) { const document = column.document;
if (nextCell) {
column.column.insertBefore(this.label, nextCell.label);
} else {
column.column.appendChild(this.label);
}
if (column.table.cellContextMenuId) { this.label.setAttribute("context", column.table.cellContextMenuId); this.label.addEventListener("contextmenu", () => { // Make the ID of the clicked cell available as a property on the table. // It's then available for the popupshowing or command handler.
column.table.contextMenuRowId = this.id;
});
}
set value(value) { this._value = value; if (value == null) { this.label.setAttribute("value", ""); return;
}
if (this.wrapTextInElements && !Node.isInstance(value)) { const span = this.label.ownerDocument.createElementNS(HTML_NS, "span");
span.textContent = value;
value = span;
}
if (Node.isInstance(value)) { this.label.removeAttribute("value");
while (this.label.firstChild) { this.label.firstChild.remove();
}
this.label.appendChild(value);
} else { this.label.setAttribute("value", value + "");
}
},
get value() { returnthis._value;
},
get classList() { returnthis.label.classList;
},
/** * Flashes the cell for a brief time. This when done for with cells in all * columns, makes it look like the row is being highlighted/flashed.
*/
flash() { if (!this.label.parentNode) { return;
} this.label.classList.remove("flash-out"); // Cause a reflow so that the animation retriggers on adding back the class
let a = this.label.parentNode.offsetWidth; // eslint-disable-line const onAnimEnd = () => { this.label.classList.remove("flash-out"); this.label.removeEventListener("animationend", onAnimEnd);
}; this.label.addEventListener("animationend", onAnimEnd); this.label.classList.add("flash-out");
},
/** * Simple widget to make nodes matching a CSS selector editable. * * @param {Object} options * An object with the following format: * { * // The node that will act as a container for the editor e.g. a * // div or table. * root: someNode, * * // The onTab event to be handled by the caller. * onTab: function(event) { ... } * * // Optional event used to trigger the editor. By default this is * // dblclick. * onTriggerEvent: "dblclick", * * // Array or comma separated string of CSS Selectors matching * // elements that are to be made editable. * selectors: [ * "#name .table-widget-cell", * "#value .table-widget-cell" * ] * }
*/ function EditableFieldsEngine(options) {
EventEmitter.decorate(this);
if (!Array.isArray(options.selectors)) {
options.selectors = [options.selectors];
}
/** * Called when a trigger event is detected (default is dblclick). * * @param {EventTarget} target * Calling event's target.
*/
onTrigger({ target }) { this.edit(target);
},
/** * Handle keydowns when in edit mode: * - <escape> revert the value and close the textbox. * - <return> apply the value and close the textbox. * - <tab> Handled by the consumer's `onTab` callback. * - <shift><tab> Handled by the consumer's `onTab` callback. * * @param {Event} event * The calling event.
*/
onKeydown(event) { if (!this.textbox) { return;
}
switch (event.keyCode) { case KeyCodes.DOM_VK_ESCAPE: this.cancelEdit();
event.preventDefault(); break; case KeyCodes.DOM_VK_RETURN: this.completeEdit(); break; case KeyCodes.DOM_VK_TAB: if (this.onTab) { this.onTab(event);
} break;
}
},
/** * Overlay the target node with an edit field. * * @param {Node} target * Dom node to be edited.
*/
edit(target) { if (!target) { return;
}
// Some item names and values are not parsable by the client or server so should not be // editable. const name = target.getAttribute("data-id"); const item = this.items.get(name); if ("isValueEditable" in item && !item.isValueEditable) { return;
}
target.scrollIntoView(false);
target.focus();
if (!target.matches(this.selectors.join(","))) { return;
}
// If we are actively editing something complete the edit first. if (this.isEditing) { this.completeEdit();
}
/** * Cancel an edit.
*/
cancelEdit() { if (!this.isEditing) { return;
} if (this.currentTarget) { this.currentTarget.hidden = false;
}
this.textbox.hidden = true;
},
/** * Stop edit mode and apply changes.
*/
blur() { if (this.isEditing) { this.completeEdit();
}
},
/** * Copies various styles from one node to another. * * @param {Node} source * The node to copy styles from. * @param {Node} destination [description] * The node to copy styles to.
*/
copyStyles(source, destination) { const style = source.ownerDocument.defaultView.getComputedStyle(source); const props = [ "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "fontFamily", "fontSize", "fontWeight", "height", "marginTop", "marginRight", "marginBottom", "marginLeft", "marginInlineStart", "marginInlineEnd",
];
for (const prop of props) {
destination.style[prop] = style[prop];
}
// We need to set the label width to 100% to work around a XUL flex bug.
destination.style.width = "100%";
},
/** * Destroys all editors in the current document.
*/
destroy() { if (this.textbox) { this.textbox.removeEventListener("keydown", this.onKeydown); this.textbox.remove();
}
if (this.root) { this.root.removeEventListener(this.onTriggerEvent, this.onTrigger); this.root.ownerDocument.removeEventListener("blur", this.completeEdit);
}
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.38Bemerkung:
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.