UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

917 lines (799 loc) 33.3 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/events/KeyCodes", "sap/ui/core/Core", "sap/ui/core/Element", "sap/base/Log" ], function (PluginBase, KeyCodes, Core, Element, Log) { "use strict"; var DIRECTION = { ROW: "row", COL: "col" }; /** * Constructor for a new CellSelector plugin. * * @param {string} [sId] id for the new control, generated automatically if no id is given * @param {object} [mSettings] initial settings for the new control * * The CellSelector plugins 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. * * @extends sap.m.plugins.PluginBase * @class * @version 1.117.4 * @author SAP SE * * @private * @experimental * @alias sap.m.plugins.CellSelector */ var CellSelector = PluginBase.extend("sap.m.plugins.CellSelector", { metadata: { library: "sap.m", properties: { /** * For the {@link sap.ui.table.Table} control, defines the number of row contexts that needs to be retrived 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} }, events: {} } }); /* CellSelector Interactions Keyboard Usage: - SPACE: Select the currently focused cell. If pressed inside a selection block, removes selection. - SHIFT + ARROW_KEYS: Adjusts an already existing selection block OR creates a new one, if used outside of selection block. - SHIFT + SPACE: Transforms current selection block into row selection. Result depends on selection mode applied to table. - CTRL + SPACE: Extends selection to all cells of the column (up to the range limit). - CTRL + SHIFT + A: Clears selection Mouse Usage: - CTRL + Click: Select cell. If click is on selection block, removes selection block. - Mousedown + Movement: Selects the cell where mouse was pressed down. If move goes outside, extends selection. - Borders: Extend/reduce selection either horizontally or vertically. - Edge: Extend/reduce selection freely. */ CellSelector.prototype.onActivate = function (oControl) { this._iRtl = Core.getConfiguration().getRTL() ? -1 : 1; oControl.addDelegate(this, true, this); this._oSession = { cellRefs: [] }; this._mTimeouts = {}; this._fnControlUpdate = function(oEvent) { if (this._bScrolling) { this._scrollSelect(this._oSession.scrollForward, this._oSession.isVertical, oEvent); } else { this._selectCells(); } }.bind(this); this._fnOnMouseEnter = this._onmouseenter.bind(this); this._fnOnMouseOut = this._onmouseout.bind(this); this._fnOnMouseMove = this._onmousemove.bind(this); this._fnOnMouseUp = this.onmouseup.bind(this); // Register Events, as adding dependent does not trigger rerendering this._registerEvents(); }; CellSelector.prototype.onDeactivate = function (oControl) { oControl.removeDelegate(this, this); if (this._oSession) { this.removeSelection(); this._oSession = null; this._mTimeouts = null; } this._deregisterEvents(); }; CellSelector.prototype.destroy = function() { if (this.getControl() && !this.getControl().isDestroyed()) { this.removeSelection(); } this._deregisterEvents(); this._oSession = null; this._mTimeouts = null; return PluginBase.prototype.destroy.call(this); }; CellSelector.prototype.onBeforeRendering = function() { if (this._oResizer) { // remove resizer, as due to rerendering table element may be gone this._oResizer.remove(); this._oResizer = null; } }; CellSelector.prototype.onAfterRendering = function() { this._deregisterEvents(); this._registerEvents(); }; CellSelector.prototype._registerEvents = function() { if (this.getControl()) { this.getControl().attachEvent(this.getConfig("scrollEvent"), this._fnControlUpdate); var oScrollArea = this.getControl().getDomRef(this.getConfig("scrollArea")); if (oScrollArea) { oScrollArea.addEventListener("mouseleave", this._fnOnMouseOut); oScrollArea.addEventListener("mouseenter", this._fnOnMouseEnter); } } document.addEventListener("mousemove", this._fnOnMouseMove); document.addEventListener("mouseup", this._fnOnMouseUp); }; CellSelector.prototype._deregisterEvents = function() { if (this.getControl()) { this.getControl().detachEvent(this.getConfig("scrollEvent"), this._fnControlUpdate); var oScrollArea = this.getControl().getDomRef(this.getConfig("scrollArea")); oScrollArea.removeEventListener("mouseleave", this._fnOnMouseOut); oScrollArea.removeEventListener("mouseenter", this._fnOnMouseEnter); } document.removeEventListener("mousemove", this._fnOnMouseMove); document.removeEventListener("mouseup", this._fnOnMouseUp); }; /** * Returns the cell selection range. * The value <code>Infinity</code> in <code>rowIndex</code> meant for until the limit is reached. * * Note: This method is subject to change. * @returns {{from: {rowIndex: int, colIndex: int}, to: {rowIndex: int, colIndex: int}} The range of the selection * @private * @experimental Since 1.110 * @ui5-restricted sap.m.plugins.CopyProvider */ CellSelector.prototype.getSelectionRange = function () { if (!this._bSelecting) { return null; } var mSelectionRange = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget, true); if (isNaN(mSelectionRange.from.rowIndex) || isNaN(mSelectionRange.to.rowIndex)) { return null; } var iMaxColumnIndex = this.getConfig("getVisibleColumns", this.getControl()).length - 1; mSelectionRange.from.colIndex = Math.max(mSelectionRange.from.colIndex, 0); mSelectionRange.to.colIndex = Math.min(mSelectionRange.to.colIndex, iMaxColumnIndex); mSelectionRange.from.rowIndex = Math.max(mSelectionRange.from.rowIndex, 0); return mSelectionRange; }; /** * Returns the row binding context of the current selection. * * Note: This method is subject to change. * @returns {sap.ui.model.Context[]} The binding context of selected rows * @private * @experimental Since 1.110 * @ui5-restricted sap.m.plugins.CopyProvider */ CellSelector.prototype.getSelectedRowContexts = function () { var mSelectionRange = this.getSelectionRange(); if (!mSelectionRange) { return []; } return this.getConfig("rowContexts", this.getControl(), mSelectionRange.from.rowIndex, mSelectionRange.to.rowIndex, this.getRangeLimit()); }; CellSelector.prototype.onsapspace = function (oEvent) { this._startSelection(oEvent, false); }; CellSelector.prototype.onsapupmodifiers = function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.ROW, -1, 0); }; CellSelector.prototype.onsapdownmodifiers = function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.ROW, 1, 0); }; CellSelector.prototype.onsapleftmodifiers = function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.COL, 0, -1); }; CellSelector.prototype.onsaprightmodifiers = function(oEvent) { this._onsaparrowmodifiers(oEvent, DIRECTION.COL, 0, 1); }; CellSelector.prototype._onsaparrowmodifiers = function(oEvent, sDirectionType, iRowDiff, iColDiff) { if (oEvent.isMarked() || !oEvent.shiftKey) { return; } var oSelectableCell = this._getSelectableCell(oEvent.target); if (!oSelectableCell) { return; } var oInfo = this.getConfig("getCellInfo", this.getControl(), oSelectableCell); if (!this._inSelection(oEvent.target) || !this._oSession.mSource || !this._oSession.mTarget) { // If not in selection block, start new selection block this._oSession.mSource = this._oSession.mTarget = oInfo; } var mBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); const { from, to, focus } = this._getUpdatedBounds(iRowDiff, iColDiff * this._iRtl, oInfo); if (focus[sDirectionType + "Index"] < 0 || focus.colIndex >= this.getConfig("getVisibleColumns", this.getControl()).length) { return; } this.getConfig("focusCell", this.getControl(), focus, 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.preventDefault(); oEvent.setMarked(); oEvent.setMarked("sapUiTableSkipItemNavigation", true); // Required to prevent item navigation in GridTable for MultiToggle } }; // To be implemented. See internal documentation. CellSelector.prototype.onsaphomemodifiers = function (oEvent) {}; // To be implemented. See internal documentation. CellSelector.prototype.onsapendmodifiers = function (oEvent) {}; /** * Handles: * - CTRL + SHIFT + A * - CTRL + A * @param {sap.ui.base.Event} oEvent event instance */ CellSelector.prototype.onkeydown = function (oEvent) { if (!this._bSelecting || oEvent.isMarked()) { return; } if (isKeyCombination(oEvent, KeyCodes.A, true, true) || isKeyCombination(oEvent, KeyCodes.A, false, false)) { this.removeSelection(); } oEvent.preventDefault(); }; /** * Handles: * - SPACE + SHIFT * - SPACE + CTRL/CMD * @param {sap.ui.base.Event} oEvent event instance */ CellSelector.prototype.onkeyup = function(oEvent) { if (oEvent.isMarked()) { return; } var mBounds = this._bSelecting ? this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget) : undefined; if (isKeyCombination(oEvent, KeyCodes.SPACE, true, false)) { if (this._inSelection(oEvent.target)) { var oInfo = this.getConfig("getCellInfo", this.getControl(), oEvent.target); this.getConfig("selectRows", this.getControl(), mBounds.from.rowIndex, mBounds.to.rowIndex, oInfo.rowIndex) && this.removeSelection(); oEvent.setMarked(); } } else if (this._bSelecting && isKeyCombination(oEvent, KeyCodes.SPACE, false, true)) { if (!this._inSelection(oEvent.target)) { // If focus is on cell outside of selection, select focused column var oInfo = this.getConfig("getCellInfo", this.getControl(), oEvent.target); mBounds.from.colIndex = mBounds.to.colIndex = oInfo.colIndex; } mBounds.from.rowIndex = 0; mBounds.to.rowIndex = Infinity; this._selectCells(mBounds.from, mBounds.to); } oEvent.preventDefault(); }; // Mouse Navigation CellSelector.prototype.onmousedown = function(oEvent) { if (oEvent.isMarked && oEvent.isMarked()) { return; } if (oEvent.ctrlKey || oEvent.metaKey) { this._startSelection(oEvent); } this._bMouseDown = true; var oSelectableCell = this._getSelectableCell(oEvent.target); if (oSelectableCell) { this._mClickedCell = this.getConfig("getCellInfo", this.getControl(), oSelectableCell); } }; /** * Event handler for mouse movement. Handles mouse movement during cell selection. Takes on tasks like: * - updating resizer positions * - mouse selection via cell click and move * - selection enhancement via border and edge * @param {sap.ui.base.Event} oEvent event */ CellSelector.prototype._onmousemove = function(oEvent) { // Only update the resizer, if we are selecting and the border is not pressed. During border/edge pressing, don't update it if (this._bSelecting && !this._bMouseDown) { var 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) { return; } // If clicked cell (=starting cell) is equal to currently hovered cell, don't do anything var oInfo = this.getConfig("getCellInfo", this.getControl(), oSelectableCell); if (this._mClickedCell && oInfo.rowIndex == this._mClickedCell.rowIndex && oInfo.colIndex == this._mClickedCell.colIndex) { return; } // Remove text selection during mouse cell selection window.getSelection().removeAllRanges(); 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); this._oSession.border.rowIndex += mDiff.rowIndex; this._oSession.border.colIndex += mDiff.colIndex; } } else { this._startSelection(oEvent, true); } }; /** Event Handler for Mouse Selection (leaving table, etc.) */ 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), 500); // 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; const { from, to } = this._getUpdatedBounds(mDiff[DIRECTION.ROW], mDiff[DIRECTION.COL], mBounds[sType]); this._selectCells(from, to); } }; CellSelector.prototype._clearScroller = function() { if (this._mTimeouts.scrollTimerId) { window.clearTimeout(this._mTimeouts.scrollTimerId); this._mTimeouts.scrollTimerId = null; } }; CellSelector.prototype.onmouseup = function(oEvent) { this._bMouseDown = false; this._bBorderDown = false; this._mClickedCell = undefined; this._bScrolling = false; this._clearScroller(); }; CellSelector.prototype._onborderdown = function(oEvent) { this._oSession.border = Object.assign({}, this._oCurrentBorder); this._bBorderDown = true; this._bMouseDown = true; // TODO: when borderdown, make "border" active }; /** * Checks if the given DOM reference is a selectable cell. * @param {HTMLELement} oDomRef * @returns {HTMLELement|null} */ CellSelector.prototype._getSelectableCell = function (oDomRef) { return oDomRef && oDomRef.closest(this.getConfig("selectableCells")); }; CellSelector.prototype._inSelection = function(oTarget) { var oInfo = this.getConfig("getCellInfo", this.getControl(), oTarget); if (!oInfo || !this._oSession.mSource || !this._oSession.mTarget) { return false; } var oBounds = this._getNormalizedBounds(this._oSession.mSource, this._oSession.mTarget); return !(oInfo.rowIndex < oBounds.from.rowIndex || oInfo.rowIndex > oBounds.to.rowIndex || oInfo.colIndex < oBounds.from.colIndex || oInfo.colIndex > oBounds.to.colIndex); }; CellSelector.prototype._startSelection = function(oEvent, bMove) { if (oEvent.isMarked && oEvent.isMarked()) { return; } var oTarget = this._getSelectableCell(oEvent.target); if (!oTarget) { return; } if (this._inSelection(oTarget) && !bMove) { this.removeSelection(); } else { var oCellInfo = this.getConfig("getCellInfo", this.getControl(), oTarget); var mStart = this._mClickedCell ? this._mClickedCell : oCellInfo; this._bSelecting = true; this._oSession.mSource = oCellInfo; this._selectCells(mStart, oCellInfo); this.getConfig("focusCell", this.getControl(), oCellInfo); } oEvent.preventDefault(); oEvent.setMarked && oEvent.setMarked(); }; /** * Selects the next cells in a specific direction (ROW, COL). * @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 += iRowDiff; mBounds[sAdjustColType].colIndex += iColDiff; if (!this._bBorderDown) { mFocus.rowIndex = Math.max(0, mFocus.rowIndex + iRowDiff); mFocus.colIndex = Math.max(0, mFocus.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 {Object} mFrom source cell coordinates * @param {int} mFrom.rowIndex row index * @param {int} mFrom.colIndex column index * @param {Object} mTo target cell coordinates * @param {int} mTo.rowIndex row index * @param {int} mTo.colIndex column index * @private */ CellSelector.prototype._selectCells = function (mFrom, mTo, mFocus) { if (!this._bSelecting) { return; } this._clearSelection(); mFrom = mFrom ? mFrom : this._oSession.mSource; mTo = mTo ? mTo : this._oSession.mTarget; var mBounds = this._getNormalizedBounds(mFrom, mTo); if (mTo.rowIndex == Infinity || mFrom.rowIndex == Infinity) { this.getConfig("loadContexts", this.getControl(), mBounds.from.rowIndex, this.getRangeLimit()); } this._drawSelection(mBounds); this._oSession.mSource = mFrom; this._oSession.mTarget = mTo; }; /** * Draws the selection for the given bounds. * @param {Object} mBounds object containing the bounds information (from, to) * @param {Object} mBounds.from from position * @param {Object} mBounds.to to position * @private */ CellSelector.prototype._drawSelection = function (mBounds) { if (!mBounds.from || !mBounds.to) { return; } this._oSession.cellRefs = []; for (var iRow = mBounds.from.rowIndex; iRow <= mBounds.to.rowIndex; iRow++) { for (var iCol = mBounds.from.colIndex; iCol <= mBounds.to.colIndex; iCol++) { var oCellRef = this.getConfig("getCellRef", this.getControl(), {rowIndex: iRow, colIndex: iCol}); if (oCellRef) { oCellRef.classList.toggle("sapMPluginsCellSelectorTop", iRow == mBounds.from.rowIndex); oCellRef.classList.toggle("sapMPluginsCellSelectorBottom", iRow == mBounds.to.rowIndex); oCellRef.classList.toggle("sapMPluginsCellSelectorRight", iCol == mBounds.to.colIndex); oCellRef.classList.toggle("sapMPluginsCellSelectorSelected", true); oCellRef.setAttribute("aria-selected", "true"); this._oSession.cellRefs.push(oCellRef); // Grid Table has only border-right, so adding border-left would change the size of the column. Instead, for the left border, take the previous cell and set border-right. if (iCol == mBounds.from.colIndex) { const oPrevCellRef = this.getConfig("getCellRef", this.getControl(), {rowIndex: iRow, colIndex: iCol - 1}); let sClass = "sapMPluginsCellSelectorLeft"; if (oPrevCellRef) { oCellRef = oPrevCellRef; sClass = "sapMPluginsCellSelectorRight"; this._oSession.cellRefs.push(oCellRef); } oCellRef.classList.toggle(sClass, iCol == mBounds.from.colIndex); } } } } }; CellSelector.prototype._updateResizers = function(mBounds, iPositionX, iPositionY) { var oResizer = this._getResizer(); 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); } var oFromRect = oFromRef.getBoundingClientRect(), oToRect = oToRef.getBoundingClientRect(), oTableRect = this.getControl().getDomRef().getBoundingClientRect(); var mStyleMap = { x: { 0: oFromRect.left, 1: oToRect.left + oToRect.width }, 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; } }; 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)); 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"; }; /** * Remove the current selection block. */ CellSelector.prototype.removeSelection = function () { this._clearSelection(); this._bSelecting = false; this._oSession = { cellRefs: [] }; }; /** * Returns an object containing normalized coordinates for the given bounding area. * <code>from</code> will contain the coordinates for the upper left corner of the bounding area, * while <code>to</code> contains the coordinates of the lower right corner of the bounding area. * @param {Object} mFrom * @param {int} mFrom.rowIndex row index * @param {int} mFrom.colIndex column index * @param {Object} mTo * @param {int} mTo.rowIndex row index * @param {int} mTo.colIndex column index * @returns object containing coordinates for from and to */ CellSelector.prototype._getNormalizedBounds = function(mFrom, mTo, bKeepBounds) { const iMaxColumns = this.getConfig("getVisibleColumns", this.getControl()).length; const iMaxRows = this.getRangeLimit() == 0 ? this.getConfig("getRowCount", this.getControl()) : this.getRangeLimit(); let toRowIndex = Math.max(mFrom.rowIndex, mTo.rowIndex); let toColIndex = Math.max(mFrom.colIndex, mTo.colIndex); if (!bKeepBounds) { toRowIndex = Math.min(iMaxRows - 1, 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))}, to: {rowIndex: toRowIndex, colIndex: toColIndex} }; }; /** * Check if the given key combination applies to the event. * @param {sap.ui.base.Event} oEvent event instance * @param {string} sKeyCode key code * @param {boolean} bShift shift key pressed * @param {boolean} bCtrl control key pressed * @returns is combination or not */ function isKeyCombination(oEvent, sKeyCode, bShift, bCtrl) { return oEvent.keyCode == sKeyCode && oEvent.shiftKey == bShift && (oEvent.ctrlKey == bCtrl || oEvent.metaKey == bCtrl); } PluginBase.setConfigs({ "sap.ui.table.Table": { selectableCells: ".sapUiTableDataCell", scrollArea: "sapUiTableCtrlScr", scrollEvent: "_rowsUpdated", /** * Get 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(); }); }, getRowCount: function(oTable) { return oTable._getTotalRowCount(); }, /** * Retrieve the cell reference for a given position * @param {sap.ui.table.Table} oTable table instance * @param {Object} mPosition position * @param {int} mPosition.rowIndex row index * @param {int} mPosition.colIndex column index * @returns {HTMLElement} cell's DOM element */ getCellRef: function (oTable, mPosition, bRange) { var aRows = oTable.getRows(); var oRow = aRows.find(function(oRow) { return oRow.getIndex() == mPosition.rowIndex; }); if (oRow) { var oColumn = this.getVisibleColumns(oTable)[mPosition.colIndex]; var oCell = oColumn && oRow.getCells()[mPosition.colIndex]; if (oCell) { return oCell.$().closest(this.selectableCells)[0]; } } else if (bRange) { if (aRows[0].getIndex() > mPosition.rowIndex) { oRow = aRows[0]; var oColumn = this.getVisibleColumns(oTable)[mPosition.colIndex]; var oCell = oColumn && oRow.getCells()[mPosition.colIndex]; if (oCell) { return oCell.$().closest(this.selectableCells)[0]; } } else if (aRows[aRows.length - 1].getIndex() < mPosition.rowIndex) { oRow = aRows[aRows.length - 1]; var oColumn = this.getVisibleColumns(oTable)[mPosition.colIndex]; var oCell = oColumn && oRow.getCells()[mPosition.colIndex]; if (oCell) { return oCell.$().closest(this.selectableCells)[0]; } } } }, /** * Retrieve cell information for a given DOM element. * @param {sap.ui.table.Table} oTable table instance * @param {HTMLElement} oTarget DOM element of cell * @returns {Object} cell information containing rowIndex and colIndex */ getCellInfo: function (oTable, oTarget) { return { rowIndex: Element.closestTo(oTarget, true).getIndex(), colIndex: this.getVisibleColumns(oTable).indexOf(Core.byId(oTarget.getAttribute("data-sap-ui-colid"))) }; }, /** * Loads contexts according to the provided parameters without changing the binding's state. * * @param {sap.ui.table.Table} oTable The Table instance * @param {int} iStartIndex The index where to start the retrieval of contexts * @param {int} iLength The number of contexts to retrieve beginning from the start index. */ loadContexts: function(oTable, iStartIndex, iLength) { var oBinding = oTable.getBinding("rows"); if (!oBinding || oBinding.isA("sap.ui.model.ClientListBinding")) { return; } oBinding.getContexts(Math.max(0, iStartIndex), Math.max(1, iLength), 0, true); }, /** * 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 */ rowContexts: 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; }, /** * Select rows beginning at iFrom to iTo. * @param {sap.ui.table.Table} oTable The table instance * @param {int} iFrom starting row index * @param {int} iTo ending row index * @param {int} mFocus focused row index */ selectRows: function(oTable, iFrom, iTo, iFocus) { var oSelectionOwner = PluginBase.getPlugin(oTable, "sap.ui.table.plugins.SelectionPlugin") || oTable; var sSelectionMode = oTable.getSelectionMode(); if (sSelectionMode == "None") { return false; } else if (sSelectionMode == "Single") { iFrom = iTo = iFocus; } if (oSelectionOwner.addSelectionInterval) { oSelectionOwner.addSelectionInterval(iFrom, iTo); 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(function(oRow) { oSelectionOwner.setSelected(oRow, true); }); return true; }, focusCell: function(oTable, mFocus, 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._getScrollExtension().getHorizontalScrollbar(); var iScrollDiff = Math.pow(-1, +!bForward) * 10; oScrollBar.scrollLeft = Math.max(0, oScrollBar.scrollLeft + iScrollDiff); return Promise.resolve(); } return false; } } }, CellSelector); return CellSelector; });