UNPKG

@eclipse-scout/core

Version:
891 lines (755 loc) 31.5 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 { aria, arrays, Column, ColumnModel, ColumnUserFilter, Device, EventHandler, graphics, GroupBoxMenuItemsOrder, InitModelOf, inspector, MenuBar, MenuDestinations, ObjectIdProvider, objects, PropertyChangeEvent, Rectangle, scout, scrollbars, SomeRequired, strings, styles, Table, TableColumnMovedEvent, TableColumnResizedEvent, TableFilterAddedEvent, TableFilterRemovedEvent, TableHeaderEventMap, TableHeaderMenu, TableHeaderModel, tooltips, Widget } from '../index'; import $ from 'jquery'; export class TableHeader extends Widget implements TableHeaderModel { declare model: TableHeaderModel; declare initModel: SomeRequired<this['model'], 'parent' | 'table'>; declare eventMap: TableHeaderEventMap; declare self: TableHeader; headerMenusEnabled: boolean; table: Table; dragging: boolean; columnMoved: boolean; menuBar: MenuBar; tableHeaderMenu: TableHeaderMenu; headerLabelId: string; $menuBarContainer: JQuery; $filler: JQuery; protected _tableDataScrollHandler: () => void; protected _tableAddFilterRemovedHandler: EventHandler<TableFilterAddedEvent | TableFilterRemovedEvent>; protected _tableColumnResizedHandler: EventHandler<TableColumnResizedEvent>; protected _tableColumnMovedHandler: EventHandler<TableColumnMovedEvent>; protected _renderedColumns: Column<any>[]; protected _$window: JQuery<Window>; protected _$body: JQuery<Body>; protected _fixTimeout: number; constructor() { super(); this.dragging = false; this.headerMenusEnabled = true; this.table = null; this.headerLabelId = null; this._tableDataScrollHandler = this._onTableDataScroll.bind(this); this._tableAddFilterRemovedHandler = this._onTableAddFilterRemoved.bind(this); this._tableColumnResizedHandler = this._onTableColumnResized.bind(this); this._tableColumnMovedHandler = this._onTableColumnMoved.bind(this); this._renderedColumns = []; } protected override _init(options: InitModelOf<this>) { super._init(options); this.menuBar = scout.create(MenuBar, { parent: this, tabbable: false, position: MenuBar.Position.BOTTOM, menuOrder: new GroupBoxMenuItemsOrder() }); this.menuBar.on('propertyChange', this._onMenuBarPropertyChange.bind(this)); this.updateMenuBar(); } protected override _render() { this.$container = this.table.$data.beforeDiv('table-header') .cssBorderLeftWidth(this.table.rowBorders.left || ''); aria.role(this.$container, 'row'); // Filler is necessary to make sure the header is always as large as the table data, otherwise horizontal scrolling does not work correctly this.$filler = this.$container.appendDiv('table-header-item filler').css('visibility', 'hidden'); // Required to make "height: 100%" rule work. menuBarContainer and menuBar itself must have the same visibility. // Otherwise, they could cover the sorting/filter icons on the table-header of the column. this.$menuBarContainer = this.$container .appendDiv('menubar-container') .setVisible(this.menuBar.visible); this.menuBar.render(this.$menuBarContainer); this._$window = this.$container.window(); this._$body = this.$container.body(); this._renderColumns(); this.table.$data.on('scroll', this._tableDataScrollHandler); this.table.on('filterAdded', this._tableAddFilterRemovedHandler); this.table.on('filterRemoved', this._tableAddFilterRemovedHandler); this.table.on('columnResized', this._tableColumnResizedHandler); this.table.on('columnMoved', this._tableColumnMovedHandler); } protected override _remove() { this.table.$data.off('scroll', this._tableDataScrollHandler); this.table.off('filterAdded', this._tableAddFilterRemovedHandler); this.table.off('filterRemoved', this._tableAddFilterRemovedHandler); this.table.off('columnResized', this._tableColumnResizedHandler); this.table.off('columnMoved', this._tableColumnMovedHandler); this._removeColumns(); super._remove(); } rerenderColumns() { this._removeColumns(); this._renderColumns(); } protected _renderColumns() { let visibleColumns = this._visibleColumns(); visibleColumns.forEach(this._renderColumn, this); if (visibleColumns.length === 0) { // If there are no columns, make the filler visible and make sure the header is as large as normally using nbsp this.$filler.css('visibility', 'visible').html('&nbsp;').addClass('empty'); } this._reconcileScrollPos(); } protected _renderColumn(column: Column<any>, index: number) { let columnWidth = column.realWidthIfAvailable(), visibleColumns = this._visibleColumns(), isFirstColumn = (index === 0), isLastColumn = (index === visibleColumns.length - 1); let $header = this.$filler.beforeDiv('table-header-item') .setEnabled(this.enabled) // enabledComputed not used on purpose .data('column', column); aria.role($header, 'columnheader'); let margins = graphics.margins($header); columnWidth -= margins.horizontal(); $header.cssMinWidth(columnWidth).cssMaxWidth(columnWidth); // add label id to header item text, so table cells can reference it for screen readers this.headerLabelId = ObjectIdProvider.get().createUiSeqId(); $header.appendSpan('table-header-item-text').attr('id', this.headerLabelId); if (this.enabled) { // enabledComputed not used on purpose $header .on('click', this._onHeaderItemClick.bind(this)) .on('mousedown', this._onHeaderItemMouseDown.bind(this)); } inspector.applyInfo(column, $header); if (isFirstColumn) { $header.addClass('first'); } if (isLastColumn) { $header.addClass('last'); } column.$header = $header; this._installHeaderItemTooltip(column); this._decorateHeader(column); let showSeparator = column.showSeparator; if (isLastColumn && !this.enabled) { // enabledComputed not used on purpose showSeparator = false; } if (showSeparator) { let $separator = this.$filler.beforeDiv('table-header-resize'); aria.role($separator, 'none'); if (column.fixedWidth || !this.enabled) { // enabledComputed not used on purpose $separator.setEnabled(false); } else { $separator .on('mousedown', '', this._onSeparatorMouseDown.bind(this)) .on('dblclick', this._onSeparatorDblclick.bind(this)); } column.$separator = $separator; } this._renderedColumns.push(column); } protected _removeColumns() { this._renderedColumns.slice().forEach(this._removeColumn, this); } protected _removeColumn(column: Column<any>) { if (column.$header) { column.$header.remove(); column.$header = null; } if (column.$separator) { column.$separator.remove(); column.$separator = null; } arrays.remove(this._renderedColumns, column); } resizeHeaderItem(column: Column<any>) { if (!column) { // May be undefined if there are no columns return; } if (!column.$header) { // May be undefined if called when header item is not rendered yet (e.g. coming from _adjustColumnMinWidth) return; } let $header = column.$header, columnWidth = column.realWidthIfAvailable(), margins = graphics.margins($header), menuBarWidth = (this.menuBar.visible ? this.$menuBarContainer.outerWidth(true) : 0), visibleColumns = this._visibleColumns(), visibleColumnIndex = visibleColumns.indexOf(column), isLastColumn = visibleColumnIndex === visibleColumns.length - 1; columnWidth -= margins.horizontal(); if (isLastColumn && menuBarWidth > 0) { let remainingHeaderSpace = this.$container.width() - this.table.rowWidth + graphics.insets(this.$container).right; if (remainingHeaderSpace < menuBarWidth) { let adjustment = menuBarWidth; if (column.$separator) { adjustment += column.$separator.width(); } if (remainingHeaderSpace > 0) { adjustment -= remainingHeaderSpace; } let origColumnWidth = columnWidth; columnWidth = Math.max(columnWidth - adjustment, column.minWidth); this.$filler.cssWidth(origColumnWidth - columnWidth); } } $header .cssMinWidth(columnWidth) .cssMaxWidth(columnWidth); } /** * Resizes all header items to theirs desired widths. */ resizeHeaderItems() { this._visibleColumns().forEach(this.resizeHeaderItem.bind(this)); } protected _reconcileScrollPos() { // When scrolling horizontally scroll header as well let scrollLeft = this.table.get$Scrollable().scrollLeft(), lastColumn = this._lastVisibleColumn(); this.resizeHeaderItem(lastColumn); this.$container.scrollLeft(scrollLeft); this.$menuBarContainer.cssRight(-1 * scrollLeft); } protected _arrangeHeaderItems($headers: JQuery) { let that = this; $headers.each(function() { // move to old position and then animate const $headerItem = $(this); const oldLeft = $headerItem.data('old-left') - $headerItem.offset().left; $headerItem.cssLeftAnimated(oldLeft, 0, { progress: function(animation, progress, remainingMs) { const $headerItem = $(this); if (!$headerItem.isSelected()) { return; } // make sure selected header item is visible scrollbars.scrollHorizontalTo(that.table.$data, $headerItem); // move menu if (that.tableHeaderMenu && that.tableHeaderMenu.rendered) { that.tableHeaderMenu.position(); } } }); }); } protected _installHeaderItemTooltip(column: Column<any>) { tooltips.install(column.$header, { parent: this, text: this._headerItemTooltipText.bind(this), arrowPosition: 50, arrowPositionUnit: '%', originProducer: this._headerItemTooltipOrigin.bind(this), nativeTooltip: !Device.get().isCustomEllipsisTooltipPossible(), htmlEnabled: this._headerItemTooltipHtmlEnabled.bind(this) }); } protected _installHeaderItemTooltips() { this._visibleColumns().forEach(this._installHeaderItemTooltip, this); } protected _uninstallHeaderItemTooltip(column: Column<any>) { tooltips.uninstall(column.$header); } protected _uninstallHeaderItemTooltips() { this._visibleColumns().forEach(this._uninstallHeaderItemTooltip, this); } protected _headerItemTooltipText($col: JQuery): string { let column = $col.data('column') as Column<any>; if (column && strings.hasText(column.headerTooltipText)) { return column.headerTooltipText; } let $text = $col.children('.table-header-item-text'); if ($text.isContentTruncated() || !this._textInView($text)) { // Show a tooltip if the content is truncated (text shows an ellipsis) or if the text is partially out of view because of the horizontal scroll position of the table let text = strings.plainText($text.html(), {trim: true}); if (strings.hasText(text)) { return text; } } return null; } protected _textInView($text: JQuery): boolean { let textBounds = graphics.offsetBounds($text); let containerBounds = this._offsetBoundsWithoutMenuBar(); return textBounds.right() <= containerBounds.right() && textBounds.x >= containerBounds.x; } /** * @returns the part of the header item that is visible in the current viewport of the table so the tooltip won't point to an invisible part of the header item */ protected _headerItemTooltipOrigin($col: JQuery): Rectangle { let headerItemBounds = graphics.offsetBounds($col); let containerBounds = this._offsetBoundsWithoutMenuBar(); return containerBounds.intersect(headerItemBounds); } protected _offsetBoundsWithoutMenuBar(): Rectangle { let containerBounds = graphics.offsetBounds(this.$container); containerBounds.width -= graphics.size(this.$menuBarContainer).width; return containerBounds; } protected _headerItemTooltipHtmlEnabled($col: JQuery): boolean { let column = $col.data('column') as Column<any>; return column.headerTooltipHtmlEnabled; } setHeaderMenusEnabled(headerMenusEnabled: boolean) { this.setProperty('headerMenusEnabled', headerMenusEnabled); } protected _renderHeaderMenusEnabled() { this._visibleColumns().forEach(column => this._decorateHeader(column)); } openHeaderMenu(column: Column<any>) { if (this.tableHeaderMenu) { // Make sure existing header menu is closed first this.closeHeaderMenu(); } this.tableHeaderMenu = column.createTableHeaderMenu(this); this.tableHeaderMenu.open(); // Trigger events on column to make it possible to react to the opening of the menu column.trigger('headerMenuOpen', { menu: this.tableHeaderMenu }); this.tableHeaderMenu.one('destroy', () => { column.trigger('headerMenuClose', { menu: this.tableHeaderMenu }); }); } closeHeaderMenu() { this.tableHeaderMenu.destroy(); this.tableHeaderMenu = null; } findHeaderItems(): JQuery { return this.$container.find('.table-header-item:not(.filler)'); } /** * Updates the column headers visualization of the text, sorting and styling state * @param oldColumnState only necessary when the css class was updated */ updateHeader(column: Column<any>, oldColumnState?: ColumnModel<any>) { if (!column.visible) { return; } this._decorateHeader(column, oldColumnState); } protected _decorateHeader(column: Column<any>, oldColumnState?: ColumnModel<any>) { this._renderColumnCssClass(column, oldColumnState); this._renderColumnText(column); this._renderColumnIconId(column); this._renderColumnState(column); this._renderColumnLegacyStyle(column); this._renderColumnHeaderMenuEnabled(column); this._renderColumnHorizontalAlignment(column); this._renderColumnTooltipText(column); } protected _renderColumnCssClass(column: Column<any>, oldColumnState?: ColumnModel<any>) { let $header = column.$header; if (oldColumnState) { $header.removeClass(oldColumnState.headerCssClass); } $header.addClass(column.headerCssClass); } protected _renderColumnText(column: Column<any>) { let text = column.text, $header = column.$header, $headerText = $header.children('.table-header-item-text'); if (!column.headerHtmlEnabled) { text = strings.nl2br(text); } // Make sure empty header is as height as the others to make it properly clickable $headerText.htmlOrNbsp(text, 'empty'); this._updateColumnIconAndTextStyle(column); } protected _renderColumnTooltipText(column: Column<any>) { // add tooltip as invisible text for screen readers if (column.$header && strings.hasText(column.headerTooltipText)) { let $descriptionElement = column.$header.appendDiv().addClass('text').text(column.headerTooltipText); aria.screenReaderOnly($descriptionElement); } } protected _renderColumnIconId(column: Column<any>) { column.$header.icon(column.headerIconId); this._updateColumnIconAndTextStyle(column); } protected _renderColumnHorizontalAlignment(column: Column<any>) { column.$header.removeClass('halign-left halign-center halign-right'); column.$header.addClass('halign-' + Table.parseHorizontalAlignment(column.horizontalAlignment)); } protected _updateColumnIconAndTextStyle(column: Column<any>) { let $icon = column.$header.data('$icon'), $text = column.$header.children('.table-header-item-text'); if ($icon) { $icon.toggleClass('with-text', !!column.text); } // Make text invisible if there is an icon but no text $text.setVisible(!($icon && $text.html() === '&nbsp;')); // Mark icon-only columns to prevent ellipsis (like IconColumn.js does for table cells) column.$header.toggleClass('table-header-item-icon-only', !!(column.headerIconId && !column.text)); } protected _renderColumnLegacyStyle(column: Column<any>) { styles.legacyStyle(column, column.$header, 'header'); } protected _renderColumnHeaderMenuEnabled(column: Column<any>) { column.$header.toggleClass('disabled', !this._isHeaderMenuEnabled(column) || !this.enabled); // enabledComputed not used on purpose } protected _renderColumnState(column: Column<any>) { let $header = column.$header; let filtered = column.filtered; $header.children('.table-header-item-state').remove(); let $state = $header.appendSpan('table-header-item-state'); $state.empty(); $header.removeClass('sort-asc sort-desc sorted group-asc group-desc grouped filtered'); $state.removeClass('sort-asc sort-desc sorted group-asc group-desc grouped filtered'); let accessibleStateText = ''; if (column.sortActive) { let sortDirection = column.sortAscending ? 'asc' : 'desc'; accessibleStateText += this.session.text('ui.Sorting') + ' ' + (column.sortAscending ? this.session.text('ui.ascending') : this.session.text('ui.descending')); if (column.grouped) { $header.addClass('group-' + sortDirection); } $header.addClass('sorted sort-' + sortDirection); $state.addClass('sorted sort-' + sortDirection); } if (column.grouped || filtered) { // contains group and filter symbols let $left = $state.appendDiv('left'); if (column.grouped) { $header.addClass('grouped'); $state.addClass('grouped'); let $g = $left.appendDiv().text('G'); aria.hidden($g, true); accessibleStateText += ' ' + this.session.text('ui.Grouping'); } if (filtered) { $header.addClass('filtered'); $state.addClass('filtered'); let $f = $left.appendDiv().text('F'); aria.hidden($f, true); accessibleStateText += ' ' + this.session.text('ui.Filter'); } } let $accessibleState = $state.appendDiv().addClass('text').text(accessibleStateText); aria.screenReaderOnly($accessibleState); // Contains sort arrow let sortArrow = $state.appendDiv('right'); aria.hidden(sortArrow, true); this._adjustColumnMinWidth(column); } /** * Makes sure state is fully visible by adjusting width (happens if column.minWidth is < DEFAULT_MIN_WIDTH) */ protected _adjustColumnMinWidth(column: Column<any> & { __minWidthWithoutState?: number; __widthWithoutState?: number }) { if (column.sortActive || column.grouped || column.filtered) { if (column.minWidth < Column.DEFAULT_MIN_WIDTH) { column.__minWidthWithoutState = column.minWidth; column.__widthWithoutState = column.width; column.minWidth = Column.DEFAULT_MIN_WIDTH; } if (column.width < column.minWidth) { this.table.resizeColumn(column, column.minWidth); } } else { // Reset to previous min width if no state is visible if (!objects.isNullOrUndefined(column.__minWidthWithoutState)) { column.minWidth = column.__minWidthWithoutState; // Resize to previous min width, assuming user has not manually changed the size because column is still as width as default_min_width if (column.width === Column.DEFAULT_MIN_WIDTH) { this.table.resizeColumn(column, column.__widthWithoutState); } column.__minWidthWithoutState = null; column.__widthWithoutState = null; } } } updateMenuBar() { let menuItems = this.table._filterMenus(this.table.menus, MenuDestinations.HEADER); this.menuBar.setHiddenByUi(!this.enabled); // enabledComputed not used on purpose this.menuBar.setMenuItems(menuItems); } protected _onTableColumnResized(event: TableColumnResizedEvent) { let column = event.column, lastColumn = this._lastVisibleColumn(); this.resizeHeaderItem(column); if (lastColumn !== column) { this.resizeHeaderItem(lastColumn); } } onSortingChanged() { this._visibleColumns().forEach(this._renderColumnState, this); } protected _onTableColumnMoved(event: TableColumnMovedEvent) { // store old position of header const $headers = this.findHeaderItems(); $headers.each(function() { $(this).data('old-left', $(this).offset().left); }); // change order in dom of header const oldPos = event.oldPos; const newPos = event.newPos; const $movedHeader = $headers.eq(oldPos); const $targetHeader = $headers.eq(newPos); // The separator always belongs to a column -> remember it before moving the header const $separator = $movedHeader.next('.table-header-resize'); if (newPos < oldPos) { $targetHeader.before($movedHeader); } else { $targetHeader.after($movedHeader); $movedHeader.before($movedHeader.next('.table-header-resize')); } $movedHeader.after($separator); // Update first/last markers if ($headers.length > 0) { $headers.eq(0).removeClass('first'); $headers.eq($headers.length - 1).removeClass('last'); } const visibleColumns = this._visibleColumns(); const lastColumnPos = visibleColumns.length - 1; if (visibleColumns.length > 0) { visibleColumns[0].$header.addClass('first'); visibleColumns[lastColumnPos].$header.addClass('last'); } // Update header size due to header menu items if moved from or to last position if (oldPos === lastColumnPos || newPos === lastColumnPos) { visibleColumns.forEach(column => this.resizeHeaderItem(column)); } // move to old position and then animate unless it was dragged and being released if (!this.dragging) { this._arrangeHeaderItems($([$movedHeader[0], $targetHeader[0]])); } } protected _visibleColumns(): Column<any>[] { return this.table.visibleColumns(); } protected _lastVisibleColumn(): Column<any> { return arrays.last(this._visibleColumns()); } onOrderChanged(oldColumnOrder: Column<any>[]) { let $headers = this.findHeaderItems(); // store old position of headers $headers.each(function() { $(this).data('old-left', $(this).offset().left); }); // change order in dom of header this._visibleColumns().forEach(column => { let $header = column.$header; let $headerResize = $header.next('.table-header-resize'); this.$container.append($header); this.$container.append($headerResize); }); // ensure filler is at the end this.$container.append(this.$filler); this._arrangeHeaderItems($headers); } /** * Header menus are enabled when property is enabled on the header itself and on the column too. */ protected _isHeaderMenuEnabled(column: Column<any>): boolean { return !!(column.headerMenuEnabled && this.headerMenusEnabled); } protected _onHeaderItemClick(event: JQuery.ClickEvent): boolean { let $headerItem = $(event.currentTarget), column = $headerItem.data('column') as Column<any>; if (this.dragging || this.columnMoved) { this.dragging = false; this.columnMoved = false; } else if (this.table.sortEnabled && (event.shiftKey || event.ctrlKey || !this._isHeaderMenuEnabled(column))) { this.table.sort(column, $headerItem.hasClass('sort-asc') ? 'desc' : 'asc', event.shiftKey); } else if (this.tableHeaderMenu && this.tableHeaderMenu.isOpenFor($headerItem)) { this.closeHeaderMenu(); } else if (this._isHeaderMenuEnabled(column)) { this.openHeaderMenu(column); } return false; } protected _onHeaderItemMouseDown(event: JQuery.MouseDownEvent) { if (event.button > 0) { return; // ignore buttons other than the main (left) mouse button } let diff = 0, that = this, startX = Math.floor(event.pageX), $header = $(event.currentTarget), column = $header.data('column') as Column<any>, oldPos = this._visibleColumns().indexOf(column), newPos = oldPos, move = $header.outerWidth(), $otherHeaders = $header.siblings('.table-header-item:not(.filler)'); if (column.fixedPosition) { // Don't allow moving a column with fixed position return; } this.dragging = false; // firefox fires a click action after a column has been dropped at the new location, chrome doesn't -> we need a hint to avoid menu gets opened after drop this.columnMoved = false; // start drag & drop events this._$window .on('mousemove.tableheader', '', dragMove) .one('mouseup', '', dragEnd); function dragMove(event: JQuery.MouseMoveEvent) { diff = Math.floor(event.pageX) - startX; if (-2 < diff && diff < 2) { // Don't move if it was no movement or just a very small one return; } that.dragging = true; // change css of dragged header $header.addClass('moving'); that.$container && that.$container.addClass('moving'); // move dragged header $header.css('left', diff); // find other affected headers let middle = realMiddle($header); $otherHeaders.each(function(i) { let $otherHeader = $(this); let m = realMiddle($otherHeader); if (middle < m && i < oldPos) { $otherHeader.css('left', move); } else if (middle > m && i >= oldPos) { $otherHeader.css('left', -move); } else { $otherHeader.css('left', 0); } }); if (that.tableHeaderMenu) { that.tableHeaderMenu.destroy(); that.tableHeaderMenu = null; } // Don't show tooltips while dragging that.rendered && that._uninstallHeaderItemTooltips(); } function realWidth($div: JQuery): number { let html = $div.html(), width = $div.html('<span>' + html + '</span>').find('span:first').width(); $div.html(html); return width; } /** * @returns the middle of the text (not the middle of the whole header item) */ function realMiddle($div: JQuery): number { if ($div.hasClass('halign-right')) { return $div.offset().left + $div.outerWidth() - realWidth($div) / 2; } return $div.offset().left + realWidth($div) / 2; } function dragEnd(event: JQuery.MouseUpEvent): boolean { that._$window && that._$window.off('mousemove.tableheader'); // in case of no movement: return if (!that.dragging) { return true; } // find new position of dragged header let h = (diff < 0) ? $otherHeaders : $($otherHeaders.get().reverse()); h.each(function(i) { if ($(this).css('left') !== '0px') { newPos = that._visibleColumns().indexOf(($(this).data('column'))); return false; } }); // move column const oldLeft = $header.offset().left; const moved = that.table.moveColumn($header.data('column'), newPos); if (moved) { const left = $header.cssLeft() + oldLeft - $header.offset().left; $header.css('left', left) .addClass('releasing') .animateAVCSD('left', 0, () => $header.removeClass('releasing')); that.dragging = false; that.columnMoved = true; } else { $header.addClass('releasing'); $header.animateAVCSD('left', '', () => { that.dragging = false; $header.removeClass('releasing'); }); } // reset css of dragged header $otherHeaders.each(function() { $(this).css('left', ''); }); $header.removeClass('moving'); that.$container && that.$container.removeClass('moving'); // Reinstall tooltips that.rendered && that._installHeaderItemTooltips(); } } protected _onSeparatorDblclick(event: JQuery.DoubleClickEvent) { if (event.button > 0) { return; // ignore buttons other than the main (left) mouse button } if (event.shiftKey) { // Optimize all columns this._visibleColumns().forEach(column => { this.table.resizeToFit(column); }); } else { // Optimize the column left of the separator let $header = $(event.target).prev(); let column = $header.data('column'); this.table.resizeToFit(column); } } protected _onSeparatorMouseDown(event: JQuery.MouseDownEvent) { if (event.button > 0) { return; // ignore buttons other than the main (left) mouse button } let startX = Math.floor(event.pageX), $header = $(event.target).prev(), column = $header.data('column') as Column<any>, that = this, headerWidth = column.width; // Install resize helpers. Those helpers make sure the header and the data element keep their // current width until the resizing has finished. Otherwise, make a column smaller while the // table has been horizontally scrolled to the right would behave very strange. let $headerColumnResizedHelper = this.$container .appendDiv('table-column-resize-helper') .css('width', this.table.rowWidth + this.table.rowBorders.horizontal()); let $dataColumnResizedHelper = this.table.$data .appendDiv('table-column-resize-helper') .css('width', this.table.rowWidth); this._$window .on('mousemove.tableheader', resizeMove) .one('mouseup', resizeEnd); this._$body.addClass('col-resize'); // Prevent text selection in a form, don't stop propagation to allow others (e.g. cell editor) to react event.preventDefault(); function resizeMove(event: JQuery.MouseMoveEvent) { let diff = Math.floor(event.pageX) - startX, wHeader = headerWidth + diff; wHeader = Math.max(wHeader, column.minWidth); if (that.rendered && wHeader !== column.width) { that.table.resizeColumn(column, wHeader); } } function resizeEnd(event: JQuery.MouseUpEvent) { // Remove resize helpers $headerColumnResizedHelper.remove(); $dataColumnResizedHelper.remove(); that._$window && that._$window.off('mousemove.tableheader'); that._$body && that._$body.removeClass('col-resize'); if (that.table.rendered && column.width !== headerWidth) { that.table.resizeColumn(column, column.width); } } } protected _onTableDataScroll() { scrollbars.fix(this.$menuBarContainer); this._reconcileScrollPos(); this._fixTimeout = scrollbars.unfix(this.$menuBarContainer, this._fixTimeout); } protected _onMenuBarPropertyChange(event: PropertyChangeEvent<any, MenuBar>) { if (this.rendered && event.propertyName === 'visible') { this.$menuBarContainer.setVisible(event.newValue); } } protected _onTableAddFilterRemoved(event: TableFilterAddedEvent | TableFilterRemovedEvent) { if (!(event.filter instanceof ColumnUserFilter)) { return; } let column = event.filter.column; // Check for column.$header because column may have been removed in the meantime due to a structure changed event -> don't try to render state if (event.filter.filterType === ColumnUserFilter.TYPE && column.$header) { this._renderColumnState(column); } } }