UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,369 lines (1,204 loc) 63.4 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ "./PluginBase", "sap/base/i18n/Localization", "sap/base/util/deepEqual", "sap/ui/events/KeyCodes", "sap/ui/core/Element", "sap/m/library", "sap/ui/core/Lib", "sap/ui/core/InvisibleRenderer" ], function (PluginBase, Localization, deepEqual, KeyCodes, Element, library, Lib, InvisibleRenderer) { "use strict"; const ListMode = library.ListMode; const DELAY_SHORT = 250; //TBD: Are 2 different delays necessary? const DELAY_LONG = DELAY_SHORT * 2; const DIRECTION = { ROW: "row", COL: "col" }; const CellType = { /** * Data cells that can be selected. */ Cell: "Cell", /** * Cells that require special handling or look different. */ Other: "Other", /** * Cells that can be ignored from selection, but should be respected to not interrupt selection. */ Ignore: "Ignore" }; /** * Constructor for a new CellSelector plugin. * * @param {string} [sId] ID for the new <code>CellSelector</code>, generated automatically if no id is given * @param {object} [mSettings] Initial settings for the new <code>CellSelector</code> * * @class * The <code>CellSelector</code> plugin enables cell selection inside the table when it is added as a dependent to the control. * It allows the user to individually select a cell block. * * Currently, the <code>CellSelector</code> plugin does not offer touch support. * * The <code>CellSelector</code> plugin can be used with the {@link sap.ui.table.Table} and {@link sap.m.Table} unless the following applies: * <ul> * <li>Drag for rows is active</li> * <li>If used in combination with {@link sap.ui.table.Table#cellClick} or {@link sap.m.Table#itemPress}</li> * <li>If the <code>sap.m.ListType.SingleSelectMaster</code> mode is used in the <code>sap.m.Table</code></li> * </ul> * * When the <code>CellSelector</code> is used in combination with the {@link sap.ui.mdc.Table}, modifying the following settings on the {@link sap.ui.mdc.Table} may lead to problems: * <ul> * <li>attaching a {@link sap.ui.mdc.Table#rowPress rowPress} event to the table after initialization of table and plugin</li> * <li>changing {@link sap.ui.mdc.Table#getSelectionMode selectionMode} to something else than <code>Multi</code></li> * </ul> * * @extends sap.ui.core.Element * @version 1.146.0 * @author SAP SE * * @public * @since 1.119 * @alias sap.m.plugins.CellSelector * @borrows sap.m.plugins.PluginBase.findOn as findOn */ var CellSelector = PluginBase.extend("sap.m.plugins.CellSelector", /** @lends sap.m.plugins.CellSelector.prototype */ { metadata: { library: "sap.m", properties: { /** * Defines the number of row contexts for the {@link sap.ui.table.Table} control that need to be retrieved from the binding * when the range selection (e.g. enhancing the cell selection block to cover all rows of a column) is triggered by the user. * This helps to make the contexts already available for the user actions after the cell selection (e.g. copy to clipboard). * This property accepts positive integer values. * <b>Note:</b> To avoid performance problems, the <code>rangeLimit</code> should only be set higher than the default value of 200 in the following cases: * <ul> * <li>With client-side models</li> * <li>With server-side models if they are used in client mode</li> * <li>If the entity set is small</li> * </ul> * In other cases, it is recommended to set the <code>rangeLimit</code> to at least double the value of the {@link sap.ui.table.Table#getThreshold threshold} property. */ rangeLimit: {type: "int", group: "Behavior", defaultValue: 200}, /** * Indicates whether this plugin is active or not. */ enabled: {type: "boolean", defaultValue: true} }, events: { /** * Fired when the selection changes * @since 1.130 */ selectionChange: {} } } }); CellSelector.findOn = PluginBase.findOn; /** * A selection object representing the selected cells. * * The selection object contains the selected cells separated into rows and columns. * Rows are represented by their context, while columns are the column instance, which may vary depending on the table type. * @public * @typedef {object} sap.m.plugins.CellSelector.Selection * @property {sap.ui.model.Context[]} rows The row contexts of the selected cells. * @property {sap.ui.core.Element[]} columns The column instances of the selected cells; the content is based on the owner control. */ /** * An object representing the position of a cell. * * Consists of a row index and a column index describing the position of the cell in the table. * @private * @typedef {object} sap.m.plugins.CellSelector.CellPosition * @property {number} rowIndex Row index of the cell * @property {number} colIndex Column index of the cell */ /** * Delegate containing events that are fired after control events. */ const EventDelegate = { onkeydown: function(oEvent) { if (!this._bSelecting) { return; } if (isKeyCombination(oEvent, KeyCodes.A, true, true) || (isKeyCombination(oEvent, KeyCodes.A, false, true) && oEvent.isMarked(this.getConfig("eventClearedAll")))) { if (isSelectableCell(oEvent.target, this.getConfig("selectableCells"))) { this.removeSelection(); oEvent.preventDefault(); } } else if (isKeyCombination(oEvent, KeyCodes.SPACE, true, false) || isKeyCombination(oEvent, KeyCodes.SPACE, false, true)) { // prevent scrolling by pressing space oEvent.preventDefault(); } } }; /** * Delegate containing events that are fired before control events. */ const PriorityDelegate = { onBeforeRendering: function() { this._iBtt = this.getConfig("isBottomToTop", this.getControl()) ? -1 : 1; if (this._oResizer) { // Remove resizer, as due to rerendering table element may be gone this._oResizer.remove(); this._oResizer = null; } }, onAfterRendering: function() { this._deregisterEvents(); this._registerEvents(); this._bSelecting && !this._bMouseDown && this.removeSelection(); this._bSelecting && this._selectCells(); this._bRenderResizer = this.getConfig("shouldRenderResizer", this.getControl()); }, onsapupmodifiers: function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.ROW, -1, 0); }, onsapdownmodifiers: function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.ROW, 1, 0); }, onsapspace: function(oEvent) { if (isSelectableCell(oEvent.target, this.getConfig("selectableCells"))) { oEvent.preventDefault(); // Prevent default, otherwise m.Table will scroll } }, onsapspacemodifiers: function(oEvent) { if (this.getConfig("preventScrollOnShiftSpace", this.getControl())) { oEvent.preventDefault(); } }, onsapleftmodifiers: function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.COL, 0, -1); }, onsaprightmodifiers: function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.COL, 0, 1); }, onsapescape: function(oEvent) { if (oEvent.isMarked()) { return; } if (this._bSelecting && isSelectableCell(oEvent.target, this.getConfig("selectableCells"))) { this.removeSelection(); oEvent.preventDefault(); oEvent.stopPropagation(); } }, onkeyup: function(oEvent) { if (oEvent.isMarked() || !this.getConfig("isSupported", this.getControl(), this)) { return; } /* Handling CTRL + SPACE for Column Selection. Will be handled/implemented in a separate BLI if (isKeyCombination(oEvent, KeyCodes.SPACE, false, true) && this._getSelectableCell(oEvent.target)) { if (!this._inSelection(oEvent.target)) { // If focus is on cell outside of selection, select focused column this._oSession.mSource = this._oSession.mTarget = this.getConfig("getCellInfo", this.getControl(), oEvent.target, this._oPreviousCell); } this._oSession.mSource = Object.assign({}, this._oSession.mSource, { rowIndex: 0 }); this._oSession.mTarget = Object.assign({}, this._oSession.mTarget, { rowIndex: Infinity }); const mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget, true); this._bSelecting = true; this._selectCells(mBounds.from, mBounds.to); oEvent.preventDefault(); } */ if (isKeyCombination(oEvent, KeyCodes.SPACE, true, false)) { const mBounds = this._bSelecting ? this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget) : {}; const oInfo = this.getConfig("getCellInfo", this.getControl(), oEvent.target, this._oPreviousCell); if (!this._inSelection(oEvent.target)) { mBounds.from = mBounds.to = {}; mBounds.from.rowIndex = mBounds.to.rowIndex = oInfo.rowIndex; } this.getConfig("selectRows", this.getControl(), mBounds.from.rowIndex, mBounds.to.rowIndex, oInfo.rowIndex); oEvent.setMarked(); oEvent.preventDefault(); } else if (isKeyCombination(oEvent, KeyCodes.SPACE, false, false)) { if (!isSelectableCell(oEvent.target, this.getConfig("selectableCells"))) { return; } this._oPreviousCell = null; this._startSelection(oEvent, false); oEvent.setMarked(); } }, onmousedown: function(oEvent) { if (oEvent.isMarked?.() || oEvent.button != 0 || !this.getConfig("isSupported", this.getControl(), this)) { return; } const oSelectableCell = this._getSelectableCell(oEvent.target); if (!oSelectableCell) { return; } const oInfo = this.getConfig("getCellInfo", this.getControl(), oSelectableCell, this._oPreviousCell); this._bMouseDown = true; this._oMouseSource = Element.closestTo(oEvent.target); if (oEvent.shiftKey) { if (this._oPreviousCell?.rowIndex !== oInfo.rowIndex || this._oPreviousCell?.colIndex !== oInfo.colIndex) { window.getSelection().removeAllRanges(); if (this._oOriginCell) { this._selectCells(this._oOriginCell, oInfo); oEvent.preventDefault(); oEvent.setMarked?.(); } } } this._mClickedCell = this._oPreviousCell = oInfo; if (oEvent.ctrlKey || oEvent.metaKey) { this._startSelection(oEvent); if (this._mClickedCell) { this.getConfig("focusCell", this.getControl(), this._mClickedCell); } } }, onmouseup: function(oEvent) { clearTimeout(this._iTimer); this._bMouseDown = false; this._bBorderDown = false; this._oMouseSource = null; this._mClickedCell = undefined; this._bScrolling = false; this._mTempCell = undefined; this._oHoveredCell = undefined; this._endSelection(oEvent); this._clearScroller(); setTimeout(() => { this._startTarget = null; }, 0); }, onclick: function(oEvent) { const oTarget = this._getSelectableCell(oEvent.target); const oElement = Element.closestTo(oEvent.target); if (oTarget && this._startTarget === oTarget && !oElement.isA("sap.m.Link")) { oEvent.stopPropagation(); } } }; function getRTL() { return Localization.getRTL() ? -1 : 1; } CellSelector.prototype.init = function() { this._iRtl = getRTL(); }; CellSelector.prototype.onLocalizationChanged = function() { this._iRtl = getRTL(); this._iBtt = this.getConfig("isBottomToTop", this.getControl()) ? -1 : 1; this.removeSelection(); }; /** * @inheritDoc */ CellSelector.prototype.onActivate = function (oControl) { oControl.addDelegate(PriorityDelegate, true, this); oControl.addDelegate(EventDelegate, false, this); this._oSession = { cellRefs: [], cellTypes: [] }; this._iBtt = this.getConfig("isBottomToTop", this.getControl()) ? -1 : 1; this._mTimeouts = {}; this._fnControlUpdate = function(oEvent) { if (this._bScrolling) { this._scrollSelect(this._oSession.scrollForward, this._oSession.isVertical, oEvent); } else { if (!this._oSession.mSource || !this._oSession.mTarget) { return; } this._drawSelection(this._oSession.mSource, this._oSession.mTarget); } }.bind(this); this._fnOnMouseEnter = this._onmouseenter.bind(this); this._fnOnMouseOut = this._onmouseout.bind(this); this._fnOnMouseMove = this._onmousemove.bind(this); this._fnOnMouseUp = PriorityDelegate.onmouseup.bind(this); this._fnOnClick = PriorityDelegate.onclick.bind(this); this._fnRemoveSelection = this.removeSelection.bind(this); // Register Events as adding dependent does not trigger rerendering this._registerEvents(); this._onSelectableChange(); }; /** * @inheritDoc */ CellSelector.prototype.onDeactivate = function (oControl) { oControl.removeDelegate(PriorityDelegate, this); oControl.removeDelegate(EventDelegate, this); if (this._oSession) { this.removeSelection(); this._oSession = null; } this._mTimeouts = null; this._deregisterEvents(); this._onSelectableChange(); }; /** * Determines whether cells are selectable. * * @private * @returns {boolean} Whether cells are selectable * @ui5-restricted sap.m.plugins.CopyProvider */ CellSelector.prototype.isSelectable = function() { return this.isActive() ? this.getConfig("isSupported", this.getControl(), this) : false; }; /** * Determines whether there is a cell selection. * * @public * @returns {boolean} Whether there is a cell selection */ CellSelector.prototype.hasSelection = function() { return Boolean(this._bSelecting && this._oSession?.mSource); }; CellSelector.prototype._onSelectableChange = function() { this.getPlugin("sap.m.plugins.CopyProvider")?.onCellSelectorSelectableChange(this); }; CellSelector.prototype._onSelectionChange = function() { this.fireSelectionChange(); }; CellSelector.prototype._registerEvents = function() { var oControl = this.getControl(); if (oControl) { this.getConfig("scrollEvent") && oControl.attachEvent(this.getConfig("scrollEvent"), this._fnControlUpdate); this.getConfig("attachSelectionChange", oControl, this._fnRemoveSelection); this.getConfig("attachBindingUpdate", oControl, this); var oScrollArea = oControl.getDomRef(this.getConfig("scrollArea")); if (oScrollArea) { oScrollArea.addEventListener("mouseleave", this._fnOnMouseOut); oScrollArea.addEventListener("mouseenter", this._fnOnMouseEnter); oScrollArea.addEventListener("mousemove", this._fnOnMouseMove); oScrollArea.addEventListener("click", this._fnOnClick); } } document.addEventListener("mouseup", this._fnOnMouseUp); }; CellSelector.prototype._deregisterEvents = function() { var oControl = this.getControl(); if (oControl) { this.getConfig("scrollEvent") && oControl.detachEvent(this.getConfig("scrollEvent"), this._fnControlUpdate); this.getConfig("detachSelectionChange", oControl, this._fnRemoveSelection); this.getConfig("detachBindingUpdate", oControl, this._fnOnBindingUpdate); var oScrollArea = oControl.getDomRef(this.getConfig("scrollArea")); if (oScrollArea) { oScrollArea.removeEventListener("mouseleave", this._fnOnMouseOut); oScrollArea.removeEventListener("mouseenter", this._fnOnMouseEnter); oScrollArea.removeEventListener("mousemove", this._fnOnMouseMove); oScrollArea.removeEventListener("click", this._fnOnClick); } } document.removeEventListener("mouseup", this._fnOnMouseUp); }; /** * Returns the cell selection range. * The value <code>Infinity</code> in <code>rowIndex</code> indicates that the limit is reached. * * @param {boolean} bIgnore Ignore group header rows within selection range * @returns {object} {{from: {rowIndex: int, colIndex: int}, to: {rowIndex: int, colIndex: int}} The selection range * @ui5-restricted sap.m.plugins.CopyProvider * @private */ CellSelector.prototype.getSelectionRange = function (bIgnore) { if (!this._bSelecting) { return null; } var mSelectionRange = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); if (isNaN(mSelectionRange.from.rowIndex) || isNaN(mSelectionRange.to.rowIndex)) { return null; } var iMaxColumnIndex = this.getConfig("numberOfColumns", this.getControl(), true) - 1; mSelectionRange.from.colIndex = Math.max(mSelectionRange.from.colIndex, 0); mSelectionRange.to.colIndex = this._oSession.cellTypes.includes(CellType.Other) ? iMaxColumnIndex : Math.min(mSelectionRange.to.colIndex, iMaxColumnIndex); mSelectionRange.from.rowIndex = Math.max(mSelectionRange.from.rowIndex, 0); if (bIgnore) { mSelectionRange.ignoredRows = []; const aContexts = this.getSelectedRowContexts(); aContexts.forEach((oContext, iIndex) => { const iRowIndex = mSelectionRange.from.rowIndex + iIndex; if (isGroupRow(this._getBinding(), oContext, iRowIndex)) { mSelectionRange.ignoredRows.push(iRowIndex); } }); } delete mSelectionRange.from.type; delete mSelectionRange.to.type; return mSelectionRange; }; /** * Returns the row binding context of the current selection. * * @returns {sap.ui.model.Context[]} The binding context of selected rows * @private * @ui5-restricted sap.m.plugins.CopyProvider */ CellSelector.prototype.getSelectedRowContexts = function () { var mSelectionRange = this.getSelectionRange(); if (!mSelectionRange) { return []; } return this.getConfig("getSelectedRowContexts", this.getControl(), mSelectionRange.from.rowIndex, mSelectionRange.to.rowIndex, this.getRangeLimit()); }; /** * Returns the selected cells separated into selected rows and columns. * * Example: * If the cells from (0, 0) to (2, 4) are selected, this method will return the following object: * <pre> * { * rows: [Row0_Context, Row1_Context, Row2_Context], * columns: [Column0, Column1, Column2, Column3, Column4] * } * </pre> * * <b>Note:</b> The content of the <code>rows</code> and <code>columns</code> depends on the owner control. * The type of the column that is returned depends on the table type for which the plugin is used (for example, <code>sap.ui.table.Column</code> for <code>sap.ui.table.Table</code>). * * @param {boolean} bIgnore Ignores group headers from selection * @returns {sap.m.plugins.CellSelector.Selection} An object containing the selected cells separated into rows and columns * @public * @since 1.124 */ CellSelector.prototype.getSelection = function(bIgnore) { var mSelectionRange = this.getSelectionRange(); if (!mSelectionRange) { return {rows: [], columns: []}; } var aSelection = this.getConfig("getSelectedRowContexts", this.getControl(), mSelectionRange.from.rowIndex, mSelectionRange.to.rowIndex, this.getRangeLimit()); if (bIgnore) { aSelection = aSelection.filter((oContext, iIndex) => !isGroupRow(this._getBinding(), oContext, iIndex + mSelectionRange.from.rowIndex)); } var aSelectedColumns = this.getConfig("getVisibleColumns", this.getControl(), true).slice(mSelectionRange.from.colIndex, mSelectionRange.to.colIndex + 1); if (this.getControl().getParent().isA("sap.ui.mdc.Table")) { aSelectedColumns = aSelectedColumns.map(function(oSelectedColumn) { return Element.getElementById(oSelectedColumn.getId().replace(/\-innerColumn$/, "")); }); } return { rows: aSelection, columns: aSelectedColumns }; }; /** * Remove the current selection block. * * @public */ CellSelector.prototype.removeSelection = function () { this._clearSelection(); const bSelectionChange = this._oSession?.mSource || this._oSession?.mTarget; this._bSelecting = false; this._mClickedCell = this._oPreviousCell = this._oHoveredCell = this._oOriginCell = null; this._oSession = { cellRefs: [], cellTypes: [] }; if (bSelectionChange) { this._onSelectionChange(); } }; CellSelector.prototype._onsaparrowmodifiers = function(oEvent, sDirectionType, iRowDiff, iColDiff) { if (!this._shouldBeHandled(oEvent) || !oEvent.shiftKey || !this._getSelectableCell(oEvent.target) || oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) { this._oPreviousCell = undefined; return; } var oSelectableCell = this._getSelectableCell(oEvent.target); if (!oSelectableCell) { return; } var oInfo = this.getConfig("getCellInfo", this.getControl(), oSelectableCell, this._oPreviousCell); if (oInfo.rowIndex < 0 || oInfo.colIndex < 0) { return; } if (!this._inSelection(oEvent.target) || !this._oSession.mSource || !this._oSession.mTarget) { if (this.getConfig("isRowSelected", this.getControl(), oInfo.rowIndex)) { return; } // If not in selection block, start new selection block this._oSession.mSource = this._oSession.mTarget = oInfo; this._oPreviousCell = null; } if (oInfo.type == CellType.Ignore) { if (sDirectionType == DIRECTION.COL || !this._oPreviousCell) { // Do not modify/select if on a header/group header row and navigating in column direction (as their is technically only one) return; } oInfo.colIndex = this._oPreviousCell.colIndex; } this._oPreviousCell = oInfo; var mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); const { from, to, focus } = this._getUpdatedBounds(iRowDiff * this._iBtt, iColDiff * this._iRtl, oInfo); if (focus[sDirectionType + "Index"] < 0 || focus.colIndex >= this.getConfig("numberOfColumns", this.getControl())) { return; } this.getConfig("focusCell", this.getControl(), focus, true, iRowDiff > 0); if (sDirectionType == DIRECTION.ROW && (oInfo.rowIndex == mBounds.from.rowIndex || oInfo.rowIndex == mBounds.to.rowIndex) || sDirectionType == DIRECTION.COL && (oInfo.colIndex == mBounds.from.colIndex || oInfo.colIndex == mBounds.to.colIndex)) { this._bSelecting = true; this._selectCells(from, to); } oEvent.setMarked(); oEvent.preventDefault(); oEvent.stopPropagation(); }; /** * Event handler for <code>mousemove</code>. Handles <code>mousemove</code> event during cell selection. Takes on tasks like: * - updating resizer positions * - mouse selection via cell click and move * - selection enhancement via border and edge * @param {jQuery.Event} oEvent The mouse event * @private */ CellSelector.prototype._onmousemove = function(oEvent) { function select() { if (this._bBorderDown && !this._bScrolling) { var oBorder = this._oSession.border; var mDiff = { colIndex: isNaN(oBorder.colIndex) ? 0 : oInfo.colIndex - oBorder.colIndex, rowIndex: isNaN(oBorder.rowIndex) ? 0 : oInfo.rowIndex - oBorder.rowIndex }; if (mDiff.rowIndex != 0 || mDiff.colIndex != 0) { const { from, to } = this._getUpdatedBounds(mDiff.rowIndex, mDiff.colIndex, oBorder); this._selectCells(from, to); } } else { this._startSelection(oEvent, true); } this.getConfig("focusCell", this.getControl(), oInfo, false); this._oPreviousCell = oInfo; this._oHoveredCell = oInfo; } // Only update the resizer if during selection the border is not pressed if (this._bSelecting && !this._bMouseDown && this._bRenderResizer) { const mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); this._updateResizers(mBounds, oEvent.clientX, oEvent.clientY); } var oSelectableCell = this._getSelectableCell(oEvent.target); if (!oSelectableCell || !this._bMouseDown || this._oMouseSource?.isA("sap.m.InputBase")) { // Selection logic should not execute if mouse is not down or target is not a cell return; } clearTimeout(this._iTimer); oEvent.stopImmediatePropagation(); // Stop propagation to surpress other actions such as column resizing var oInfo = this.getConfig("getCellInfo", this.getControl(), oSelectableCell, this._oPreviousCell); if (oInfo.rowIndex < 0 || oInfo.colIndex < 0) { return; } const bClickedHovered = oInfo.rowIndex == this._oPreviousCell?.rowIndex && oInfo.colIndex == this._oPreviousCell?.colIndex; if (bClickedHovered || oInfo.type == CellType.Ignore) { return; } // If previously hovered cell is the same as the currently hovered one, do not execute anything (except the hovered cell is of type Other. if (oInfo.type == CellType.Other && this._oHoveredCell?.rowIndex == oInfo.rowIndex && this._oHoveredCell?.colIndex == oInfo.colIndex) { return; } // Remove text selection during mouse cell selection window.getSelection().removeAllRanges(); if (!this._oSession.mSource && !this._oSession.mTarget) { this._oSession.mSource = this._oSession.mTarget = this._mClickedCell; } this._oHoveredCell = null; if (this._oPreviousCell && this._oPreviousCell.type != oInfo.type) { this._iTimer = setTimeout(select.bind(this), DELAY_SHORT); return; } if (this._mClickedCell && this._mClickedCell.type == CellType.Other && this._oPreviousCell?.type == CellType.Cell && oInfo.type == CellType.Other) { this._iTimer = setTimeout(select.bind(this), DELAY_LONG); } else { if (oInfo.type == CellType.Other && this._mClickedCell.type != CellType.Other) { const mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); this._mTempCell = this._mClickedCell; // very hacky to get it to work with popin hover starting from last column this._mClickedCell = mBounds.from; } else { this._mClickedCell = this._mTempCell ?? this._mClickedCell; } select.call(this); } }; /** * Event handler for mouse selection (leaving table, etc.) * * @param {jQuery.Event} oEvent The mouse event * @private */ CellSelector.prototype._onmouseout = function(oEvent) { var oScrollAreaRef = this.getControl().getDomRef(this.getConfig("scrollArea")); if (!oScrollAreaRef || !this._bMouseDown) { return; } var oScrollAreaRect = oScrollAreaRef.getBoundingClientRect(); var bForward, bVertical; this._bScrolling = false; if (oEvent.clientY > oScrollAreaRect.bottom || oEvent.clientY < oScrollAreaRect.top) { this._oSession.scrollForward = bForward = oEvent.clientY > oScrollAreaRect.bottom; this._oSession.isVertical = bVertical = true; this._bScrolling = true; } if (oEvent.clientX > oScrollAreaRect.right || oEvent.clientX < oScrollAreaRect.left) { this._oSession.scrollForward = bForward = oEvent.clientX > oScrollAreaRect.right; this._oSession.isVertical = bVertical = false; this._bScrolling = true; } if (this._bScrolling) { this._doScroll(bForward, bVertical, oEvent); } }; CellSelector.prototype._onmouseenter = function(oEvent) { this._bScrolling = false; this._clearScroller(); }; CellSelector.prototype._doScroll = function(bForward, bVertical, oEvent) { this._clearScroller(); if (this._bScrolling) { this.getConfig("scroll", this.getControl(), bForward, bVertical); this._mTimeouts.scrollTimerId = setTimeout(this._doScroll.bind(this, bForward, bVertical), DELAY_LONG); // If vertical scrolling, wait for the event, then select the next cells, not possible currently with horizontal scrolling if (!bVertical) { this._scrollSelect(bForward, bVertical, oEvent); } } }; CellSelector.prototype._scrollSelect = function(bForward, bVertical, oEvent) { if (!this._bSelecting) { return; } var mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); if (this._bScrolling) { var sDirectionType = bVertical ? DIRECTION.ROW : DIRECTION.COL; var mDiff = { "row": 0, "col": 0 }; var sType = bForward ? "to" : "from"; mDiff[sDirectionType] = bForward ? 1 : -1; let mOldFocus = mBounds[sType]; if (this._bBorderDown) { mOldFocus = this._oSession.border; } const { from, to } = this._getUpdatedBounds(mDiff[DIRECTION.ROW], mDiff[DIRECTION.COL], mOldFocus); this._selectCells(from, to); } }; CellSelector.prototype._clearScroller = function() { if (this._mTimeouts.scrollTimerId) { window.clearTimeout(this._mTimeouts.scrollTimerId); this._mTimeouts.scrollTimerId = null; } }; CellSelector.prototype._onborderdown = function(oEvent) { this._oSession.border = Object.assign({}, this._oCurrentBorder); this._bBorderDown = true; this._bMouseDown = true; // TODO: When borderdown, make "border" active }; /** * For a given DOM reference it returns the closest selectable cell. * @param {HTMLELement} oDomRef DOM reference * @returns {HTMLELement|null} Selectable cell DOM reference * @private */ CellSelector.prototype._getSelectableCell = function (oDomRef) { if (!oDomRef) { return; } return oDomRef.closest(this.getConfig("selectableCells")); }; CellSelector.prototype._inSelection = function(oTarget) { var oInfo = this.getConfig("getCellInfo", this.getControl(), oTarget, this._oPreviousCell); if (!oInfo || !this._oSession.mSource || !this._oSession.mTarget) { return false; } var oBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); const bInBounds = !(oInfo.rowIndex < oBounds.from.rowIndex || oInfo.rowIndex > oBounds.to.rowIndex || oInfo.colIndex < oBounds.from.colIndex || oInfo.colIndex > oBounds.to.colIndex); const bOtherSelected = oInfo.type == CellType.Other && this._oSession.cellTypes.includes(CellType.Other); return bInBounds || bOtherSelected; }; CellSelector.prototype._startSelection = function(oEvent, bMove) { if (!this._shouldBeHandled(oEvent)) { return; } var oTarget = this._getSelectableCell(oEvent.target); if (!oTarget) { return; } if (!this._bSelectionInProgress) { this.getConfig("onSelectionStart", this.getControl(), oEvent); this._bSelectionInProgress = true; if (this._oPreviousCell) { this._startTarget = this.getConfig("getCellRef", this.getControl(), this._oPreviousCell); } } if (this._inSelection(oTarget) && !bMove) { this.removeSelection(); } else { var oCellInfo = this.getConfig("getCellInfo", this.getControl(), oTarget, this._oPreviousCell); var mStart = this._mClickedCell ? this._mClickedCell : oCellInfo; this._bSelecting = true; this._oSession.mSource = oCellInfo; this._selectCells(mStart, oCellInfo); this._oPreviousCell = oCellInfo; this._oOriginCell = mStart; } oEvent.preventDefault(); oEvent.setMarked && oEvent.setMarked(); }; CellSelector.prototype._endSelection = function(oEvent) { if (!this._bSelectionInProgress) { return; } this._bSelectionInProgress = false; var oTarget = this._getSelectableCell(oEvent.target); if (!oTarget) { return; } this.getConfig("onSelectionEnd", this.getControl(), oEvent); }; /** * Selects the next cells in a specific direction (ROW, COL). * @param iRowDiff {int} * @param iColDiff {int} * @param mOldFocus {object} * @returns {object} The updated bounding * @private */ CellSelector.prototype._getUpdatedBounds = function(iRowDiff, iColDiff, mOldFocus) { var mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); var mFocus = Object.assign({}, mOldFocus); // Determine which "side" to adjust according to current position var sAdjustRowType = mFocus.rowIndex == mBounds.from.rowIndex ? "from" : "to"; var sAdjustColType = mFocus.colIndex == mBounds.from.colIndex ? "from" : "to"; mBounds[sAdjustRowType].rowIndex = Math.max(mBounds[sAdjustRowType].rowIndex + iRowDiff, 0); mBounds[sAdjustColType].colIndex = Math.max(mBounds[sAdjustColType].colIndex + iColDiff, 0); const oAdjustedColCell = this.getConfig("getCellRef", this.getControl(), mBounds[sAdjustColType]); if (oAdjustedColCell) { mBounds[sAdjustColType].type = this.getConfig("getCellType", this.getControl(), oAdjustedColCell); } if (!this._bBorderDown) { mFocus.rowIndex = Math.max(0, mFocus.rowIndex + iRowDiff); mFocus.colIndex = Math.max(0, mFocus.colIndex + iColDiff); } else { this._oSession.border.rowIndex += iRowDiff; this._oSession.border.colIndex += iColDiff; } return { from: mBounds.from, to: mBounds.to, focus: mFocus }; }; /** * Selects the cell from the source cell to the provided target cell's coordinates. * * The algorithm builds up a bounding box, goes through all the cells inside it and determines their selection state. * The bounding box can either be ranging from * a) source cell to target cell or * b) source cell to current lower right cell. * The bigger bounding box of the two will be inspected. * @param {sap.m.plugins.CellSelector.CellPosition} mFrom Source cell coordinates * @param {sap.m.plugins.CellSelector.CellPosition} mTo Target cell coordinates * @private */ CellSelector.prototype._selectCells = function (mFrom, mTo) { if (!this._bSelecting) { return; } mFrom = mFrom ? mFrom : this._oSession.mSource; mTo = mTo ? mTo : this._oSession.mTarget; this._oSession.cellTypes = [mFrom.type]; // If the cell type of the hovered cell is not in cell types add it (a Set is probably better here) if (!this._oSession.cellTypes.includes(mTo.type)) { this._oSession.cellTypes.push(mTo.type); } if (mTo.rowIndex == Infinity || mFrom.rowIndex == Infinity) { this.getConfig("loadContexts", this.getControl(), Math.max(Math.min(mFrom, mTo), 0), this.getRangeLimit()); } this._drawSelection(mFrom, mTo); if (!deepEqual(this._oSession.mSource, mFrom) || !deepEqual(this._oSession.mTarget, mTo)) { this._oSession.mSource = mFrom; this._oSession.mTarget = mTo; this._onSelectionChange(); } }; CellSelector.prototype._drawSelection = function (mFrom, mTo) { const bAdjustBounds = !isFinite(mFrom.rowIndex) || !isFinite(mTo.rowIndex); const mBounds = this._getNormalizedBounds(mFrom, mTo, bAdjustBounds); if (!mBounds.from || !mBounds.to) { return; } this._clearSelection(); this._oSession.cellRefs = []; // Check if we need to draw "Other" cells const bDrawOther = this._oSession.cellTypes.includes(CellType.Other); for (var iRow = mBounds.from.rowIndex; iRow <= mBounds.to.rowIndex; iRow++) { // Only draw cells, if the Cell type is included in the selection if (this._oSession.cellTypes.includes(CellType.Cell)) { for (var iCol = mBounds.from.colIndex; iCol <= mBounds.to.colIndex; iCol++) { const mPosition = {rowIndex: iRow, colIndex: iCol}; var oCellRef = this.getConfig("getCellRef", this.getControl(), mPosition); if (oCellRef) { const aRefs = this.getConfig("drawCellBorder", this.getControl(), oCellRef, mPosition, mBounds); this._oSession.cellRefs.push(...aRefs); } } } // Draw other cells, like Popin if (bDrawOther) { const iCol = this.getConfig("numberOfColumns", this.getControl()) - 1; const mPosition = {rowIndex: iRow, colIndex: iCol}; const oCellRef = this.getConfig("getCellRef", this.getControl(), mPosition); if (oCellRef) { const aRefs = this.getConfig("drawCellBorder", this.getControl(), oCellRef, mPosition, mBounds); this._oSession.cellRefs.push(...aRefs); } } } }; CellSelector.prototype._updateResizers = function(mBounds, iPositionX, iPositionY) { var oResizer = this._getResizer(); if (this._iRtl == -1) { const iFromColIndex = mBounds.from.colIndex; mBounds.from.colIndex = mBounds.to.colIndex; mBounds.to.colIndex = iFromColIndex; } var oFromRef = this.getConfig("getCellRef", this.getControl(), mBounds.from, false), oToRef = this.getConfig("getCellRef", this.getControl(), mBounds.to, false); var mOutOfBounds = { 0: false, 1: false }; // 0: top, 1: bottom if (!oFromRef) { mOutOfBounds[0] = true; oFromRef = this.getConfig("getCellRef", this.getControl(), mBounds.from, true); } if (!oToRef) { mOutOfBounds[1] = true; oToRef = this.getConfig("getCellRef", this.getControl(), mBounds.to, true); } if (!oFromRef || !oToRef) { return; } var oFromRect = oFromRef.getBoundingClientRect(), oToRect = oToRef.getBoundingClientRect(), oTableRect = this.getControl().getDomRef().getBoundingClientRect(); var mStyleMap = { x: { 0: oFromRect.left - oTableRect.left, 1: oToRect.left + oToRect.width - oTableRect.left }, y: { 0: oFromRect.top - oTableRect.top, 1: oToRect.top + oToRect.height - oTableRect.top } }; var mDiffMap = { x: { 0: iPositionX - oFromRect.left, 1: iPositionX - oToRect.right }, y: { 0: iPositionY - oFromRect.top, 1: iPositionY - oToRect.bottom } }; // 2 Bit Flags: // Y Direction | X Direction // 0 | 0 var mFlags = 0; mFlags |= Math.abs(mDiffMap.x[0]) < Math.abs(mDiffMap.x[1]) ? 0 : 1; mFlags |= Math.abs(mDiffMap.y[0]) < Math.abs(mDiffMap.y[1]) ? 0 : 2; var iDiffX = Math.abs(mDiffMap.x[mFlags & 1]), iDiffY = Math.abs(mDiffMap.y[(mFlags >> 1) & 1]); if (iDiffX > 10 && iDiffY > 10 || iDiffX > 10 && mOutOfBounds[(mFlags >> 1) & 1]) { return; } oResizer.style.left = iDiffX <= 10 ? mStyleMap.x[mFlags & 1] + "px" : mStyleMap.x[0] + "px"; oResizer.style.top = iDiffY <= 10 ? mStyleMap.y[(mFlags >> 1) & 1] + "px" : mStyleMap.y[0] + "px"; oResizer.style.width = iDiffX <= 10 ? "" : oToRect.right - oFromRect.left + "px"; oResizer.style.height = iDiffX <= 10 ? oToRect.bottom - oFromRect.top + "px" : ""; const bXinRange = iDiffX <= 10, bYinRange = iDiffY <= 10; oResizer.classList.toggle("sapMPluginsVerticalBorder", bXinRange); oResizer.classList.toggle("sapMPluginsHorizontalBorder", bYinRange); oResizer.classList.toggle("sapMPluginsEdge", bXinRange && bYinRange); oResizer.classList.toggle("sapMPluginsNESW", bXinRange && bYinRange && (mFlags == 2 || mFlags == 1)); oResizer.classList.toggle("sapMPluginsNWSE", bXinRange && bYinRange && (mFlags == 3 || mFlags == 0)); this._oCurrentBorder = {}; if (bXinRange) { this._oCurrentBorder.colIndex = mFlags & 1 ? mBounds.to.colIndex : mBounds.from.colIndex; this._oCurrentBorder.type = DIRECTION.COL; } if (bYinRange) { this._oCurrentBorder.rowIndex = (mFlags >> 1) & 1 ? mBounds.to.rowIndex : mBounds.from.rowIndex; this._oCurrentBorder.type = DIRECTION.ROW; } }; /** * Retrieves the resizer element. If none exists, creates an element. * @returns {HTMLELement} Resizer element * @private */ CellSelector.prototype._getResizer = function() { if (!this._oResizer) { this._oResizer = document.createElement("div"); this._oResizer.setAttribute("id", "cs-rsz"); this._oResizer.classList.add("sapMPluginsCellSelectorRsz"); this._oResizer.addEventListener("mousedown", this._onborderdown.bind(this)); if (this.getControl().getDomRef()) { this.getControl().getDomRef().appendChild(this._oResizer); } } return this._oResizer; }; CellSelector.prototype._clearSelection = function() { this._oSession?.cellRefs?.forEach(function(oCellRef) { oCellRef.classList.remove("sapMPluginsCellSelectorSelected", "sapMPluginsCellSelectorTop", "sapMPluginsCellSelectorBottom", "sapMPluginsCellSelectorLeft", "sapMPluginsCellSelectorRight"); oCellRef.removeAttribute("aria-selected"); }); var oResizer = this._getResizer(); oResizer.style.left = "-10000px"; oResizer.style.top = "-10000px"; }; /** * Returns an object containing normalized coordinates for the given bounding area. * <code>from</code> contains the coordinates for the upper left corner of the bounding area, * <code>to</code> contains the coordinates of the lower right corner of the bounding area. * @param {Object} mFrom Source cell coordinates * @param {Object} mTo Target cell coordinates * @param {boolean} bAdjustBounds bounds are adjusted to fit into limit/table boundaries (e.g. range selection) * @returns {object} Object containing coordinates for the bounding area */ CellSelector.prototype._getNormalizedBounds = function(mFrom, mTo, bAdjustBounds) { const iMaxColumns = this.getConfig("numberOfColumns", this.getControl()); const iMaxRows = this.getRangeLimit() == 0 ? this.getConfig("getRowCount", this.getControl()) : this.getRangeLimit(); let toRowIndex = Math.max(mFrom.rowIndex, mTo.rowIndex), toColIndex = Math.max(mFrom.colIndex, mTo.colIndex); if (bAdjustBounds) { toRowIndex = Math.min(iMaxRows, toRowIndex); toColIndex = Math.min(iMaxColumns, toColIndex); } return { from: {rowIndex: Math.max(0, Math.min(mFrom.rowIndex, mTo.rowIndex)), colIndex: Math.max(0, Math.min(mFrom.colIndex, mTo.colIndex)), type: mFrom.type}, to: {rowIndex: toRowIndex, colIndex: toColIndex, type: mTo.type} }; }; CellSelector.prototype._shouldBeHandled = function(oEvent) { // Handle if event is not marked and control is applicable return !oEvent.isMarked?.() && this.getConfig("isSupported", this.getControl(), this); }; CellSelector.prototype._getBinding = function() { return this.getConfig("getBinding", this.getControl()); }; function isSelectableCell(oDomRef, sSelectors) { return oDomRef.matches(sSelectors); } function isGroupRow(oBinding, oContext, iIndex) { const oRowContext = oBinding?.getNodeByIndex?.(iIndex) ?? oContext; if (oBinding?.nodeHasChildren) { return oBinding.nodeHasChildren(oRowContext); } return !(oRowContext.getProperty("@ui5.node.isExpanded") === undefined); } function getRow(aRows, iRow, bIsRange, fnGetIndex) { if (bIsRange && aRows[0]) { return fnGetIndex(aRows[0]) > iRow ? aRows[0] : aRows[aRows.length - 1]; } return aRows.find((oRow) => fnGetIndex(oRow) == iRow); } function getCellDOM(oCell, sClasses) { let oCellRef = oCell?.$().closest(sClasses)[0]; if (!oCellRef && !oCell.getVisible()) { const oInvisibleCell = document.getElementById(InvisibleRenderer.createInvisiblePlaceholderId(oCell)); oCellRef = oInvisibleCell?.closest(sClasses); } return oCellRef; } /** * Checks whether the key press event is a key combination. * * @param {sap.ui.base.Event} oEvent The keyboard event * @param {string} sKeyCode Key code * @param {boolean} bShift Shift key pressed * @param {boolean} bCtrl Control key pressed * @returns {boolean} Whether the key press event is a key combination * @private */ function isKeyCombination(oEvent, sKeyCode, bShift, bCtrl) { return oEvent.keyCode == sKeyCode && oEvent.shiftKey == bShift && (oEvent.ctrlKey == bCtrl || oEvent.metaKey == bCtrl); } /** * Checks whether drag on the rows/items aggregation is activated. * @param {sap.ui.core.Control} oControl Control to be checked * @param {string} sAffectedAggregation Name of the aggregation which is affected by D&D * @returns {boolean} Whether drag on rows is enabled */ function hasDragEnabled(oControl, sAffectedAggregation) { return oControl.getDragDropConfig().some((oConfig) => oConfig.getSourceAggregation?.() == sAffectedAggregation && oConfig.getEnabled()); } PluginBase.setConfigs({ "sap.ui.table.Table": { selectableCells: ".sapUiTableDataCell", scrollArea: "tableCCnt", scrollEvent: "firstVisibleRowChanged", eventClearedAll: "sapUiTableClearAll", onActivate: function(oTable, oPlugin) { oTable.attachEvent("_change", oPlugin, this._onPropertyChange); oTable.attachEvent("EventHandlerChange", oPlugin, this._onEventHandlerChange); }, onDeactivate: function(oTable, oPlugin) { oTable.detachEvent("_change", this._onPropertyChange); oTable.detachEvent("EventHandlerChange", this._onEventHandlerChange); }, _onPropertyChange: function(oEvent, oPlugin) { oEvent.getParameter("name") == "selectionBehavior" && oPlugin._onSelectableChange(); }, _onEventHandlerChange: function(oEvent, oPlugin) { oEvent.getParameter("EventId") == "cellClick" && oPlugin._onSelectableChange(); }, /** * Checks whether the table is compatible with cell selection. * @param {sap.ui.table.Table} oTable Table instance * @returns {boolean} Compatibility with cell selection */ isSupported: function(oTable, oPlugin) { return !oTable.hasListeners("cellClick") && !hasDragEnabled(oTable, "rows"); }, isBottomToTop: function(oTable) { return false; }, /** * Returns the visible columns of the table. * @param {sap.ui.table.Table} oTable Table instance * @returns {sap.ui.table.Column[]} Array of visible columns */ getVisibleColumns: function (oTable) { return oTable.getColumns().filter(function (oColumn) { return oColumn.getDomRef(); }); }, /** * Retrieve the number of visible columns in the table. * @param {sap.ui.table.Table} oTable Table instance * @param {boolean} bIncludeSpecial Include special columns as separate columns * @returns {number} Number of columns */ numberOfColumns: function(oTable, bIncludeSpecial) { return this.getVisibleColumns(oTable).length; }, getRowCount: function(oTable) { return oTable._getTotalRowCount(); }, /** * Retrieve the cell reference for a given position * @param {sap.ui.table.Table} oTable table instance * @param {sap.m.plugins.CellSelector.CellPosition} mPosition position of cell * @param {boolean} bRange whether the cell is part of a range * @returns {HTMLElement|undefined} cell's DOM element or undefined if the row or column index are invalid */ getCellRef: function (oTable, mPosition, bRange) { const oRow = getRow(oTable.getRows(), mPosition.rowIndex, bRange, (oRow) => oRow?.getIndex()); const oColumn = this.getVisibleColumns(oTable)[mPosition.colIndex]; if (!oRow || !oColumn) { return null; } return getCellDOM(oRow.getCells().filter((oCell) => oCell.getDomRef() || !oCell.getVisible())[mPosition.colIndex], this.selectableCells); }, /** * Retrieve cell information for a given DOM element. * @param {sap.ui.table.Table} oTable Table instance * @param {HTMLElement} oTarget DOM reference of cell * @returns {object} Cell information containing rowIndex, colIndex and cell type */ getCellInfo: function (oTable, oTarget) { return { rowIndex: Element.closestTo(oTarget, true).getIndex(), colIndex: this.getVisibleColumns(oTable).indexOf(Element.getElementById(oTarget.getAttribute("data-sap-ui-colid"))), type: this.getCellType(oTable, oTarget) }; }, /** * Returns the cell type of the given target cell. * @param {sap.ui.table.Table} oTable Table instance * @param {HTMLELement} oTarget Cell reference * @returns {string} Cell type */ getCellType: function(oTable, oTarget) { const oRow = Element.closestTo(oTarget, true); let sType = CellType.Cell; if (oRow.isGroupHeader()) { sType = CellType.Ignore; } return sType; }, /** * Retrieves the row contexts of the table according to the specified parameters. * @param {sap.ui.table.Table} oTable The table instance * @param {int} iFromIndex The start index * @param {int} iToIndex The end index * @param {int} iLimit The range limit * @returns {sap.ui.model.Context[]} A portion of the row binding contexts */ getSelectedRowContexts: function(oTable, iFromIndex, iToIndex, iLimit) { if (iToIndex == Infinity) { var iMaxIndex = oTable.getBinding("rows").getAllCurrentContexts().length - 1; iToIndex = Math.min(iToIndex, iFromIndex + iLimit - 1, iMaxIndex); } var aContexts = []; for (var i = iFromIndex; i <= iToIndex; i++) { aContexts.push(oTable.getContextByIndex(i)); } return aContexts; }, /** * Selects the rows with indices between iFrom and iTo. * @param {sap.ui.table.Table} oTable The table instance * @param {int} iFrom Start row index * @param {int} iTo End row index * @param {int} iFocus Focused row index * @returns {boolean} Returns true if the selection was successful */ selectRows: function(oTable, iFrom, iTo, iFocus) { var oSelectionOwner = this._getSelectionOwner(oTable); var sSelectionMode = oTable.getSelectionMode(); if (sSelectionMode == "None") { return false; } else if (sSelectionMode == "Single") { iFrom = iTo = iFocus; } if (oSelectionOwner.addSelectionInterval && oSelectionOwner.removeSelectionInterval) { for (let i = iFrom; i <= iTo; i++) { const bSelected = oSelectionOwner.isIndexSelected?.(i) ?? false; // Toggle Selection State if (bSelected) { oSelectionOwner.removeSelectionInterval(i, i); } else { oSelectionOwner.addSelectionInterval(i, i); } } return true; } // TODO: Handle V4 correctly. Currrently only selects visible rows var aRows = oTable.getRows().filter(function(oRow) { return oRow.getIndex() >= iFrom && oRow.getIndex() <= iTo; }); aRows.forEach((oRow) => { oSelectionOwner.setSelected(oRow, !this.isRowSelected(oTable, oRow)); }); return true; }, /** * Checks if the given row is selected. * @param {sap.ui.table.Table} oTable Table instance * @param {number|sap.ui.table.Row} vRow Either row index or row instance * @returns {boolean} Selection state */ isRowSelected: function(oTable, vRow) { var oSelectionOwner = this._getSelectionOwner(oTable); if (typeof vRow === "number") { vRow = oTable.getRows().find(function(oRow) { return oRow.getIndex() == vRow; }); } let bSelectionState = oSelectionOwner.isIndexSelected?.(vRow); if (vRow) { bSelectionState = oSelectionOwner.isSelected?.(vRow); } return bSelectionState ?? false; }, focusCell: function(oTable, mFocus, bIsKeyboard, bForward) { var oCellRef = this.getCellRef(oTable, mFocus); if (!oCellRef) { this.scroll(oTable, bForward, true); return; } oCellRef.focus(); }, scroll: function(oTable, bForward, bVertical) { if (bVertical) { var iFirstVisibleRowIndex = oTable.getFirstVisibleRow(); var iIndex = bForward ? iFirstVisibleRowIndex + 1 : iFirstVisibleRowIndex - 1; if (iIndex >= 0 && iIndex != iFirstVisibleRowIndex) { oTable.setFirstVisibleRow(iIndex); return Promise.resolve(); } } else { var oScrollBar = oTable._getScrollEx