UNPKG

@eclipse-scout/core

Version:
1,025 lines (902 loc) 36.3 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import { AbstractLayout, aria, arrays, Cell, Column, ColumnUserFilter, ColumnUserFilterValues, Device, EnumObject, EventHandler, FilterFieldsGroupBox, graphics, HtmlComponent, InitModelOf, ListBoxTableAccessibilityRenderer, NumberColumn, NumberColumnAggregationFunction, NumberField, Popup, RowLayout, scout, SomeRequired, Table, TableHeader, TableHeaderMenuButton, TableHeaderMenuEventMap, TableHeaderMenuGroup, TableHeaderMenuGroupItem, TableHeaderMenuLayout, TableHeaderMenuModel, TableRow, TableRowModel, TableRowsCheckedEvent, ValueField } from '../index'; export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { declare model: TableHeaderMenuModel; declare initModel: SomeRequired<this['model'], 'parent' | 'column' | 'tableHeader'>; declare eventMap: TableHeaderMenuEventMap; declare self: TableHeaderMenu; column: Column<any>; tableHeader: TableHeader; compact: boolean; table: Table; filterTable: Table; filter: ColumnUserFilter; filterCheckedMode: TableHeaderMenuCheckedMode; filterSortMode: TableHeaderMenuSortMode; hasFilterTable: boolean; hasFilterFields: boolean; leftGroups: TableHeaderMenuGroup[]; moveGroup: TableHeaderMenuGroup; hierarchyGroup: TableHeaderMenuGroup; toBeginButton: TableHeaderMenuButton; forwardButton: TableHeaderMenuButton; backwardButton: TableHeaderMenuButton; toEndButton: TableHeaderMenuButton; sortingGroup: TableHeaderMenuGroup; sortAscButton: TableHeaderMenuButton; sortDescButton: TableHeaderMenuButton; sortAscAddButton: TableHeaderMenuButton; sortDescAddButton: TableHeaderMenuButton; columnActionsGroup: TableHeaderMenuGroup; addColumnButton: TableHeaderMenuButton; removeColumnButton: TableHeaderMenuButton; modifyColumnButton: TableHeaderMenuButton; groupButton: TableHeaderMenuButton; groupAddButton: TableHeaderMenuButton; barChartButton: TableHeaderMenuButton; colorGradient1Button: TableHeaderMenuButton; colorGradient2Button: TableHeaderMenuButton; collapseAllButton: TableHeaderMenuButton; expandAllButton: TableHeaderMenuButton; sumButton: TableHeaderMenuButton; averageButton: TableHeaderMenuButton; minimumButton: TableHeaderMenuButton; maximumButton: TableHeaderMenuButton; filterFieldsGroupBox: FilterFieldsGroupBox; $rightGroups: JQuery[]; $headerItem: JQuery; $columnActions: JQuery; $columnFilters: JQuery; $filterTableGroup: JQuery; $filterToggleChecked: JQuery; $filterTableGroupTitle: JQuery; $filterSortOrder: JQuery; $filterFieldsGroup: JQuery; $body: JQuery; protected _onColumnMovedHandler: () => void; protected _tableFilterHandler: () => void; protected _tableHeaderScrollHandler: (event: JQuery.ScrollEvent) => void; protected _filterTableRowsCheckedHandler: EventHandler<TableRowsCheckedEvent>; constructor() { super(); this.column = null; this.tableHeader = null; this.table = null; this.filter = null; this.filterCheckedMode = TableHeaderMenu.CheckedMode.ALL; this.filterSortMode = TableHeaderMenu.SortMode.ALPHABETICALLY; this.hasFilterTable = false; this.hasFilterFields = false; this.animateOpening = true; this.animateRemoval = true; this.focusableContainer = true; this.leftGroups = []; this.moveGroup = null; this.toBeginButton = null; this.forwardButton = null; this.backwardButton = null; this.toEndButton = null; this.sortingGroup = null; this.sortDescButton = null; this.sortAscAddButton = null; this.sortDescAddButton = null; this.columnActionsGroup = null; this.addColumnButton = null; this.removeColumnButton = null; this.modifyColumnButton = null; this.groupButton = null; this.groupAddButton = null; this.barChartButton = null; this.colorGradient1Button = null; this.colorGradient2Button = null; this.collapseAllButton = null; this.expandAllButton = null; this.sumButton = null; this.averageButton = null; this.minimumButton = null; this.maximumButton = null; this.$rightGroups = []; this.$headerItem = null; this.$columnActions = null; this.$columnFilters = null; this.$filterTableGroup = null; this.$filterToggleChecked = null; this.$filterTableGroupTitle = null; this.$filterSortOrder = null; this.$filterFieldsGroup = null; this._onColumnMovedHandler = this._onColumnMoved.bind(this); this._tableHeaderScrollHandler = this._onAnchorScroll.bind(this); // Make sure the actions are not disabled even if the table is disabled // To disable the menu use headerEnabled or headerMenusEnabled this.inheritAccessibility = false; } static CheckedMode = { ALL: { checkAll: true, text: 'ui.SelectAllFilter' }, NONE: { checkAll: false, text: 'ui.SelectNoneFilter' } } as const; static SortMode = { ALPHABETICALLY: { text: 'ui.SortAlphabeticallyFilter', cssClass: 'table-header-menu-toggle-sort-order-alphabetically' }, AMOUNT: { text: 'ui.SortByAmountFilter', cssClass: 'table-header-menu-toggle-sort-order-amount' } } as const; protected override _init(options: InitModelOf<this>) { options.scrollType = options.scrollType || 'none'; super._init(options); this.tableHeader = options.tableHeader; this.column = options.column; this.table = this.tableHeader.table; this.$headerItem = this.$anchor; this.table.on('columnMoved', this._onColumnMovedHandler); // Filtering this.filter = this.table.getFilter(this.column.id) as ColumnUserFilter; if (!this.filter) { this.filter = this.column.createFilter(); } // always recalculate available values to make sure new/updated/deleted rows are considered this.filter.calculate(); this.filter.on('filterFieldsChanged', this._updateFilterTable.bind(this)); this._updateFilterTableCheckedMode(); this.hasFilterTable = this.filter.availableValues.length > 0; this.hasFilterFields = this.filter.hasFilterFields; if (this.hasFilterTable) { this._tableFilterHandler = this._onFilterTableChanged.bind(this); this.table.on('filterAdded', this._tableFilterHandler); this.table.on('filterRemoved', this._tableFilterHandler); this._filterTableRowsCheckedHandler = this._onFilterTableRowsChecked.bind(this); } } protected override _createLayout(): AbstractLayout { return new TableHeaderMenuLayout(this); } protected override _render() { this.leftGroups = []; this.$rightGroups = []; this.$headerItem.select(true); this.$container = this.$parent.appendDiv('popup table-header-menu'); this.htmlComp = HtmlComponent.install(this.$container, this.session); this.htmlComp.setLayout(this._createLayout()); this.$body = this.$container.appendDiv('table-header-menu-body'); HtmlComponent.install(this.$body, this.session); this._installScrollbars({ axis: 'y', scrollShadow: 'none' }); this.$columnActions = this.$body.appendDiv('table-header-menu-actions'); HtmlComponent.install(this.$columnActions, this.session); // only add right column if filter has a filter-table or filter-fields if (this.hasFilterTable || this.hasFilterFields) { this.$columnFilters = this.$body.appendDiv('table-header-menu-filters'); let htmlColumnFilters = HtmlComponent.install(this.$columnFilters, this.session); htmlColumnFilters.setLayout(new RowLayout()); } this.tableHeader.$container.on('scroll', this._tableHeaderScrollHandler); // -- Left column -- // // Moving let movableColumns = this.table.visibleColumns().filter(column => !column.fixedPosition); if (movableColumns.length > 1 && !this.column.fixedPosition) { this.leftGroups.push(this._renderMovingGroup()); } // Sorting if (this.table.sortEnabled) { this.leftGroups.push(this._renderSortingGroup()); } // Add/remove/change columns if (this._isColumnActionsGroupVisible()) { this.leftGroups.push(this._renderColumnActionsGroup()); } // Grouping // column.grouped check necessary to make ungroup possible, even if grouping is not possible anymore if (this.table.isGroupingPossible(this.column) || this.column.grouped) { this.leftGroups.push(this._renderGroupingGroup()); } // Expand/Collapse this.leftGroups.push(this._renderHierarchyGroup()); // Width if (!this.column.fixedWidth) { this.leftGroups.push(this._renderWidthGroup()); } // Aggregation if (this.table.isAggregationPossible(this.column)) { this.leftGroups.push(this._renderAggregationGroup()); } // Coloring if (this.column instanceof NumberColumn) { this.leftGroups.push(this._renderColoringGroup()); } // -- Right column -- // // Filter table if (this.hasFilterTable) { this.$rightGroups.push(this._renderFilterTable()); } // Filter fields if (this.hasFilterFields) { this.$rightGroups.push(this._renderFilterFields()); } this._onColumnMoved(); // Set table style to focused, so that it looks as it still has the focus. if (this.table.enabled) { this.table.$container.addClass('focused'); } } override validateFocus() { if (this.filterFieldsGroupBox) { this.filterFieldsGroupBox.focus(); } // Super call will focus container if no element has been focused yet super.validateFocus(); } override get$Scrollable(): JQuery { return this.$body; } protected _updateFirstLast() { addFirstLastClass(this.leftGroups.filter(group => group.visible)); addFirstLastClass(this.$rightGroups); function addFirstLastClass(groups: (JQuery | TableHeaderMenuGroup)[]) { groups.forEach((group, index, arr) => { toggleCssClass(group, 'first', index === 0); toggleCssClass(group, 'last', index === arr.length - 1); }); } // Note: we should refactor code for filter-fields and filter-table so they could also // work with a model-class (like the button menu groups). Currently this would cause to much work. function toggleCssClass(group: JQuery | TableHeaderMenuGroup, cssClass: string, condition: boolean) { let $container = group instanceof TableHeaderMenuGroup ? group.$container : group; $container.toggleClass(cssClass, condition); } } protected override _remove() { if (this.filterTable) { this.filterTable.off('rowsChecked', this._filterTableRowsCheckedHandler); } if (this.tableHeader.rendered) { this.tableHeader.$container.off('scroll', this._tableHeaderScrollHandler); } this.$headerItem.select(false); this.table.off('columnMoved', this._onColumnMovedHandler); this.table.off('filterAdded', this._tableFilterHandler); this.table.off('filterRemoved', this._tableFilterHandler); super._remove(); // table may have been removed in the meantime if (this.table.rendered) { this.table.$container.removeClass('focused'); } } protected _renderMovingGroup(): TableHeaderMenuGroup { let table = this.table; let column = this.column; this.moveGroup = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ui.Move', cssClass: 'first' }); this.toBeginButton = scout.create(TableHeaderMenuButton, { parent: this.moveGroup, text: '${textKey:ui.toBegin}', cssClass: 'move move-top' }); this.toBeginButton.on('action', () => { table.moveColumn(column, 0); }); this.forwardButton = scout.create(TableHeaderMenuButton, { parent: this.moveGroup, text: '${textKey:ui.forward}', cssClass: 'move move-up' }); this.forwardButton.on('action', () => { let pos = table.visibleColumns().indexOf(column); table.moveColumn(column, Math.max(pos - 1, 0)); }); this.backwardButton = scout.create(TableHeaderMenuButton, { parent: this.moveGroup, text: '${textKey:ui.backward}', cssClass: 'move move-down' }); this.backwardButton.on('action', () => { let pos = table.visibleColumns().indexOf(column); table.moveColumn(column, Math.min(pos + 1, table.header.findHeaderItems().length - 1)); }); this.toEndButton = scout.create(TableHeaderMenuButton, { parent: this.moveGroup, text: '${textKey:ui.toEnd}', cssClass: 'move move-bottom' }); this.toEndButton.on('action', () => { table.moveColumn(column, table.header.findHeaderItems().length - 1); }); this.moveGroup.render(this.$columnActions); return this.moveGroup; } protected _onColumnMoved() { let column = this.column; if (this.moveGroup) { let forwardEnabled = this.table.organizer.isColumnMovableToLeft(column); let backwardEnabled = this.table.organizer.isColumnMovableToRight(column); this.toBeginButton.setEnabled(forwardEnabled); this.forwardButton.setEnabled(forwardEnabled); this.backwardButton.setEnabled(backwardEnabled); this.toEndButton.setEnabled(backwardEnabled); } this.hierarchyGroup.setVisible(this.table.isTableNodeColumn(column)); this._updateFirstLast(); } protected _isColumnActionsGroupVisible(): boolean { return this.table.isColumnAddable() || this.table.isColumnRemovable(this.column) || this.table.isColumnModifiable(this.column); } protected _renderColumnActionsGroup(): TableHeaderMenuGroup { this.columnActionsGroup = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ui.Column' }); this.addColumnButton = scout.create(TableHeaderMenuButton, { parent: this.columnActionsGroup, text: '${textKey:ui.addColumn}', cssClass: 'add-column', visible: this.table.isColumnAddable() }); this.addColumnButton.on('action', () => { this.close(); this.table.organizer.addColumn(this.column); }); this.removeColumnButton = scout.create(TableHeaderMenuButton, { parent: this.columnActionsGroup, text: '${textKey:ui.removeColumn}', cssClass: 'remove-column', visible: this.table.isColumnRemovable(this.column) }); this.removeColumnButton.on('action', () => { this.close(); this.table.organizer.removeColumns([this.column]); }); this.modifyColumnButton = scout.create(TableHeaderMenuButton, { parent: this.columnActionsGroup, text: '${textKey:ui.changeColumn}', cssClass: 'change-column', visible: this.table.isColumnModifiable(this.column) }); this.modifyColumnButton.on('action', () => { this.close(); this.table.organizer.modifyColumn(this.column); }); this.columnActionsGroup.render(this.$columnActions); return this.columnActionsGroup; } protected _renderSortingGroup(): TableHeaderMenuGroup { let table = this.table, column = this.column, menuPopup = this; this.sortingGroup = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ColumnSorting' }); if (!table.hasPermanentHeadOrTailSortColumns()) { this.sortAscButton = scout.create(TableHeaderMenuButton, { parent: this.sortingGroup, text: '${textKey:ui.ascending}', cssClass: 'sort sort-asc', direction: 'asc', toggleAction: true }); this.sortAscButton.on('action', onSortClick.bind(this.sortAscButton)); this.sortDescButton = scout.create(TableHeaderMenuButton, { parent: this.sortingGroup, text: '${textKey:ui.descending}', cssClass: 'sort sort-desc', direction: 'desc', toggleAction: true }); this.sortDescButton.on('action', onSortClick.bind(this.sortDescButton)); } this.sortAscAddButton = scout.create(TableHeaderMenuButton, { parent: this.sortingGroup, text: '${textKey:ui.ascendingAdditionally}', cssClass: 'sort sort-asc-add', direction: 'asc', toggleAction: true }); this.sortAscAddButton.on('action', onSortAdditionalClick.bind(this.sortAscAddButton)); this.sortDescAddButton = scout.create(TableHeaderMenuButton, { parent: this.sortingGroup, text: '${textKey:ui.descendingAdditionally}', cssClass: 'sort sort-desc-add', direction: 'desc', toggleAction: true }); this.sortDescAddButton.on('action', onSortAdditionalClick.bind(this.sortDescAddButton)); this._updateSortingSelectedState(); this.sortingGroup.render(this.$columnActions); return this.sortingGroup; function onSortClick() { menuPopup.close(); sort(this.direction, false, !this.selected); } function onSortAdditionalClick() { menuPopup.close(); sort(this.direction, true, !this.selected); } function sort(direction: 'asc' | 'desc', multiSort: boolean, remove: boolean) { table.sort(column, direction, multiSort, remove); menuPopup._updateSortingSelectedState(); } } protected _updateSortingSelectedState() { if (!this.table.sortEnabled) { return; } let showAddCommands = false, sortCount = this._sortColumnCount(); this.sortingGroup.children.forEach((button: TableHeaderMenuButton) => button.setSelected(false)); if (sortCount === 1 && !this.table.hasPermanentHeadOrTailSortColumns()) { if (this.column.sortActive) { if (this.column.sortAscending) { this.sortAscButton.setSelected(true); } else { this.sortDescButton.setSelected(true); } } else { showAddCommands = true; } } else if (sortCount > 1 || this.table.hasPermanentHeadOrTailSortColumns()) { showAddCommands = true; if (this.column.sortActive) { if (this.column.sortAscending) { this.sortAscAddButton.setSelected(true); } else { this.sortDescAddButton.setSelected(true); } let addIcon = (this.column.sortIndex + 1) + ''; this.sortAscAddButton.setIconId(addIcon); this.sortDescAddButton.setIconId(addIcon); } } this.sortAscAddButton.setVisible(showAddCommands); this.sortDescAddButton.setVisible(showAddCommands); } protected _renderGroupingGroup(): TableHeaderMenuGroup { let menuPopup = this, table = this.table, column = this.column, groupCount = this._groupColumnCount(); let group = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ui.Grouping' }); this.groupButton = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.groupingApply}', cssClass: 'group', additional: false, toggleAction: true }); this.groupButton.on('action', groupColumn.bind(this.groupButton)); this.groupAddButton = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.additionally}', cssClass: 'group-add', additional: true, toggleAction: true }); this.groupAddButton.on('action', groupColumn.bind(this.groupAddButton)); if (groupCount === 0) { this.groupAddButton.setVisible(false); } else if (groupCount === 1 && this.column.grouped) { this.groupButton.setSelected(true); this.groupAddButton.setVisible(false); } else if (groupCount > 1) { this.groupAddButton.setVisible(true); } if (table.hasPermanentHeadOrTailSortColumns() && groupCount > 0) { // If table has permanent head columns, other columns may not be grouped exclusively -> only enable add button (equally done for sort buttons) this.groupButton.setVisible(false); this.groupAddButton.setVisible(true); } if (this.column.grouped) { if (groupCount === 1) { this.groupAddButton.setSelected(true); } else if (groupCount > 1) { this.groupAddButton.setSelected(true); this.groupAddButton.setIconId((this.column.sortIndex + 1) + ''); } } group.render(this.$columnActions); return group; function groupColumn() { let direction: 'asc' | 'desc' = (column.sortIndex >= 0 && !column.sortAscending) ? 'desc' : 'asc'; menuPopup.close(); table.group(column, direction, this.additional, !this.selected); } } protected _renderHierarchyGroup(): TableHeaderMenuGroup { let table = this.table, menuPopup = this; this.hierarchyGroup = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ui.Hierarchy', visible: this.table.isTableNodeColumn(this.column) }); this.collapseAllButton = scout.create(TableHeaderMenuButton, { parent: this.hierarchyGroup, text: '${textKey:ui.CollapseAll}', cssClass: 'hierarchy-collapse-all', enabled: !!arrays.find(table.rows, row => row.expanded && !arrays.empty(row.childRows)) }); this.collapseAllButton.on('action', () => { menuPopup.close(); table.collapseAll(); }); this.expandAllButton = scout.create(TableHeaderMenuButton, { parent: this.hierarchyGroup, text: '${textKey:ui.ExpandAll}', cssClass: 'hierarchy-expand-all', enabled: !!arrays.find(table.rows, row => !row.expanded && !arrays.empty(row.childRows)) }); this.expandAllButton.on('action', () => { menuPopup.close(); table.expandAll(); }); this.hierarchyGroup.render(this.$columnActions); return this.hierarchyGroup; } protected _renderWidthGroup(): TableHeaderMenuGroup { let group = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'Width' }); let optimizeWidthButton = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.optimizeWidth}', cssClass: 'optimize-width' }); optimizeWidthButton.on('action', () => { this.table.resizeToFit(this.column); this.close(); }); let optimizeWidthAllButton = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.optimizeWidthAll}', cssClass: 'optimize-widths' }); optimizeWidthAllButton.on('action', () => { this.table.visibleColumns().forEach(column => this.table.resizeToFit(column)); this.close(); }); let widthField = scout.create(NumberField, { parent: group, cssClass: 'table-header-menu-command width no-mandatory-indicator', label: '${textKey:Width}', labelVisible: false, clearable: ValueField.Clearable.NEVER, value: this.column.width, minValue: this.column.minWidth, gridData: { // Don't use hints because parent has no logical grid but FormField._updateElementInnerAlignment expects that horizontalAlignment: 0 } }) as NumberField & TableHeaderMenuGroupItem; widthField.on('propertyChange:value', () => { this.table.resizeColumn(this.column, widthField.value); }); widthField.computeGroupSuffix = () => this.session.text('ui.adjust'); group.render(this.$columnActions); HtmlComponent.install(group.$container, this.session); return group; } protected _renderAggregationGroup(): TableHeaderMenuGroup { let table = this.table, column = this.column as NumberColumn, aggregation = column.aggregationFunction, menuPopup = this, group = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ui.Aggregation' }), allowedAggregationFunctions = arrays.ensure(column.allowedAggregationFunctions), isAggregationNoneAllowed = allowedAggregationFunctions.indexOf('none') !== -1; this.sumButton = createHeaderMenuButtonForAggregationFunction('${textKey:ui.Sum}', 'sum'); this.averageButton = createHeaderMenuButtonForAggregationFunction('${textKey:ui.Average}', 'avg'); this.minimumButton = createHeaderMenuButtonForAggregationFunction('${textKey:ui.Minimum}', 'min'); this.maximumButton = createHeaderMenuButtonForAggregationFunction('${textKey:ui.Maximum}', 'max'); group.children.forEach((button: TableHeaderMenuButton) => button.setSelected(button.aggregation === aggregation)); group.render(this.$columnActions); return group; function createHeaderMenuButtonForAggregationFunction(text: string, aggregation: NumberColumnAggregationFunction): TableHeaderMenuButton { if (allowedAggregationFunctions.indexOf(aggregation) !== -1) { let aggrButton = scout.create(TableHeaderMenuButton, { parent: group, text: text, cssClass: 'aggregation-function ' + aggregation, aggregation: aggregation, toggleAction: isAggregationNoneAllowed }); aggrButton.on('action', onClick.bind(aggrButton)); return aggrButton; } return null; } function onClick() { menuPopup.close(); table.changeAggregation(column, this.aggregation === aggregation ? 'none' : this.aggregation); } } protected _renderColoringGroup(): TableHeaderMenuGroup { let table = this.table, column = this.column as NumberColumn, menuPopup = this, backgroundEffect = column.backgroundEffect, group = scout.create(TableHeaderMenuGroup, { parent: this, textKey: 'ui.Coloring' }); this.colorGradient1Button = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.fromRedToGreen}', cssClass: 'color color-gradient1', backgroundEffect: 'colorGradient1', toggleAction: true }); this.colorGradient1Button.on('action', onClick.bind(this.colorGradient1Button)); this.colorGradient2Button = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.fromGreenToRed}', cssClass: 'color color-gradient2', backgroundEffect: 'colorGradient2', toggleAction: true }); this.colorGradient2Button.on('action', onClick.bind(this.colorGradient2Button)); if (Device.get().supportsCssGradient()) { this.barChartButton = scout.create(TableHeaderMenuButton, { parent: group, text: '${textKey:ui.withBarChart}', cssClass: 'color color-bar-chart', backgroundEffect: 'barChart', toggleAction: true }); this.barChartButton.on('action', onClick.bind(this.barChartButton)); } group.children.forEach((button: TableHeaderMenuButton) => button.setSelected(button.backgroundEffect === backgroundEffect)); group.render(this.$columnActions); return group; function onClick() { menuPopup.close(); table.setColumnBackgroundEffect(column, !this.selected ? null : this.backgroundEffect); } } protected _renderFilterTable(): JQuery { this.$filterTableGroup = this.$columnFilters .appendDiv('table-header-menu-group first'); let htmlComp = HtmlComponent.install(this.$filterTableGroup, this.session); htmlComp.setLayout(new RowLayout()); let $filterActions = this.$filterTableGroup .appendDiv('table-header-menu-filter-actions'); this.$filterSortOrder = $filterActions .appendDiv('link table-header-menu-toggle-sort-order') .on('click', this._onSortModeClick.bind(this)) .addClass(this.filterSortMode.cssClass); this.$filterToggleChecked = $filterActions .appendDiv('link table-header-menu-filter-toggle-checked') .text(this.session.text(this.filterCheckedMode.text)) .on('click', this._onFilterCheckedModeClick.bind(this)); this.$filterTableGroupTitle = this.$filterTableGroup .appendDiv('table-header-menu-group-text') .text(this._filterByText()); HtmlComponent.install(this.$filterTableGroupTitle, this.session); this.filterTable = this._createFilterTable(); this.filterTable.accessibilityRenderer = new ListBoxTableAccessibilityRenderer(); this.filterTable.on('rowsChecked', this._filterTableRowsCheckedHandler); let tableRows: TableRowModel[] = []; this.filter.availableValues.forEach(filterValue => { let tableRow: TableRowModel = { cells: [ scout.create(Cell, { text: (this.filter.column instanceof NumberColumn) ? filterValue.text : null, value: (this.filter.column instanceof NumberColumn) ? filterValue.key : filterValue.text, iconId: filterValue.iconId, htmlEnabled: filterValue.htmlEnabled, cssClass: filterValue.cssClass }), filterValue.count, filterValue.key === null ? 1 : 0 // empty cell should always be at the bottom ], checked: this.filter.selectedValues.indexOf(filterValue.key) > -1, dataMap: { filterValue: filterValue } }; tableRows.push(tableRow); }); this.filterTable.insertRows(tableRows); this.filterTable.render(this.$filterTableGroup); aria.linkElementWithLabel(this.filterTable.$container, this.$filterTableGroupTitle); // must do this in a setTimeout, since table/popup is not visible yet (same as Table#revealSelection). setTimeout(this.filterTable.revealChecked.bind(this.filterTable)); return this.$filterTableGroup; } protected _createFilterTable(): Table { let objectType = Column<any>; if (this.column instanceof NumberColumn) { objectType = NumberColumn; } return scout.create(Table, { parent: this, headerVisible: false, multiSelect: false, autoResizeColumns: true, checkable: true, cssClass: 'table-header-menu-filter-table', checkableStyle: Table.CheckableStyle.TABLE_ROW, // column-texts are not visible since header is not visible columns: [{ objectType: objectType, text: 'filter-value', width: 120, sortIndex: 1, horizontalAlignment: -1 }, { objectType: NumberColumn, text: 'aggregate-count', cssClass: 'table-header-menu-filter-number-column', width: 50, minWidth: 32, autoOptimizeWidth: true }, { objectType: NumberColumn, displayable: false, sortIndex: 0 }] }); } /** * @returns the title-text used for the filter-table */ protected _filterByText(): string { let text = this.session.text('ui.Filter'), numSelected = this.filter.selectedValues.length, numFilters = this.filter.availableValues.length; if (numSelected && numFilters) { text += ' ' + this.session.text('ui.FilterInfoXOfY', numSelected + '', numFilters + ''); } else if (numFilters) { text += ' ' + this.session.text('ui.FilterInfoCount', numFilters + ''); } return text; } protected _onFilterCheckedModeClick() { let checkedMode = TableHeaderMenu.CheckedMode; let checkAll = this.filterCheckedMode.checkAll; this.filter.selectedValues = []; if (this.filterCheckedMode === checkedMode.ALL) { this.filterCheckedMode = checkedMode.NONE; this.filter.availableValues.forEach(filterValue => this.filter.selectedValues.push(filterValue.key)); } else { this.filterCheckedMode = checkedMode.ALL; } this.filterTable.checkAll(checkAll); this._updateFilterTableActions(); } protected _onSortModeClick() { let sortMode = TableHeaderMenu.SortMode; if (this.filterSortMode === sortMode.ALPHABETICALLY) { // sort by amount this.filterTable.sort(this.filterTable.columns[1], 'desc'); this.filterSortMode = sortMode.AMOUNT; } else { // sort alphabetically (first by invisible column to make sure empty cells are always at the bottom) this.filterTable.sort(this.filterTable.columns[2], 'asc'); this.filterTable.sort(this.filterTable.columns[0], 'asc', true); this.filterSortMode = sortMode.ALPHABETICALLY; } this._updateFilterTableActions(); } protected _updateFilterTable() { if (this.filter.filterActive()) { this.table.addFilter(this.filter); } else { this.table.removeFilterByKey(this.column.id); } } protected _updateFilterTableActions() { // checked mode this.$filterToggleChecked.text(this.session.text(this.filterCheckedMode.text)); // sort mode let sortMode = TableHeaderMenu.SortMode; let sortAlphabetically = this.filterSortMode === TableHeaderMenu.SortMode.ALPHABETICALLY; this.$filterSortOrder.toggleClass(sortMode.ALPHABETICALLY.cssClass, sortAlphabetically); this.$filterSortOrder.toggleClass(sortMode.AMOUNT.cssClass, !sortAlphabetically); } protected _renderFilterFields(): JQuery { this.filterFieldsGroupBox = scout.create(FilterFieldsGroupBox, { parent: this, filter: this.filter }); this.$filterFieldsGroup = this.$columnFilters.appendDiv('table-header-menu-group'); let htmlComp = HtmlComponent.install(this.$filterFieldsGroup, this.session); htmlComp.setLayout(new RowLayout()); let $filterFieldsText = this.$filterFieldsGroup .appendDiv('table-header-menu-group-text') .text(this.filter.filterFieldsTitle()); htmlComp = HtmlComponent.install($filterFieldsText, this.session); this.filterFieldsGroupBox.render(this.$filterFieldsGroup); this.filterFieldsGroupBox.linkFieldsWithTitle($filterFieldsText); return this.$filterFieldsGroup; } isOpenFor($headerItem: JQuery): boolean { return this.rendered && this.belongsTo($headerItem); } protected _sortColumnCount(): number { return this.table.visibleSortColumnsCount(); } protected _groupColumnCount(): number { return this.table.visibleGroupColumnsCount(); } protected _renderCompact() { this.$body.toggleClass('compact', this.compact); this.invalidateLayoutTree(); } setCompact(compact: boolean) { this.setProperty('compact', compact); } protected override _position(switchIfNecessary?: boolean) { let headerItemBounds = graphics.offsetBounds(this.$headerItem); let containerBounds = graphics.offsetBounds(this.tableHeader.$container); // Hide menu when the header item is not in view (menu gets repositioned when the table gets scrolled). // This cannot be done in _isAnchorInView() because the TableHeaderLayout would mark the menu as 'compact' when scrolling outside the right side of the table. let inView = headerItemBounds.x < containerBounds.right() && headerItemBounds.right() > containerBounds.x; this.$container.setVisible(inView); super._position(switchIfNecessary); } protected override _onAnchorScroll(event: JQuery.ScrollEvent) { this.position(); } protected _onFilterTableRowsChecked(event: TableRowsCheckedEvent) { this.filter.selectedValues = []; this.filterTable.rows.forEach((row: TableHeaderMenuTableRow) => { if (row.checked) { this.filter.selectedValues.push(row.dataMap.filterValue.key); } }); this._updateFilterTable(); } protected _onFilterTableChanged() { this.$filterTableGroupTitle.text(this._filterByText()); this._updateFilterTableCheckedMode(); this._updateFilterTableActions(); } // When no filter value is selected, we change the selection mode to ALL // since it makes no sense to choose NONE when no value is currently selected protected _updateFilterTableCheckedMode() { if (this.filter.selectedValues.length === 0) { this.filterCheckedMode = TableHeaderMenu.CheckedMode.ALL; } else { this.filterCheckedMode = TableHeaderMenu.CheckedMode.NONE; } } protected override _onMouseDownOutside(event: MouseEvent) { // close popup only if source of event is not $headerItem or one of it's children. if (this.$headerItem.isOrHas(event.target as HTMLElement)) { return; } this.close(); } } export type TableHeaderMenuCheckedMode = EnumObject<typeof TableHeaderMenu.CheckedMode>; export type TableHeaderMenuSortMode = EnumObject<typeof TableHeaderMenu.SortMode>; export type TableHeaderMenuTableRow = TableRow & { dataMap: Record<string, ColumnUserFilterValues> };