UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,624 lines (1,399 loc) 68.1 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.TileContainer. sap.ui.define([ './library', "sap/base/i18n/Localization", 'sap/ui/core/Control', 'sap/ui/core/Element', 'sap/ui/core/IconPool', 'sap/ui/Device', "sap/ui/core/RenderManager", 'sap/ui/core/ResizeHandler', './TileContainerRenderer', "sap/base/Log", "sap/ui/thirdparty/jquery", // jQuery custom selectors ':sapTabbable' "sap/ui/dom/jquery/Selectors" ], function( library, Localization, Control, Element, IconPool, Device, RenderManager, ResizeHandler, TileContainerRenderer, Log, jQuery ) { "use strict"; /** * Constructor for a new TileContainer. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * A container that arranges same-size tiles nicely on carousel pages. * @extends sap.ui.core.Control * * @author SAP SE * @version 1.146.0 * * @constructor * @public * @since 1.12 * @deprecated as of version 1.50, replaced by a container of your choice with {@link sap.m.GenericTile} instances * @alias sap.m.TileContainer */ var TileContainer = Control.extend("sap.m.TileContainer", /** @lends sap.m.TileContainer.prototype */ { metadata : { library : "sap.m", deprecated: true, properties : { /** * Defines the width of the TileContainer in px. */ width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : '100%'}, /** * Defines the height of the TileContainer in px. */ height : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : '100%'}, /** * Determines whether the TileContainer is editable so you can move, delete or add tiles. */ editable : {type : "boolean", group : "Misc", defaultValue : null}, /** * Determines whether the user is allowed to add Tiles in Edit mode (editable = true). */ allowAdd : {type : "boolean", group : "Misc", defaultValue : null} }, defaultAggregation : "tiles", aggregations : { /** * The Tiles to be displayed by the TileContainer. */ tiles : {type : "sap.m.Tile", multiple : true, singularName : "tile"} }, events : { /** * Fires if a Tile is moved. */ tileMove : { parameters : { /** * The Tile that has been moved. */ tile : {type : "sap.m.Tile"}, /** * The new index of the Tile in the tiles aggregation. */ newIndex : {type : "int"} } }, /** * Fires if a Tile is deleted in Edit mode. */ tileDelete : { parameters : { /** * The deleted Tile. */ tile : {type : "sap.m.Tile"} } }, /** * Fires when a Tile is added. */ tileAdd : {} } }, renderer: TileContainerRenderer }); IconPool.insertFontFaceStyle(); TileContainer.prototype._bRtl = Localization.getRTL(); /** * Initializes the control. * * @private */ TileContainer.prototype.init = function() { this._iCurrentTileStartIndex = 0; //keeps info about last known container dimension in order to reduce the access to the DOM. Guarantee up to date //value hooking into the resize handler and onAfterRendering. this._oDim = null; this._iScrollLeft = 0; this._iScrollGap = 0; // gap to the left and right that is allowed to be moved while touchmove event if max scrollwidth or min scrollwidth is already reached if (!Device.system.desktop) { this._iScrollGap = 0; } this.bAllowTextSelection = false; this._oDragSession = null; this._oTouchSession = null; this._bAvoidChildTapEvent = false; // the amount on the left and right during drag drop of a tile needed to start showing the edge of the page this._iEdgeShowStart = Device.system.phone ? 10 : 20; // the amount of pixels a tile needs to be moved over the left or right edge to trigger a scroll if (Device.system.phone) { this._iTriggerScrollOffset = 10; } else if (Device.system.desktop) { this._iTriggerScrollOffset = -40; } else { this._iTriggerScrollOffset = 20; } // keyboard support this._iCurrentFocusIndex = -1; if (Device.system.desktop || Device.system.combi) { var fnOnHome = jQuery.proxy(function(oEvent) { if (this._iCurrentFocusIndex >= 0) { var iRowFirstTileIndex = this._iCurrentFocusIndex - this._iCurrentFocusIndex % this._iMaxTilesX; var iFirstOnPageOrVeryFirstIndex = this._iCurrentTileStartIndex === this._iCurrentFocusIndex ? 0 : this._iCurrentTileStartIndex; var iTargetTileIndex = oEvent.ctrlKey // if we are on the first tile of the current page already, go to the very first tile ? iFirstOnPageOrVeryFirstIndex : iRowFirstTileIndex; var oFirstTile = this._getVisibleTiles()[iTargetTileIndex]; if (oFirstTile) { this._findTile(oFirstTile.$()).trigger("focus"); // event should not trigger any further actions oEvent.stopPropagation(); } this._handleAriaActiveDescendant(); } }, this), fnOnEnd = jQuery.proxy(function(oEvent) { if (this._iCurrentFocusIndex >= 0) { var oTiles = this._getVisibleTiles(); var iRowFirstTileIndex = this._iCurrentFocusIndex - this._iCurrentFocusIndex % this._iMaxTilesX; var iRowLastTileIndex = iRowFirstTileIndex + this._iMaxTilesX < oTiles.length ? iRowFirstTileIndex + this._iMaxTilesX - 1 : oTiles.length - 1; var iLastTileIndex = this._iCurrentTileStartIndex + this._iMaxTiles < oTiles.length ? this._iCurrentTileStartIndex + this._iMaxTiles - 1 : oTiles.length - 1; var iLastOnPageOrVeryLastIndex = iLastTileIndex === this._iCurrentFocusIndex ? oTiles.length - 1 : iLastTileIndex; var iTargetTileIndex = oEvent.ctrlKey ? iLastOnPageOrVeryLastIndex : iRowLastTileIndex; if (oTiles.length > 0) { this._findTile(oTiles[iTargetTileIndex].$()).trigger("focus"); // event should not trigger any further actions oEvent.stopPropagation(); } this._handleAriaActiveDescendant(); } }, this), fnOnPageUp = jQuery.proxy(function(oEvent) { var aTiles = this._getVisibleTiles(); if (aTiles.length > 0) { var iNextIndex = this._iCurrentFocusIndex - this._iMaxTiles >= 0 ? this._iCurrentFocusIndex - this._iMaxTiles : 0; var oNextTile = aTiles[iNextIndex]; if (oNextTile) { this._renderTilesInTheSamePage(iNextIndex, aTiles); this._findTile(oNextTile.$()).trigger("focus"); // event should not trigger any further actions oEvent.stopPropagation(); } this._handleAriaActiveDescendant(); } }, this), fnOnPageDown = jQuery.proxy(function(oEvent) { var aTiles = this._getVisibleTiles(), iTilesCount = aTiles.length; if (iTilesCount > 0) { var iNextIndex = this._iCurrentFocusIndex + this._iMaxTiles < iTilesCount ? this._iCurrentFocusIndex + this._iMaxTiles : iTilesCount - 1; var oNextTile = aTiles[iNextIndex]; if (oNextTile) { this._renderTilesInTheSamePage(iNextIndex, aTiles); this._findTile(oNextTile.$()).trigger("focus"); // event should not trigger any further actions oEvent.stopPropagation(); } this._handleAriaActiveDescendant(); } }, this), fnOnRight = jQuery.proxy(function(oEvent) { if (this._iCurrentFocusIndex >= 0) { var aTiles = this._getVisibleTiles(); var iNextIndex = this._iCurrentFocusIndex + 1 < aTiles.length ? this._iCurrentFocusIndex + 1 : this._iCurrentFocusIndex; if (!oEvent.ctrlKey) { var oNextTile = aTiles[iNextIndex]; if (oNextTile) { if (iNextIndex < this._iCurrentTileStartIndex + this._iMaxTiles) { // tile on same page? this._findTile(oNextTile.$()).trigger("focus"); } else { this._renderTilesInTheSamePage(iNextIndex, aTiles); this.scrollIntoView(oNextTile, true, aTiles); var that = this; setTimeout(function() { that._findTile(oNextTile.$()).trigger("focus"); }, 400); } } } else if (this.getEditable()) { var oTile = aTiles[this._iCurrentFocusIndex]; this.moveTile(oTile, iNextIndex); oTile.$().trigger("focus"); } this._handleAriaActiveDescendant(); // event should not trigger any further actions oEvent.stopPropagation(); } }, this), fnOnLeft = jQuery.proxy(function(oEvent) { if (this._iCurrentFocusIndex >= 0) { var aTiles = this._getVisibleTiles(); var iNextIndex = this._iCurrentFocusIndex - 1 >= 0 ? this._iCurrentFocusIndex - 1 : this._iCurrentFocusIndex; if (!oEvent.ctrlKey) { var oNextTile = aTiles[iNextIndex]; if (oNextTile) { if (iNextIndex >= this._iCurrentTileStartIndex) { // tile on same page? this._findTile(oNextTile.$()).trigger("focus"); } else { this._renderTilesInTheSamePage(iNextIndex, aTiles); this.scrollIntoView(oNextTile, true, aTiles); var that = this; setTimeout(function () { that._findTile(oNextTile.$()).trigger("focus"); }, 400); } } } else if (this.getEditable()) { var oTile = aTiles[this._iCurrentFocusIndex]; this.moveTile(oTile, iNextIndex); oTile.$().trigger("focus"); } this._handleAriaActiveDescendant(); // event should not trigger any further actions oEvent.stopPropagation(); } }, this), fnOnDown = jQuery.proxy(function(oEvent) { var oTiles = this._getVisibleTiles(); if (this._iCurrentFocusIndex >= 0) { var iModCurr = this._iCurrentFocusIndex % this._iMaxTiles, iNextIndex = this._iCurrentFocusIndex + this._iMaxTilesX, iModNext = iNextIndex % this._iMaxTiles; if (!oEvent.ctrlKey) { var oNextTile = oTiles[iNextIndex]; if ((iModNext > iModCurr) && oNextTile) { // '(iModNext > iModCurr)' means: still on same page this._findTile(oNextTile.$()).trigger("focus"); } } else if (this.getEditable()) { var oTile = oTiles[this._iCurrentFocusIndex]; this.moveTile(oTile, iNextIndex); oTile.$().trigger("focus"); } this._handleAriaActiveDescendant(); // event should not trigger any further actions oEvent.stopPropagation(); } }, this), fnOnUp = jQuery.proxy(function(oEvent) { var oTiles = this._getVisibleTiles(); if (this._iCurrentFocusIndex >= 0) { var iModCurr = this._iCurrentFocusIndex % this._iMaxTiles, iNextIndex = this._iCurrentFocusIndex - this._iMaxTilesX, iModNext = iNextIndex % this._iMaxTiles; if (!oEvent.ctrlKey) { var oNextTile = oTiles[iNextIndex]; if ((iModNext < iModCurr) && oNextTile) { // '(iModNext < iModCurr)' means: still on same page this._findTile(oNextTile.$()).trigger("focus"); } } else if (this.getEditable()) { var oTile = oTiles[this._iCurrentFocusIndex]; this.moveTile(oTile, iNextIndex); oTile.$().trigger("focus"); } this._handleAriaActiveDescendant(); // event should not trigger any further actions oEvent.stopPropagation(); } }, this), fnOnDelete = jQuery.proxy(function(oEvent) { var oTiles = this._getVisibleTiles(); if (this._iCurrentFocusIndex >= 0 && this.getEditable()) { var oTile = oTiles[this._iCurrentFocusIndex]; if (oTile.getRemovable()) { this.deleteTile(oTile); oTiles = this._getVisibleTiles(); if (this._iCurrentFocusIndex === oTiles.length) { if (oTiles.length !== 0) { oTiles[this._iCurrentFocusIndex - 1].$().trigger("focus"); } else { this._findNextTabbable().trigger("focus"); } } else { oTiles[this._iCurrentFocusIndex].$().trigger("focus"); } this._handleAriaActiveDescendant(); } oEvent.stopPropagation(); } }, this); this.onsaphome = fnOnHome; this.onsaphomemodifiers = fnOnHome; this.onsapend = fnOnEnd; this.onsapendmodifiers = fnOnEnd; this.onsapright = this._bRtl ? fnOnLeft : fnOnRight; this.onsaprightmodifiers = this._bRtl ? fnOnLeft : fnOnRight; this.onsapleft = this._bRtl ? fnOnRight : fnOnLeft; this.onsapleftmodifiers = this._bRtl ? fnOnRight : fnOnLeft; this.onsapup = fnOnUp; this.onsapupmodifiers = fnOnUp; this.onsapdown = fnOnDown; this.onsapdownmodifiers = fnOnDown; this.onsappageup = fnOnPageUp; this.onsappagedown = fnOnPageDown; this.onsapdelete = fnOnDelete; this.data("sap-ui-fastnavgroup", "true", true); // Define group for F6 handling } if (Device.system.tablet || Device.system.phone) { this._fnOrientationChange = function(oEvent) { if (this.getDomRef()) { this._oTileDimensionCalculator.calc(); //there is not need to call this._update, because resize event will be triggered also, where it is called } }.bind(this); } this._oTileDimensionCalculator = new TileDimensionCalculator(this); this._bRtl = Localization.getRTL(); //Keeps info about the current page and total page count. In addition the old(previous) values of the same are kept. this._oPagesInfo = (function (bRightToLeftMode) { var iCurrentPage, iCount, iOldCurrentPage, iOldCount, bPagerCreated = false, bRtl = bRightToLeftMode; return { /* Zero based index of the current page */ setCurrentPage: function (currentPage) { iOldCurrentPage = iCurrentPage; iCurrentPage = currentPage; }, setCount: function (count) { iOldCount = iCount; iCount = count; }, /*Sets that the pager with dots is created*/ setPagerCreated: function(created) { bPagerCreated = created; }, /*Sets the old values the same as the current*/ syncOldToCurrentValues: function() { iOldCount = iCount; iOldCurrentPage = iCurrentPage; }, reset: function() { iOldCount = undefined; iOldCurrentPage = undefined; iCount = undefined; iCurrentPage = undefined; bPagerCreated = false; }, getCurrentPage: function () { return iCurrentPage; }, getCount: function () { return iCount; }, getOldCurrentPage: function () { return iOldCurrentPage; }, getOldCount: function () { return iOldCount; }, /*If the pager with dots is created*/ isPagerCreated: function() { return bPagerCreated; }, /*Checks if the current page is the last page (considers RTL)*/ currentPageIsLast: function() { return bRtl ? (iCurrentPage === 0) : (iCurrentPage === iCount - 1); }, /*Checks if the current page is the first page (considers RTL)*/ currentPageIsFirst: function() { return bRtl ? (iCurrentPage === iCount - 1) : (iCurrentPage === 0); }, oldCurrentPageIsLast: function() { if (isNaN(iOldCurrentPage)) { return false; } return bRtl ? (iOldCurrentPage === 0) : (iOldCurrentPage === iOldCount - 1); }, oldCurrentPageIsFirst: function() { if (isNaN(iOldCurrentPage)) { return false; } return bRtl ? (iOldCurrentPage === iOldCount - 1) : (iOldCurrentPage === 0); }, /*Is the 'currentPage is last' has changed. Example - it wasn't last before, but now it is and vice versa*/ currentPageIsLastChanged: function() { return this.currentPageIsLast() !== this.oldCurrentPageIsLast(); }, /*Is the 'currentPage is first' has changed. Example - it wasn't first before, but now it is and vice versa*/ currentPageIsFirstChanged: function() { return this.currentPageIsFirst() !== this.oldCurrentPageIsFirst(); }, /* true if current page's relative position is changed - the page becomes first, last or was first or last and now it is not*/ currentPageRelativePositionChanged: function() { return this.currentPageIsFirstChanged() || this.currentPageIsLastChanged(); }, pageCountChanged: function() { return iCount !== iOldCount; }, currentPageChanged: function() { return iCurrentPage !== iOldCurrentPage; } }; }(this._bRtl)); //make sure we start from starting meaningful, otherwise we may not have right value unless height is given. this._iMaxTiles = 1; }; /** * Finds the next tabbable element after the TileContainer. * @returns {Element} The next tabbable element after the tile container * @private */ TileContainer.prototype._findNextTabbable = function() { var $Ref = this.$(); var $Tabbables = jQuery.merge( jQuery.merge($Ref.nextAll(), $Ref.parents().nextAll()).find(':sapTabbable').addBack(':sapTabbable'), jQuery.merge($Ref.parents().prevAll(), $Ref.prevAll()).find(':sapTabbable').addBack(':sapTabbable') ); return $Tabbables.first(); }; /** * Handles the internal event onBeforeRendering. * * @private */ TileContainer.prototype.onBeforeRendering = function () { var aTiles = this.getTiles(), iTilesCount = aTiles.length; // unregister the resize listener if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } this._oPagesInfo.reset(); for (var i = 0; i < iTilesCount; i++) { aTiles[i]._rendered = false; } }; /** * Handles the internal event onAfterRendering. * * @private */ TileContainer.prototype.onAfterRendering = function() { var aVisibleTiles = []; // init resizing this._sResizeListenerId = ResizeHandler.register(this.getDomRef().parentElement, jQuery.proxy(this._resize, this)); // init the dimensions to the container scoll area this._oDim = this._calculateDimension(); this._applyDimension(); this.$().toggleClass("sapMTCEditable",this.getEditable() === true); if (this._bRenderFirstPage) { //Set by the TileContainerRenderer if it cannot determine the size of the tiles per page this._bRenderFirstPage = false; aVisibleTiles = this._getVisibleTiles(); this._updateTileDimensionInfoAndPageSize(aVisibleTiles); if (this.getTiles().length === 1) { // in case of only one tile, it was rendered // but still needs it's position and visibility to be updated this._update(false, aVisibleTiles); } else if (this._iMaxTiles !== Infinity && this._iMaxTiles ) { this._renderTiles(aVisibleTiles, 0, this._iMaxTiles - 1); } } else { this._update(true); } if (Device.system.desktop || Device.system.combi) { var aTiles = aVisibleTiles || this._getVisibleTiles(); if (aTiles.length > 0 && this._mFocusables && this._mFocusables[aTiles[0].getId()]) { this._mFocusables[aTiles[0].getId()].eq(0).attr('tabindex', '0'); } } if (Device.system.tablet || Device.system.phone) { Device.orientation.attachHandler(this._fnOrientationChange, this); } }; /** * Sets the editable property to the TileContainer, allowing to move icons. * This is currently also set with a long tap. * * @param {boolean} bValue Whether the container is in edit mode or not * @returns {this} this pointer for chaining * @public */ TileContainer.prototype.setEditable = function(bValue) { var aTiles = this._getVisibleTiles(); // set the property this.setProperty("editable", bValue, true); var bEditable = this.getEditable(); this.$().toggleClass("sapMTCEditable", bEditable); for (var i = 0;i < aTiles.length; i++) { var oTile = aTiles[i]; if (oTile.isA("sap.m.Tile")) { oTile.isEditable(bEditable); } } return this; // allow chaining; }; /** * Called whenever the model is updated * * @private */ TileContainer.prototype.updateTiles = function () { this.destroyTiles(); this.updateAggregation('tiles'); }; /** * Applies the container's dimensions. * * @private */ TileContainer.prototype._applyDimension = function() { var oDim = this._getDimension(), $this = this.$(), oThisPos, iOffset = 10, $scroll = this.$("scrl"), scrollPos, scrollOuterHeight, pagerHeight = this.$("pager").outerHeight(); $scroll.css({ width : oDim.outerwidth + "px", height : (oDim.outerheight - pagerHeight) + "px" }); oThisPos = $this.position(); scrollPos = $scroll.position(); scrollOuterHeight = $scroll.outerHeight(); if (Device.system.phone) { iOffset = 2; } else if (Device.system.desktop) { iOffset = 0; } this.$("blind").css({ top : (scrollPos.top + iOffset) + "px", left : (scrollPos.left + iOffset) + "px", right: "auto", width : ($scroll.outerWidth() - iOffset) + "px", height : (scrollOuterHeight - iOffset) + "px" }); this.$("rightedge").css({ top : (oThisPos.top + iOffset) + "px", right : iOffset + "px", left : "auto", height : (scrollOuterHeight - iOffset) + "px" }); this.$("leftedge").css({ top : (oThisPos.top + iOffset) + "px", left : (oThisPos.left + iOffset) + "px", right: "auto", height : (scrollOuterHeight - iOffset) + "px" }); }; /** * Handles the resize event for the TileContainer. * Called whenever the orientation of browser size changes. * * @private */ TileContainer.prototype._resize = function() { if (this._oDragSession) { return; } setTimeout(jQuery.proxy(function() { var aVisibleTiles = this._getVisibleTiles(), iTilesCount = aVisibleTiles.length, iCurrentPageStartTileIndex = this._iCurrentTileStartIndex, oOldDim = this._oDim, iNewPage, iNewPageTileStartIndex, iNewPageTileEndIndex; this._oPagesInfo.reset(); this._oDim = this._calculateDimension(); this._updateTileDimensionInfoAndPageSize(aVisibleTiles); if (oOldDim.width !== this._oDim.width || oOldDim.height !== this._oDim.height) { //remove all previously rendered tiles(should be a few pages) // in order to make sure the don't interfere with the new for (var i = 0; i < iTilesCount; i++) { if (aVisibleTiles[i]._rendered) { aVisibleTiles[i]._rendered = false; aVisibleTiles[i].$().remove(); } } iNewPage = this._getPageNumberForTile(iCurrentPageStartTileIndex); iNewPageTileStartIndex = iNewPage * this._iMaxTiles; iNewPageTileEndIndex = iNewPageTileStartIndex + this._iMaxTiles - 1; this._renderTiles(aVisibleTiles, iNewPageTileStartIndex, iNewPageTileEndIndex); } },this), 0); }; /** * Called from parent if the control is destroyed. * * @private */ TileContainer.prototype.exit = function() { if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } if (Device.system.tablet || Device.system.phone) { Device.orientation.detachHandler(this._fnOrientationChange, this); } delete this._oPagesInfo; }; /** * Updates all Tiles. * @param {boolean} bAnimated to apply animation during update * @param {sap.m.Tile[]} [aVisibleTiles] optional list of visible tiles in order to avoid filtering them again. * @return {void} * @private */ TileContainer.prototype._update = function(bAnimated, aVisibleTiles) { if (!this.getDomRef()) { return; } if (!this.getVisible()) { return; } aVisibleTiles = aVisibleTiles || this._getVisibleTiles(); this._oTileDimensionCalculator.calc(aVisibleTiles); this._updateTilePositions(aVisibleTiles); if (!this._oDragSession) { this.scrollIntoView(this._iCurrentTileStartIndex || 0, bAnimated, aVisibleTiles); } }; /** * Returns the index of the first Tile visible in the current page. * * @returns {int} The index of the first Tile that is visible in the current page * @public */ TileContainer.prototype.getPageFirstTileIndex = function() { return this._iCurrentTileStartIndex || 0; }; /** * Moves a given Tile to the given index. * * @param {sap.m.Tile} vTile The tile to move * @param {int} iNewIndex The new Tile position in the tiles aggregation * @returns {this} this pointer for chaining * @public */ TileContainer.prototype.moveTile = function(vTile, iNewIndex) { if (!isNaN(vTile)) { vTile = this._getVisibleTiles()[vTile]; } if (!vTile) { Log.info("No Tile to move"); return this; } this.deleteTile(vTile); this.insertTile(vTile, iNewIndex); return this; }; /** * Adds a Tile to the end of the tiles collection. * * @param {sap.m.Tile} oTile The tile to add * @returns {this} this pointer for chaining * @override * @public */ TileContainer.prototype.addTile = function(oTile) { this.insertTile(oTile,this.getTiles().length); return this; }; /** * Inserts a Tile to the given index. * * @param {sap.m.Tile} oTile The Tile to insert * @param {int} iIndex The new Tile position in the tiles aggregation * @returns {this} this pointer for chaining * @override * @public */ TileContainer.prototype.insertTile = function(oTile, iIndex) { var that = this, aVisibleTiles; oTile.isEditable(this.getEditable()); // keyboard support for desktop environments if (Device.system.desktop || Device.system.combi) { oTile.addEventDelegate({ "onAfterRendering": function() { if (!that._mFocusables) { that._mFocusables = {}; } that._mFocusables[this.getId()] = this.$().find("[tabindex!='-1']").addBack().filter(that._isFocusable); that._mFocusables[this.getId()].attr('tabindex', '-1'); } }, oTile); var fnOnFocusIn = function(oEvent) { var iIndex = that._getVisibleTiles().indexOf(this), iExpectedPage = Math.floor(iIndex / that._iMaxTiles), iPageDelta = iExpectedPage - that._oPagesInfo.getCurrentPage(); var iPreviousTileIndex = that._iCurrentFocusIndex >= 0 ? that._iCurrentFocusIndex : 0; var aVTiles = that._getVisibleTiles(); var oPrevTile = aVTiles[iPreviousTileIndex]; if (oPrevTile) { that._mFocusables[oPrevTile.getId()].attr("tabindex", "-1"); that._mFocusables[this.getId()].attr("tabindex", "0"); } if (iPageDelta != 0) { that.scrollIntoView(iIndex, null, aVTiles); } that._handleAriaActiveDescendant(); that._iCurrentFocusIndex = iIndex; }; oTile.addEventDelegate({ "onfocusin": fnOnFocusIn }, oTile); } if (this.getDomRef()) { this.insertAggregation("tiles", oTile, iIndex, true); aVisibleTiles = this._getVisibleTiles(); if (!this._oDragSession) { //Render the tiles and reposition the rest if the tile is visible and inserted at position that needs other tiles repositioning. // Ex. 12 tiles, 3 pages x 4 tiles, current page is 2, tile inserted at index 0 (page 1) - should render. if (oTile.getVisible() && (aVisibleTiles.length === 1 || this._getPageNumberForTile(iIndex) <= this._oPagesInfo.getCurrentPage())) { this._renderTile(oTile, iIndex); this._update(false, aVisibleTiles);//updates also the page's count } else {//we just need to update the pager this._oPagesInfo.setCount(Math.ceil(aVisibleTiles.length / this._iMaxTiles)); this._updatePager(); } } else { this._update(false, aVisibleTiles); } // When the control is initialized/updated with data binding and optimization for rendering // tile by tile is used we need to be sure we have a focusable tile. if (Device.system.desktop || Device.system.combi) { this._updateTilesTabIndex(aVisibleTiles); } } else { this.insertAggregation("tiles",oTile,iIndex); aVisibleTiles = this._getVisibleTiles(); } if (oTile.getVisible()) { handleAriaPositionInSet.call(this, iIndex, aVisibleTiles.length, aVisibleTiles); handleAriaSize.call(this, aVisibleTiles); } return this; }; /** * Updates the tab index of the Tiles. * If there is no focusable Tile (for example, tabindex = 0), updates the first tile. * @private */ TileContainer.prototype._updateTilesTabIndex = function (aVisibleTiles) { aVisibleTiles = aVisibleTiles || this._getVisibleTiles(); if (aVisibleTiles.length && aVisibleTiles.length > 0) { for (var i = 0; i < aVisibleTiles.length; i++) { if (aVisibleTiles[i].$().attr("tabindex") === "0") { return; } } } aVisibleTiles[0].$().attr("tabindex", "0"); }; /** * Checks if a DOM element is focusable. * To be used within jQuery.filter function. * @param {int} index Index of the element within an array * @param {Element} element DOM element to check * @returns {boolean} If a DOM element is focusable * @private */ TileContainer.prototype._isFocusable = function(index, element) { var isTabIndexNotNaN = !isNaN(jQuery(element).attr("tabindex")); var nodeName = element.nodeName.toLowerCase(); if ( nodeName === "area" ) { var map = element.parentNode, mapName = map.name, img; if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { return false; } img = jQuery( "img[usemap='#" + mapName + "']" )[0]; return !!img; } /*eslint-disable no-nested-ternary */ return ( /input|select|textarea|button|object/.test( nodeName ) ? !element.disabled : nodeName == "a" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN); /*eslint-enable no-nested-ternary */ }; /** * Deletes a Tile. * * @param {sap.m.Tile} oTile The tile to move * @returns {this} this pointer for chaining * @override * @public */ TileContainer.prototype.deleteTile = function(oTile) { var aVisibleTiles = this._getVisibleTiles(), iDeletedTileIndex = this._indexOfVisibleTile(oTile, aVisibleTiles); if (this.getDomRef()) { aVisibleTiles.splice(iDeletedTileIndex, 1); this.removeAggregation("tiles",oTile,true); if (!this._oDragSession) { if (oTile.getDomRef()) { oTile.getDomRef().parentNode.removeChild(oTile.getDomRef()); } if (Device.system.desktop || Device.system.combi) { if (this._mFocusables && this._mFocusables[oTile.getId()]) { delete this._mFocusables[oTile.getId()]; } } } if (aVisibleTiles.length === 0) { this._oPagesInfo.reset(); } else if (oTile.getVisible() && iDeletedTileIndex >= 0 && this._getPageNumberForTile(iDeletedTileIndex) <= this._oPagesInfo.getCurrentPage()) { this._renderTilesInTheSamePage(this._oPagesInfo.getCurrentPage() * this._iMaxTiles, aVisibleTiles); } this._update(false); } else { this.removeAggregation("tiles",oTile,false); aVisibleTiles = this._getVisibleTiles(); } handleAriaPositionInSet.call(this, iDeletedTileIndex, aVisibleTiles.length); handleAriaSize.call(this, aVisibleTiles); return this; }; TileContainer.prototype.removeTile = TileContainer.prototype.deleteTile; TileContainer.prototype.removeAllTiles = function() { var iTileCount = this.getTiles().length - 1; //Zero based index for (var iIndex = iTileCount; iIndex >= 0; iIndex--) { var oTile = this.getTiles()[iIndex]; this.deleteTile(oTile); } return this; }; TileContainer.prototype.destroyTiles = function(){ if (this.getDomRef()) { var aTiles = this.getTiles(); this.removeAllAggregation("tiles", true); this._oPagesInfo.reset(); this._update(); for (var i = 0;i < aTiles.length; i++) { var tile = aTiles[i]; tile.destroy(); } } else { this.destroyAggregation("tiles", false); } return this; }; TileContainer.prototype.invalidate = function() { if (!this._oDragSession || this._oDragSession.bDropped) { Control.prototype.invalidate.apply(this); } }; /** * Scrolls one page to the left. * * @public */ TileContainer.prototype.scrollLeft = function() { var iScrollToIndex = 0, aVisibleTiles = this._getVisibleTiles(); if (this._bRtl) { iScrollToIndex = this._iCurrentTileStartIndex + this._iMaxTiles; } else { iScrollToIndex = this._iCurrentTileStartIndex - this._iMaxTiles; } this._renderTiles(aVisibleTiles, iScrollToIndex, iScrollToIndex + this._iMaxTiles - 1); this.scrollIntoView(iScrollToIndex, null, aVisibleTiles); }; /** * Scrolls one page to the right. * * @public */ TileContainer.prototype.scrollRight = function() { var iScrollToIndex = 0, aVisibleTiles = this._getVisibleTiles(); if (this._bRtl) { iScrollToIndex = this._iCurrentTileStartIndex - this._iMaxTiles; } else { iScrollToIndex = this._iCurrentTileStartIndex + this._iMaxTiles; } this._renderTiles(aVisibleTiles, iScrollToIndex, iScrollToIndex + this._iMaxTiles - 1); this.scrollIntoView(iScrollToIndex, null, aVisibleTiles); }; /** * Renders all tiles (if not rendered yet) that share the same page as the given tile * @param {int} tileIndex the given tile whose page of tiles should be rendered * @param {sap.m.Tile[]} tiles tiles to check against * @private * @returns {void} */ TileContainer.prototype._renderTilesInTheSamePage = function(tileIndex, tiles) { var iTilePage = this._getPageNumberForTile(tileIndex), iFirstTileInPage = iTilePage * this._iMaxTiles, iLastTileInPage = iFirstTileInPage + this._iMaxTiles - 1; this._renderTiles(tiles, iFirstTileInPage, iLastTileInPage); }; /** * Renders any tile in given range if it is not rendered yet. * @param {sap.m.Tile[]} tiles tiles list * @param {int} startIndex start position of a tile in the given tiles list * @param {int} endIndex end position (inclusive) of a tile in the given tiles list * @private * @returns {void} */ TileContainer.prototype._renderTiles = function(tiles, startIndex, endIndex) { var bNewTilesRendered = false, i; for (i = startIndex; i <= endIndex; i++) { if (tiles[i] && !tiles[i]._rendered) { this._renderTile(tiles[i], i); bNewTilesRendered = true; } } if (bNewTilesRendered) { this._update(false, tiles); // When the control is initialized/updated with data binding and optimization for rendering // tile by tile is used we need to be sure we have a focusable tile. if (Device.system.desktop || Device.system.combi) { this._updateTilesTabIndex(); } } }; /** * Scrolls to the page where the given Tile or tile index is included. * Optionally this can be done animated or not. With IE9 the scroll is never animated. * * @param {sap.m.Tile|int} vTile The Tile or tile index to be scrolled into view * @param {boolean} bAnimated Whether the scroll should be animated * @param {sap.m.Tile[]} [aVisibleTiles] optional list of visible tiles in order to avoid filtering them again. * @public */ TileContainer.prototype.scrollIntoView = function(vTile, bAnimated, aVisibleTiles) { var iContentWidth = this._getContentDimension().outerwidth, iIndex = vTile, aAllTiles = this.getTiles(); if (isNaN(vTile)) { iIndex = this.indexOfAggregation("tiles",vTile); } if (!aAllTiles[iIndex] || !aAllTiles[iIndex].getVisible()) { return; } aVisibleTiles = aVisibleTiles || this._getVisibleTiles(); iIndex = this._indexOfVisibleTile(aAllTiles[iIndex]);//find tile's index amongst visible tiles if (iIndex > -1) { this._renderTilesInTheSamePage(iIndex, aVisibleTiles); } this._applyPageStartIndex(iIndex, aVisibleTiles); this._oPagesInfo.setCurrentPage(Math.floor(this._iCurrentTileStartIndex / this._iMaxTiles)); if (this._bRtl) { this._scrollTo((this._oPagesInfo.getCount() - this._oPagesInfo.getCurrentPage()) * iContentWidth, bAnimated); } else { this._scrollTo(this._oPagesInfo.getCurrentPage() * iContentWidth, bAnimated); } this._updatePager(); }; /** * Updates the tile positions only of the rendered tiles. * Tile property _rendered is set inside Tile.js onAfterRendering. * * @private */ TileContainer.prototype._updateTilePositions = function(aVisibleTiles){ var oDim = this._getDimension(); if (oDim.height === 0) {// nothing to do because the height of the content is not (yet) available return; } aVisibleTiles = aVisibleTiles || this._getVisibleTiles(); if (aVisibleTiles.length === 0) { // no tiles this._oPagesInfo.setCount(0); // no tiles no pages this._updatePager(); return; } this._applyPageStartIndex(this._iCurrentTileStartIndex, aVisibleTiles); this._applyDimension(); var oContentDimension = this._getContentDimension(); this._oPagesInfo.setCount(Math.ceil(aVisibleTiles.length / this._iMaxTiles)); var oTileDimension = this._oTileDimensionCalculator.getLastCalculatedDimension(); for (var i = 0; i < aVisibleTiles.length; i++) { if (!aVisibleTiles[i]._rendered || aVisibleTiles[i].isDragged()) { continue; } var iPage = Math.floor(i / this._iMaxTiles), oTile = aVisibleTiles[i], iLeft = (iPage * oContentDimension.outerwidth) + this._iOffsetX + i % this._iMaxTilesX * oTileDimension.width, iTop = this._iOffsetY + Math.floor(i / this._iMaxTilesX) * oTileDimension.height - (iPage * this._iMaxTilesY * oTileDimension.height); if (this._bRtl) { iLeft = (this._oPagesInfo.getCount() - iPage) * oContentDimension.outerwidth - this._iOffsetX - (i % this._iMaxTilesX + 1) * oTileDimension.width; } oTile.setPos(iLeft,iTop); oTile.setSize(oTileDimension.width, oTileDimension.height); } }; /** * Finds a Tile. * Convenience method, which returns $node if it has Css class sapMTile * or the first child with that class. * @param {jQuery.object} $node The node to be examined * @returns {jQuery.object} The first node which has the class * @private */ TileContainer.prototype._findTile = function($node) { if ($node.hasClass('sapMTile') || $node.hasClass('sapMCustomTile')) { return $node; } else { // return $node.find('.sapMTile'); return $node.find('.sapMTile') || $node.find('.sapMCustomTile'); } }; /** * Updates the pager part of the TileContainer. * This is done dynamically. * * @private */ TileContainer.prototype._updatePager = function() { var oPager, oScrollLeft, oScrollRight, aHTML, /* true if the pager is created as part of this function*/ bPagerJustCreated = false; if (!this._oPagesInfo.pageCountChanged() && !this._oPagesInfo.currentPageChanged()) { return; } oPager = this.$("pager")[0]; oScrollLeft = this.$("leftscroller")[0]; oScrollRight = this.$("rightscroller")[0]; if (this._oPagesInfo.getCount() == undefined || this._oPagesInfo.getCount() <= 1) { //reset pager if there is no need of it oPager.innerHTML = ""; oScrollRight.style.right = "-100px"; oScrollLeft.style.left = "-100px"; oScrollLeft.style.display = "none"; oScrollRight.style.display = "none"; this._oPagesInfo.setPagerCreated(false); return; } if (!this._oPagesInfo.isPagerCreated()) { aHTML = [""]; for (var i = 0; i < this._oPagesInfo.getCount(); i++) { aHTML.push(""); } oPager.innerHTML = aHTML.join("<span></span>"); oPager.style.display = "block"; oPager.childNodes[0].className = "sapMTCActive"; //initially active page is the 1st(span) this._oPagesInfo.setPagerCreated(true); bPagerJustCreated = true; } else if (this._oPagesInfo.pageCountChanged()) { if (this._oPagesInfo.getCount() - this._oPagesInfo.getOldCount() < 0) {//one page less oPager.removeChild(oPager.lastChild); } else { oPager.appendChild(document.createElement("span")); //one page more } } if (this._oPagesInfo.currentPageChanged()) { oPager.childNodes[this._oPagesInfo.getCurrentPage()].className = "sapMTCActive"; if (oPager.childNodes[this._oPagesInfo.getOldCurrentPage()]) { oPager.childNodes[this._oPagesInfo.getOldCurrentPage()].className = ""; } if (this._oPagesInfo.getCurrentPage() >= 1) { //deactivate the initially active page (span) oPager.childNodes[0].className = ""; } } if (Device.system.desktop && (bPagerJustCreated || this._oPagesInfo.currentPageRelativePositionChanged())) { if (this._bRtl) { // Less builder swaps left and right in RTL styles, // and that is not required here, otherwise left scroller will go right and vice versa. oScrollRight.style.left = "auto"; oScrollLeft.style.right = "auto"; } oScrollRight.style.right = this._oPagesInfo.currentPageIsLast() ? "-100px" : "1rem"; oScrollLeft.style.left = this._oPagesInfo.currentPageIsFirst() ? "-100px" : "1rem"; oScrollRight.style.display = this._oPagesInfo.currentPageIsLast() ? "none" : "block"; oScrollLeft.style.display = this._oPagesInfo.currentPageIsFirst() ? "none" : "block"; } this._oPagesInfo.syncOldToCurrentValues(); }; /** * Returns the dimension (width and height) of the pages content. * * @returns {object} Width and height of the pages content * @private */ TileContainer.prototype._getContentDimension = function() { if (!this.getDomRef()) { return; } var oScroll = this.$("scrl"); return { width : oScroll.width(), height : oScroll.height() - 20, outerheight : oScroll.outerHeight() - 20, outerwidth : oScroll.outerWidth() }; }; /** * Returns the dimension (width and height) of the TileContainer content. * * @returns {{width, height, outerheight, outerwidth}|{width, height: *, outerheight: *, outerwidth: *}|*|null} * Width and height of the pages content * @private */ TileContainer.prototype._getDimension = function() { if (!this._oDim) { this._oDim = this._calculateDimension(); } return this._oDim; }; /** * Calculates the Tile page sizes, i.e. how many tiles per X, Y and page can be rendered * * @private */ TileContainer.prototype._calculatePageSize = function(aVisibleTiles) { var oDim, iTiles; aVisibleTiles = aVisibleTiles || this._getVisibleTiles(); iTiles = aVisibleTiles.length; if (iTiles === 0) {// no tiles return; } oDim = jQuery.extend({}, this._getDimension()); if (oDim.height === 0) { // nothing to do because the height of the content is not (yet) available return; } if (Device.system.desktop) { oDim.width -= 45 * 2; } var oTileDimension = this._oTileDimensionCalculator.getLastCalculatedDimension(), iPagerHeight = this.$("pager")[0].offsetHeight, iMaxTilesX = Math.max( Math.floor( oDim.width / oTileDimension.width ),1), //at least one tile needs to be visible iMaxTilesY = Math.max( Math.floor((oDim.height - iPagerHeight) / oTileDimension.height),1), //at least one tile needs to be visible iNumTileX = (iTiles < iMaxTilesX) ? iTiles : iMaxTilesX, iNumTileY = (iTiles / iNumTileX < iMaxTilesY) ? Math.ceil(iTiles / iNumTileX) : iMaxTilesY; // set the member vars for further usage this._iMaxTiles = iMaxTilesX * iMaxTilesY; this._iMaxTilesX = iMaxTilesX; this._iMaxTilesY = iMaxTilesY; this._iOffsetX = Math.floor(( oDim.width - (oTileDimension.width * iNumTileX)) / 2); if (Device.system.desktop) { this._iOffsetX += 45; } this._iOffsetY = Math.floor(( oDim.height - iPagerHeight - (oTileDimension.height * iNumTileY )) / 2); }; /** * Gets Tiles from a given position. * Returns an array for a given pixel position in the TileContainer. * Normally, there is only one Tile for a position. * * @param {int} iX Position in px * @param {int} iY Position in px * @returns {array} Array of Tiles for the given position * @private */ TileContainer.prototype._getTilesFromPosition = function(iX, iY) { if (!this._getVisibleTiles().length) { return []; } iX = iX + this._iScrollLeft; var aTiles = this._getVisibleTiles(), aResult = []; for (var i = 0;i < aTiles.length;i++) { var oTile = aTiles[i], oRect = { top: oTile._posY, left: oTile._posX, width: oTile._width, height: oTile._height }; if (!aTiles[i].isDragged() && iY > oRect.top && iY < oRect.top + oRect.height && iX > oRect.left && iX < oRect.left + oRect.width) { aResult.push(aTiles[i]); } } return aResult; }; /** * Applies the start index of the pages' first Tile according to the given index. * * @param {int} iIndex The index of the tile that should be visible * @param {sap.m.Tile[]} [aVisibleTiles] optional list of visible tiles in order to avoid filtering them again. * @private */ TileContainer.prototype._applyPageStartIndex = function (iIndex, aVisibleTiles) { var oContentDimension = this._getDimension(); if (oContentDimension.height === 0) { // nothing to do because the height of the content is not (yet) available return; } aVisibleTiles = aVisibleTiles || this._getVisibleTiles(); this._calculatePageSize(aVisibleTiles); var iLength = aVisibleTiles.length; if (iIndex < 0) { iIndex = 0; } else if (iIndex > iLength - 1) { iIndex = iLength - 1; } // where does the page start var iCurrentPage = Math.floor(iIndex / this._iMaxTiles || 0); this._iCurrentTileStartIndex = iCurrentPage * (this._iMaxTiles || 0); Log.info("current index " + this._iCurrentTileStartIndex); }; /** * Scrolls to the given position. * * @param {int} iScrollLeft The new scroll position * @param {boolean} bAnimated Whether the scroll is animated * @private */ TileContainer.prototype._scrollTo = function(iScrollLeft, bAnimated) { if (bAnimated !== false) { bAnimated = true; // animated needs to be set explicitly to false } this._applyTranslate(this.$("cnt"), -iScrollLeft, 0, bAnimated); if (this._bRtl) { this._iScrollLeft = iScrollLeft - this._getContentDimension().outerwidth; } else { this._iScrollLeft = iScrollLeft; } }; /** * Applies the translate x and y to the given jQuery object. * * @param {object} o$ The jQuery object * @param {int} iX The px x value for the translate * @param {int} iY The px y value for the translate * @param {boolean} bAnimated Whether the translate should be animated or not * @private */ TileContainer.prototype._applyTranslate = function(o$, iX, iY, bAnimated) { var o = o$[0]; this.$("cnt").toggleClass("sapMTCAnim",bAnimated); if ("webkitTransform" in o.style) { o$.css('-webkit-transform','translate3d(' + iX + 'px,' + iY + 'px,0)'); } else if ("MozTransform" in o.style) { o$.css('-moz-transform','translate(' + iX + 'px,' + iY + 'px)'); } else if ("transform" in o.style) { o$.css('transform','translate3d(' + iX + 'px,' + iY + 'px,0)'); } else if ("msTransform" in o.style) { o$.css('-ms-transform','translate(' + iX + 'px,' + iY + 'px)'); } }; /** * Initializes the touch session for the TileContainer. * * @param {jQuery.Event} oEvent The event object that started the touch * @private */ TileContainer.prototype._initTouchSession = function(oEvent) { if (oEvent.type == "touchstart") { var targetTouches = oEvent.targetTouches[0]; this._oTouchSession = { dStartTime : new Date(), fStartX : targetTouches.pageX, fStartY : targetTouches.pageY, fDiffX : 0, fDiffY : 0, oControl : oEvent.srcControl, iOffsetX : targetTouches.pageX - oEvent.target.offsetLeft }; } else { // mousedown this._oTouchSession = { dStartTime : new Date(), fStartX : oEvent.pageX, fStartY : oEvent.pageY, fDiffX : 0, fDiffY : 0, oControl : oEvent.srcControl, iOffsetX : oEvent.pageX - oEvent.target.offsetLeft }; } }; /** * Initializes the drag session for the TileContainer. * * @param {jQuery.Event} oEvent The event object that started the drag * @private */ TileContainer.prototype._initDragSession = function(oEvent) { while (oEvent.srcControl && oEvent.srcControl.getParent() != this) { oEvent.srcControl = oEvent.srcControl.getParent(); } var iIndex = this.indexOfAggregation("tiles",oEvent.srcControl); if (oEvent.type == "touchstart") { this._oDragSession = { oTile : oEvent.srcControl, oTileElement : oEvent.srcControl.$()[0], iOffsetLeft : oEvent.targetTouches[0].pageX - oEvent.srcControl._posX + this._iScrollLeft, iOffsetTop : oEvent.targetTouches[0].pageY - oEvent.srcControl._posY, iIndex : iIndex, iOldIndex : iIndex, iDiffX : oEvent.targetTouches[0].pageX, iDiffY : oEvent.targetTouches[0].pageY }; } else { // mousedown this._oDragSession = { oTile : oEvent.srcControl, oTileElement : oEvent.srcControl.$()[0], iOffsetLeft : oEvent.pageX - oEvent.srcControl._posX + this._iScrollLeft, iOffsetTop : oEvent.pageY - oEvent.srcControl._posY, iIndex : iIndex, iOldIndex : iIndex, iDiffX : oEvent.pageX, iDiffY : oEvent.pageY }; } }; /** * Handles click events for scrollers on desktop. * * @param {jQuery.Event} oEvent The event object that started the drag * @private */ TileContainer.prototype.onclick = function(oEvent) { var oPager = this.$("pager")[0]; if (oEvent.target.id == this.getId() + "-leftscroller" || oEvent.target.parentNode.id == this.getId() + "-leftscroller") { this.scrollLeft(); } else if (oEvent.target.id == this.getId() + "-rightscroller" || oEvent.target.parentNode.id == this.getId() + "-rightscroller") { this.scrollRight(); } else if (oEvent.target == oPager && Device.system.desktop) { if (oEvent.offsetX < oPager.offsetWidth / 2) { this.scrollLeft(); } else { this.scrollRight(); } } }; /** * Handles the touchstart event on the TileContainer. * * @param {jQuery.Event} oEvent The event object * @private */ TileContainer.prototype.ontouchstart = function(oEvent) { // mark the event for components that needs to know if the event was handled by this control. oEvent.setMarked(); if (oEvent.targetTouches.length > 1 || this._oTouchSession) { // allow only one touch session return; } while (oEvent.srcControl && oEvent.srcControl.getParent() != this) { oEvent.srcControl = oEvent.srcControl.getParent(); } if (oEvent.srcControl && oEvent.srcControl.isA("sap.m.Tile") && this.getEditable()) { if (oEvent.target.className != "sapMTCRemove") { this._initDragSession(oEvent); this._initTouchSession(oEvent); this._oDragSession.oTile.isDragged(true); } else { this._initTouchSession(oEvent); } this._bAvoidChildTapEve