UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

600 lines (514 loc) 19.7 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ "./PluginBase", "sap/ui/core/Core", "sap/ui/core/Element", "sap/ui/core/InvisibleText", "sap/ui/Device", "sap/m/ColumnPopoverActionItem", "sap/m/table/columnmenu/QuickAction", "sap/m/Button", "sap/ui/thirdparty/jquery", "sap/ui/dom/jquery/Aria" // jQuery Plugin "aria" ], function(PluginBase, Core, Element, InvisibleText, Device, ColumnPopoverActionItem, QuickAction, Button, jQuery ) { "use strict"; /** * Constructor for a new ColumnResizer plugin. * * @param {string} [sId] ID for the new <code>ColumnResizer</code>, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the <code>ColumnResizer</code> * * @class * Enables column resizing for the <code>sap.m.Table</code>. * This plugin can be added to the control via its <code>dependents</code> aggregation * and there must only be 1 instance of the plugin per control. * * @extends sap.ui.core.Element * @author SAP SE * @version 1.117.4 * * @public * @since 1.91 * @alias sap.m.plugins.ColumnResizer */ var ColumnResizer = PluginBase.extend("sap.m.plugins.ColumnResizer", /** @lends sap.m.plugins.ColumnResizer.prototype */ { metadata: { library: "sap.m", properties: { }, events: { /** * This event is fired when the column is resized. */ columnResize: { allowPreventDefault : true, parameters: { /** * The column being resized. */ column: {type: "sap.ui.core.Element"}, /** * The new width of the column. */ width : {type : "sap.ui.core.CSSSize"} } } } }}); var oSession = {}; var bResizing = false; var CSS_CLASS = "sapMPluginsColumnResizer"; var bRTL = Core.getConfiguration().getRTL(); var sBeginDirection = bRTL ? "right" : "left"; var sEndDirection = bRTL ? "left" : "right"; var iDirectionFactor = bRTL ? -1 : 1; ColumnResizer.getPlugin = PluginBase.getPlugin; ColumnResizer.prototype.init = function() { this._iHoveredColumnIndex = -1; this._aPositions = []; this._oHandle = null; }; ColumnResizer.prototype.onActivate = function(oControl) { oControl.addEventDelegate(this, this); if (oControl.isActive()) { this.onAfterRendering(); } }; ColumnResizer.prototype.onDeactivate = function(oControl) { oControl.removeEventDelegate(this, this); this.onBeforeRendering(); this._oHandle = null; }; ColumnResizer.prototype.onBeforeRendering = function() { if (this._$Container) { this._$Container.removeClass(CSS_CLASS + "Container").off("." + CSS_CLASS); this._$Container.find(this.getConfig("resizable")).removeClass(CSS_CLASS + "Resizable"); this._updateAriaDescribedBy("remove"); } }; ColumnResizer.prototype.onAfterRendering = function() { this._$Container = this.getControl().$(this.getConfig("container")); Device.system.desktop && this._$Container.on("mousemove." + CSS_CLASS, this._onmousemove.bind(this)); this._$Container.addClass(CSS_CLASS + "Container").on("mouseleave." + CSS_CLASS, this._onmouseleave.bind(this)); this._aResizables = this._$Container.find(this.getConfig("resizable")).addClass(CSS_CLASS + "Resizable").get(); this._updateAriaDescribedBy("add"); this._invalidatePositions(); }; /** * Adds / removes the aria-describedby attribute from the resizable control DOM. * @param {string} sAction function prefix * @private */ ColumnResizer.prototype._updateAriaDescribedBy = function(sAction) { this._aResizables.forEach(function(oResizable) { var oResizableControl = Element.closestTo(oResizable, true); var oFocusDomRef = oResizableControl && oResizableControl.getFocusDomRef(); jQuery(oFocusDomRef)[sAction + "AriaDescribedBy"](InvisibleText.getStaticId("sap.m", "COLUMNRESIZER_RESIZABLE")); }); }; ColumnResizer.prototype.ontouchstart = function(oEvent) { if (this.getConfig("allowTouchResizing") && jQuery(oEvent.target).closest(this._aResizables)[0]) { this._onmousemove(oEvent); } else if (this._iHoveredColumnIndex == -1 && this._oHandle && this._oHandle.style[sBeginDirection]) { this._onmousemove(oEvent); if (this._iHoveredColumnIndex == -1) { this._oHandle.style[sBeginDirection] = ""; this._oAlternateHandle.style[sBeginDirection] = ""; } } bResizing = (this._iHoveredColumnIndex > -1); if (!bResizing) { return; } this._startResizeSession(this._iHoveredColumnIndex); oSession.iTouchStartX = oEvent.targetTouches[0].clientX; oSession.fHandleX = parseFloat(this._oHandle.style[sBeginDirection]); this._$Container.addClass(CSS_CLASS + "Resizing"); jQuery(document).on("touchend." + CSS_CLASS + " mouseup." + CSS_CLASS, this._ontouchend.bind(this)); }; ColumnResizer.prototype.ontouchmove = function(oEvent) { if (!bResizing) { return; } this._setSessionDistanceX((oEvent.targetTouches[0].clientX - oSession.iTouchStartX)); this._oHandle.style[sBeginDirection] = oSession.fHandleX + oSession.iDistanceX + "px"; }; ColumnResizer.prototype._onmousemove = function(oEvent) { if (bResizing || this.getControl().getBusy() || this.getControl().getBlocked()) { return; } this._setPositions(); var iClientX = oEvent.targetTouches ? oEvent.targetTouches[0].clientX : oEvent.clientX; var iHoveredColumnIndex = this._getHoveredColumnIndex(iClientX); this._displayHandle(iHoveredColumnIndex); }; ColumnResizer.prototype._onmouseleave = function() { this._invalidatePositions(); this.onsapescape(); }; ColumnResizer.prototype._ontouchend = function() { this._setColumnWidth(); this._cancelResizing(true); }; ColumnResizer.prototype.onsapescape = function() { if (bResizing) { this._cancelResizing(); } }; ColumnResizer.prototype.onsaprightmodifiers = function(oEvent) { this._onLeftRightModifiersKeyDown(oEvent, 16); }; ColumnResizer.prototype.onsapleftmodifiers = function(oEvent) { this._onLeftRightModifiersKeyDown(oEvent, -16); }; ColumnResizer.prototype.ondblclick = function(oEvent) { var iClientX = oEvent.clientX, iHoveredColumnIndex = this._getHoveredColumnIndex(iClientX); if (iHoveredColumnIndex == -1) { return; } this._startResizeSession(iHoveredColumnIndex); this._setSessionDistanceX(this._calculateAutoColumnDistanceX()); this._setColumnWidth(); this._endResizeSession(); }; /** * Returns the hovered column index. If column index is found the returns the index else returns -1. * @param {int} iClientX clientX from the mouse/touch event * @returns {int} hovered column index * @private */ ColumnResizer.prototype._getHoveredColumnIndex = function(iClientX) { return this._aPositions.findIndex(function(fPosition) { return Math.abs(fPosition - iClientX) <= (this._oAlternateHandle || ColumnResizer._isInTouchMode() ? 20 : 3); }, this); }; /** * Returns the horizontal distance by which a column's width should be increased or decreased. * This gets called when columns must be automatically resized on the double click mouse. * @returns {int} horizontal distance * @private */ ColumnResizer.prototype._calculateAutoColumnDistanceX = function() { var $Cells = this.getConfig("columnRelatedCells", this._$Container, oSession.oCurrentColumn.getId()); if (!$Cells || !$Cells.length) { return; } var $HiddenArea = jQuery("<div></div>").addClass(CSS_CLASS + "SizeDetector").addClass(this.getConfig("cellPaddingStyleClass")); var $ClonedCells = $Cells.children().clone().removeAttr("id"); this.getConfig("additionalColumnWidth", $Cells, $ClonedCells); this._$Container.append($HiddenArea); var iWidth = Math.round($HiddenArea.append($ClonedCells)[0].getBoundingClientRect().width); var iDistanceX = iWidth - oSession.fCurrentColumnWidth; $HiddenArea.remove(); return iDistanceX; }; ColumnResizer.prototype._invalidatePositions = function() { window.setTimeout(function() { this._bPositionsInvalid = true; }.bind(this)); }; /** * Displays the resize handle on the column which is hovered * @param {int} iColumnIndex column index * @param {boolean} bMobileHandle indicates whether the alternate handle is visible * @private */ ColumnResizer.prototype._displayHandle = function(iColumnIndex, bMobileHandle) { if (this._iHoveredColumnIndex == iColumnIndex) { return; } if (!this._oHandle) { this._oHandle = document.createElement("div"); this._oHandle.className = CSS_CLASS + "Handle"; this._oHandle.onmouseleave = function() { this.style[sBeginDirection] = ""; }; if (bMobileHandle || ColumnResizer._isInTouchMode()) { var oCircle = document.createElement("div"); oCircle.className = CSS_CLASS + "HandleCircle"; oCircle.style.top = this._aResizables[iColumnIndex].offsetHeight - 8 + "px"; this._oHandle.appendChild(oCircle); this._oAlternateHandle = this._oHandle.cloneNode(true); } } if (this._$Container[0] !== this._oHandle.parentNode) { this._$Container.append(this._oHandle); if (bMobileHandle) { this._$Container.append(this._oAlternateHandle); } } this._oHandle.style[sBeginDirection] = (iColumnIndex > -1) ? (this._aPositions[iColumnIndex] - this._fContainerX) * iDirectionFactor + "px" : ""; if (bMobileHandle) { this._oAlternateHandle.style[sBeginDirection] = (--iColumnIndex > -1) ? (this._aPositions[iColumnIndex] - this._fContainerX) * iDirectionFactor + "px" : ""; } else { if (this._oAlternateHandle) { this._oAlternateHandle.style[sBeginDirection] = ""; } this._iHoveredColumnIndex = iColumnIndex; } }; /** * Cancels the resizing session and restores the DOM. * @param {boolean} bDelayHideHandle whether there should be a delay in hiding the resize handle * @private */ ColumnResizer.prototype._cancelResizing = function(bDelayHideHandle) { this._$Container.removeClass(CSS_CLASS + "Resizing"); if (oSession.iDistanceX || !bDelayHideHandle) { this._oHandle.style[sBeginDirection] = ""; } else { // delay hiding the handle so that in case of double-click mouse event, // the resize handle does not disappear in the initial mousedown and mouseup event // this will also prevent column press event to trigger setTimeout(function() { this._oHandle.style[sBeginDirection] = ""; }.bind(this), 300); } this._iHoveredColumnIndex = -1; jQuery(document).off("." + CSS_CLASS); this._endResizeSession(); bResizing = false; }; ColumnResizer.prototype._getColumnMinWidth = function(oColumn) { return oColumn ? 48 : 0; }; /** * This function collects all the necessary information for starting a resize session. * A resize session contains the below information: * - Current column and its width. * - Next column and its width (if available). * - Maximum increase and decrease resize value. * - Existance of dummy column. * @param {int} iIndex column index * @private */ ColumnResizer.prototype._startResizeSession = function(iIndex) { oSession.$CurrentColumn = jQuery(this._aResizables[iIndex]); oSession.oCurrentColumn = Element.closestTo(oSession.$CurrentColumn[0], true); oSession.fCurrentColumnWidth = oSession.$CurrentColumn.width(); oSession.iMaxDecrease = this._getColumnMinWidth(oSession.oCurrentColumn) - oSession.fCurrentColumnWidth; oSession.iEmptySpace = this.getConfig("emptySpace", this.getControl()); if (oSession.iEmptySpace != -1) { oSession.$NextColumn = jQuery(this._aResizables[iIndex + 1]); oSession.oNextColumn = Element.closestTo(oSession.$NextColumn[0], true); oSession.fNextColumnWidth = oSession.$NextColumn.width() || 0; oSession.iMaxIncrease = oSession.iEmptySpace + oSession.fNextColumnWidth - this._getColumnMinWidth(oSession.oNextColumn); } else { oSession.iMaxIncrease = window.innerWidth; } }; /** * Sets the horizontal resize distance to the session by which the column was increased or decreased. * @param {int} iDistanceX horizontal resize distance * @private */ ColumnResizer.prototype._setSessionDistanceX = function(iDistanceX) { oSession.iDistanceX = ((iDistanceX > 0) ? Math.min(iDistanceX, oSession.iMaxIncrease) : Math.max(iDistanceX, oSession.iMaxDecrease)) * iDirectionFactor; }; /** * Sets the column widths if the default action of the <code>columnResize</code> event is not prevented. * @private */ ColumnResizer.prototype._setColumnWidth = function() { if (!oSession.iDistanceX) { return; } var sWidth = oSession.fCurrentColumnWidth + oSession.iDistanceX + "px"; if (!this._fireColumnResize(oSession.oCurrentColumn, sWidth)) { return; } oSession.oCurrentColumn.setWidth(sWidth); if (oSession.oNextColumn && (oSession.iEmptySpace < 3 || oSession.iDistanceX > oSession.iEmptySpace)) { sWidth = oSession.fNextColumnWidth - oSession.iDistanceX + oSession.iEmptySpace + "px"; if (this._fireColumnResize(oSession.oNextColumn, sWidth)) { oSession.oNextColumn.setWidth(sWidth); } } // when any column is resized, then make all visible columns have fixed width this.getConfig("fixAutoWidthColumns") && this._aResizables.forEach(function(oResizable) { var $Resizable = jQuery(oResizable), oColumn = Element.closestTo(oResizable, true), sWidth = oColumn.getWidth(); if (sWidth && sWidth.toLowerCase() != "auto") { return; } sWidth = $Resizable.css("width"); if (sWidth && this._fireColumnResize(oColumn, sWidth)) { oColumn.setWidth(sWidth); } }, this); }; /** * Fires the column resize event with the relevant parameters. * @param {sap.m.Column|sap.ui.table.Column} oColumn column instance * @param {sap.ui.core.CSSSize} sWidth column width * @private * @return {boolean} prevent defualt */ ColumnResizer.prototype._fireColumnResize = function(oColumn, sWidth) { return this.fireColumnResize({ column: oColumn, width: sWidth }); }; /** * This function is called when column resizing is trigger via keyboard events <code>onsapleftmodifiers</code> & <code>onsaprightmodifiers</code>. * @param {object} oEvent keyboard event * @param {int} iDistanceX resize distance * @private */ ColumnResizer.prototype._onLeftRightModifiersKeyDown = function(oEvent, iDistanceX) { // prevent column resize when there is text selection in the column header if (!oEvent.shiftKey || oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey || ColumnResizer.detectTextSelection(oEvent.target)) { return; } var oResizable = jQuery(oEvent.target).closest(this._aResizables)[0], iIndex = this._aResizables.indexOf(oResizable); if (iIndex === -1) { return; } this._startResizeSession(iIndex); this._setSessionDistanceX(iDistanceX); this._setColumnWidth(); this._endResizeSession(); }; ColumnResizer.detectTextSelection = function(oDomRef) { var oSelection = window.getSelection(), sTextSelection = oSelection.toString().replace("/n", ""); return sTextSelection && (oDomRef !== oSelection.focusNode && oDomRef.contains(oSelection.focusNode)); }; /** * Ends and cleans up the resize session. * @private */ ColumnResizer.prototype._endResizeSession = function() { oSession = {}; }; /** * Sets the container and handle positions. If positions are invalid then calculates first. * @returns {Array} hoverable positions * @private */ ColumnResizer.prototype._setPositions = function() { if (!this._bPositionsInvalid) { return this._aPositions; } this._bPositionsInvalid = false; this._fContainerX = this._$Container[0].getBoundingClientRect()[sBeginDirection]; this._aPositions = this._aResizables.map(function(oResizable, iIndex, aPositions) { return oResizable.getBoundingClientRect()[sEndDirection] - (++iIndex == aPositions.length ? 1.25 * iDirectionFactor : 0); }, this); }; /** * Displays the resize handle for the provided column <code>DOM</code> reference. * @param {HTMLElement} oDomRef column DOM reference * @protected */ ColumnResizer.prototype.startResizing = function(oDomRef) { var iColumnIndex = this._aResizables.indexOf(oDomRef); this._setPositions(); this._displayHandle(iColumnIndex, true); }; /** * Returns resizer quick action instance which on press calls the <code>startResizing</code> method. * @param {sap.m.Column} oColumn Column instance * @param {sap.m.table.columnmenu.Menu} oColumnMenu The column menu instance * @returns {sap.m.table.columnmenu.QuickAction | undefined} column resize quick action * @ui5-restricted * @private */ ColumnResizer.prototype.getColumnResizeQuickAction = function(oColumn, oColumnMenu) { if (!oColumn || !ColumnResizer._isInTouchMode()) { return; } return new QuickAction({ content: new Button({ text: Core.getLibraryResourceBundle("sap.m").getText("table.COLUMNMENU_RESIZE"), press: function() { oColumnMenu.close(); this.startResizing(oColumn.getDomRef()); }.bind(this) }) }); }; /** * Returns resizer button instance which on press calls the <code>startResizing</code> method. * @param {sap.m.Column} oColumn Column instance * @returns {sap.m.ColumnPopoverActionItem | undefined} column resize action item * @ui5-restricted * @private */ ColumnResizer.prototype.getColumnResizeButton = function(oColumn) { if (!oColumn || !ColumnResizer._isInTouchMode()) { return; } return new ColumnPopoverActionItem({ text: Core.getLibraryResourceBundle("sap.m").getText("COLUMNRESIZER_RESIZE_BUTTON"), icon: "sap-icon://resize-horizontal", press: this.startResizing.bind(this, oColumn.getDomRef()) }); }; ColumnResizer._isInTouchMode = function() { return window.matchMedia("(hover:none)").matches; }; /** * Plugin-specific control configurations. */ PluginBase.setConfigs({ "sap.m.Table": { container: "listUl", resizable: ".sapMListTblHeaderCell:not([aria-hidden=true])", focusable: ".sapMColumnHeaderFocusable", cellPaddingStyleClass: "sapMListTblCell", fixAutoWidthColumns: true, onActivate: function(oTable) { this._vOrigFixedLayout = oTable.getFixedLayout(); if (!oTable.bActiveHeaders) { oTable.bFocusableHeaders = true; this.allowTouchResizing = ColumnResizer._isInTouchMode(); } oTable.setFixedLayout("Strict"); }, onDeactivate: function(oTable) { oTable.bFocusableHeaders = false; oTable.setFixedLayout(this._vOrigFixedLayout); // rerendering of the table is required if _vOrigFixedLayout == "Strict", since the focusable DOM must be removed if (this._vOrigFixedLayout == "Strict") { oTable.invalidate(); } delete this._vOrigFixedLayout; delete this.allowTouchResizing; }, emptySpace: function(oTable) { var oDummyCell = oTable.getDomRef("tblHeadDummyCell"); return oDummyCell ? oDummyCell.clientWidth : 0; }, columnRelatedCells: function($oContainer, sColumnId) { return $oContainer.find(".sapMListTblCell[data-sap-ui-column='" + sColumnId + "']"); }, additionalColumnWidth: function($Cells, $ClonedCellsChildren) { // first element in the $Cells is the <th> element var oTH = $Cells[0]; if (!oTH.hasAttribute("aria-sort") || oTH.getAttribute("aria-sort") === 'none') { return; } var oColumnHeaderDIV = $ClonedCellsChildren[0]; var oColumnComputedStyle = window.getComputedStyle(oTH.firstChild, ":after"); // add margin-left to the cloned column header <div> which is the width of the pseudo element containing the sort-indicator oColumnHeaderDIV.style.marginLeft = Math.round(parseInt(oColumnComputedStyle.getPropertyValue("width"))) + "px"; } } }, ColumnResizer); return ColumnResizer; });