UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,457 lines (1,281 loc) 46.9 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([ 'sap/ui/core/Control', 'sap/ui/core/Element', 'sap/ui/core/IconPool', 'sap/ui/core/delegate/ItemNavigation', 'sap/ui/base/ManagedObject', 'sap/ui/core/delegate/ScrollEnablement', './AccButton', './TabStripItem', 'sap/m/Select', 'sap/m/SelectList', 'sap/ui/Device', 'sap/ui/core/Renderer', 'sap/ui/core/ResizeHandler', 'sap/m/library', 'sap/ui/core/Icon', 'sap/m/Image', 'sap/m/SelectRenderer', 'sap/m/SelectListRenderer', './TabStripRenderer', "sap/base/Log", "sap/ui/thirdparty/jquery", "sap/ui/events/KeyCodes", "sap/ui/core/Configuration", "sap/ui/dom/jquery/scrollLeftRTL" // jQuery Plugin "scrollLeftRTL" ], function( Control, Element, IconPool, ItemNavigation, ManagedObject, ScrollEnablement, AccButton, TabStripItem, Select, SelectList, Device, Renderer, ResizeHandler, library, Icon, Image, SelectRenderer, SelectListRenderer, TabStripRenderer, Log, jQuery, KeyCodes, Configuration ) { "use strict"; // shortcut for sap.m.SelectType var SelectType = library.SelectType; // shortcut for sap.m.ButtonType var ButtonType = library.ButtonType; /** * Constructor for a new <code>TabStrip</code>. * * @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 * This control displays a number of tabs. If the available horizontal * space is exceeded, a horizontal scrollbar appears. * * @extends sap.ui.core.Control * @version 1.117.4 * * @constructor * @private * @since 1.34 * @alias sap.m.TabStrip */ var TabStrip = Control.extend("sap.m.TabStrip", /** @lends sap.m.TabStrip.prototype */ { metadata : { library : "sap.m", properties : { /** * Defines whether the button <code>Opened Tabs</code> for showing all the tabs in a dropdown menu is present. */ hasSelect : {type : "boolean", group : "Misc", defaultValue : false} }, aggregations : { /** * The tabs displayed in the <code>TabStrip</code>. */ items : {type : "sap.m.TabStripItem", multiple : true, singularName : "item"}, /** * The <code>Add New Tab</code> button displayed in the <code>TabStrip</code>. */ addButton : {type : "sap.m.Button", multiple : false, singularName : "addButton"}, /** * Holds the instance of the select when <code>hasSelect</code> is set to <code>true</code>. */ _select : {type: 'sap.m.Select', multiple : false, visibility : "hidden"}, /** * Holds the right arrow scrolling button. */ _rightArrowButton : {type: 'sap.m.AccButton', multiple : false, visibility : "hidden"}, /** * Holds the right arrow scrolling button. */ _leftArrowButton : {type: 'sap.m.AccButton', multiple : false, visibility : "hidden"} }, associations: { /** * Sets or retrieves the selected item from the <code>items</code> aggregation.. */ selectedItem: {type : 'sap.m.TabStripItem', group : "Misc"} }, events : { /** * Fired when an item is closed. */ itemClose: { allowPreventDefault: true, parameters: { /** * The closed item. */ item: {type: "sap.m.TabStripItem"} } }, /** * Fired when an item is pressed. */ itemPress: { parameters: { /** * The pressed item. */ item: { type: "sap.m.TabStripItem" } } }, /** * Fired when an item is pressed. * @since 1.38 */ itemSelect: { allowPreventDefault: true, parameters: { /** * The selected item. */ item: { type: "sap.m.TabContainerItem" } } } } }, constructor : function (vId, mSettings) { var bHasSelect = false; // normalize the expected arguments if (!mSettings && typeof vId === 'object') { mSettings = vId; } /* Stash the 'hasSelect' setting for later in order to have all items added to the tabstrip * before the "select" control is instantiated. */ if (mSettings) { bHasSelect = mSettings['hasSelect']; delete mSettings['hasSelect']; } ManagedObject.prototype.constructor.apply(this, arguments); // after the tabstrip is instantiated, add the select this.setProperty('hasSelect', bHasSelect, true); }, renderer: TabStripRenderer }); /** * Library internationalization resource bundle. * * @type {module:sap/base/i18n/ResourceBundle} */ var oRb = sap.ui.getCore().getLibraryResourceBundle("sap.m"); /** * Icon buttons used in <code>TabStrip</code>. * * @enum * @type {{LeftArrowButton: string, RightArrowButton: string, DownArrowButton: string, AddButton: string}} */ TabStrip.ICON_BUTTONS = { LeftArrowButton: "slim-arrow-left", RightArrowButton: "slim-arrow-right", DownArrowButton: Device.system.phone ? "navigation-down-arrow" : "slim-arrow-down", AddButton: "add" }; /** * <code>TabStripSelect</code> ID prefix. * * @type {string} */ TabStrip.SELECT_ITEMS_ID_SUFFIX = '-SelectItem'; /** * ScrollLeft constant. * * @type {number} */ TabStrip.SCROLL_SIZE = 320; /** * The minimum horizontal offset threshold for drag/swipe. * @type {number} */ TabStrip.MIN_DRAG_OFFSET = Device.support.touch ? 15 : 5; /** * Scrolling animation duration constant * * @type {number} */ TabStrip.SCROLL_ANIMATION_DURATION = (function(){ var sAnimationMode = Configuration.getAnimationMode(); return (sAnimationMode !== Configuration.AnimationMode.none && sAnimationMode !== Configuration.AnimationMode.minimal ? 500 : 0); })(); /** * Initializes the control. * * @override * @public */ TabStrip.prototype.init = function () { this._bDoScroll = !Device.system.phone; this._bRtl = Configuration.getRTL(); this._iCurrentScrollLeft = 0; this._iMaxOffsetLeft = null; this._scrollable = null; this._oTouchStartX = null; if (!Device.system.phone) { this._oScroller = new ScrollEnablement(this, this.getId() + "-tabs", { horizontal: true, vertical: false, nonTouchScrolling: true }); } }; /** * Called from parent if the control is destroyed. * * @override * @public */ TabStrip.prototype.exit = function () { this._bRtl = null; this._iCurrentScrollLeft = null; this._iMaxOffsetLeft = null; this._scrollable = null; this._oTouchStartX = null; if (this._oScroller) { this._oScroller.destroy(); this._oScroller = null; } if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } this._removeItemNavigation(); }; /** * Called before the rendering of the control is started. * * @override * @public */ TabStrip.prototype.onBeforeRendering = function () { if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } }; /** * Called when the rendering of the control is completed. * * @override * @public */ TabStrip.prototype.onAfterRendering = function () { if (this._oScroller) { this._oScroller.setIconTabBar(this, jQuery.proxy(this._handleOverflowButtons, this), null); } //use ItemNavigation for keyboardHandling this._addItemNavigation(); if (!Device.system.phone) { // workaround for the problem that the scrollEnablement obtains this reference only after its hook to onAfterRendering of the TabStrip is called this._oScroller._$Container = this.$("tabsContainer"); this._adjustScrolling(); if (this.getSelectedItem()) { if (!sap.ui.getCore().isThemeApplied()) { sap.ui.getCore().attachThemeChanged(this._handleInititalScrollToItem, this); } else { this._handleInititalScrollToItem(); } } this._sResizeListenerId = ResizeHandler.register(this.getDomRef(), jQuery.proxy(this._adjustScrolling, this)); } else { this.$().toggleClass("sapUiSelectable", this.getItems().length > 1); } }; /** * Scrolls to initially selected item by setting it after the theme is applied * and on after rendering the Tab Strip. * * @private */ TabStrip.prototype._handleInititalScrollToItem = function() { var oItem = sap.ui.getCore().byId(this.getSelectedItem()); if (oItem && oItem.$().length > 0) { // check if the item is already in the DOM this._scrollIntoView(oItem, 500); } sap.ui.getCore().detachThemeChanged(this._handleInititalScrollToItem, this); }; /** * Finds the DOM element that should get the focus. * * @returns {null | Element} The element that should receive the focus or <code>null</code> * @public * @override */ TabStrip.prototype.getFocusDomRef = function () { var oTab = sap.ui.getCore().byId(this.getSelectedItem()); if (!oTab) { return null; } return oTab.getDomRef(); }; /** * Returns an object representing the serialized focus information. * * @param {Object} oFocusInfo The focus information to be applied * @override * @public */ TabStrip.prototype.applyFocusInfo = function (oFocusInfo) { if (oFocusInfo.focusDomRef) { jQuery(oFocusInfo.focusDomRef).trigger("focus"); } }; /** * Adds item navigation functionality. * * @private */ TabStrip.prototype._addItemNavigation = function () { var oHeadDomRef = this.getDomRef("tabsContainer"), aItems = this.getItems(), aTabDomRefs = []; aItems.forEach(function(oItem) { var oItemDomRef = oItem.getDomRef(); jQuery(oItemDomRef).attr("tabindex", "-1"); aTabDomRefs.push(oItemDomRef); }); if (!this._oItemNavigation) { //Initialize the ItemNavigation this._oItemNavigation = new ItemNavigation(); } //Setup the ItemNavigation this._oItemNavigation.setRootDomRef(oHeadDomRef); this._oItemNavigation.setItemDomRefs(aTabDomRefs); this._oItemNavigation.setCycling(false); this._oItemNavigation.setPageSize(5); //alt+right/left is used for browser navigation this._oItemNavigation.setDisabledModifiers({ sapnext: ["alt", "meta"], sapprevious: ["alt", "meta"], saphome : ["alt", "meta"], sapend : ["meta"] }); //Attach ItemNavigation to the control delegate queue this.addDelegate(this._oItemNavigation); }; /** * Checks if scrolling is needed. * * @returns {boolean} Whether scrolling is needed * @private */ TabStrip.prototype._checkScrolling = function() { var oTabsDomRef = this.getDomRef("tabs"), bScrollNeeded = oTabsDomRef && (oTabsDomRef.scrollWidth > this.getDomRef("tabsContainer").offsetWidth); this.$().toggleClass("sapMTSScrollable", bScrollNeeded); return bScrollNeeded; }; TabStrip.prototype.onkeyup = function (oEvent){ if (oEvent && oEvent.keyCode === KeyCodes.ARROW_LEFT || oEvent.keyCode === KeyCodes.ARROW_RIGHT) { var oTarget = Element.closestTo(oEvent.target); this._scrollIntoView(oTarget, 500); } }; TabStrip.prototype._handleOverflowButtons = function() { var oTabsDomRef = this.getDomRef("tabs"), oTabsContainerDomRef = this.getDomRef("tabsContainer"), iScrollLeft, realWidth, availableWidth, bScrollBack = false, bScrollForward = false, bScrollNeeded = this._checkScrolling(); // in case there is something to be scrolled and the left and right "scrolling" buttons are not initialized // we should initialize and render them if (bScrollNeeded && !this.getAggregation("_rightArrowButton") && !this.getAggregation("_leftArrowButton")) { this._getLeftArrowButton(); this._getRightArrowButton(); var oRm = sap.ui.getCore().createRenderManager(); this.getRenderer().renderRightOverflowButtons(oRm, this, true); this.getRenderer().renderLeftOverflowButtons(oRm, this, true); oRm.destroy(); } if (bScrollNeeded && oTabsDomRef && oTabsContainerDomRef) { if (this._bRtl) { iScrollLeft = jQuery(oTabsContainerDomRef).scrollLeftRTL(); } else { iScrollLeft = oTabsContainerDomRef.scrollLeft; } realWidth = oTabsDomRef.scrollWidth; availableWidth = oTabsContainerDomRef.clientWidth; if (Math.abs(realWidth - availableWidth) === 1) { realWidth = availableWidth; } if (iScrollLeft > 0) { if (this._bRtl) { bScrollForward = true; } else { bScrollBack = true; } } if ((realWidth > availableWidth) && (iScrollLeft + availableWidth < realWidth)) { if (this._bRtl) { bScrollBack = true; } else { bScrollForward = true; } } this.$().toggleClass("sapMTSScrollBack", bScrollBack) .toggleClass("sapMTSScrollForward", bScrollForward); } else { this.$().toggleClass("sapMTSScrollBack", false) .toggleClass("sapMTSScrollForward", false); } }; /** * Calculates the maximum <code>OffsetLeft</code> and performs an overflow check. * * @private */ TabStrip.prototype._adjustScrolling = function() { this._iMaxOffsetLeft = Math.abs(this.$("tabsContainer").width() - this.$("tabs").width()); this._handleOverflowButtons(); }; /** * Lazily initializes the <code>_leftArrowButton</code> aggregation. * @private * @returns {sap.m.AccButton} The newly created control */ TabStrip.prototype._getLeftArrowButton = function () { return this._getArrowButton("_leftArrowButton", oRb.getText("TABSTRIP_SCROLL_BACK"), TabStrip.ICON_BUTTONS.LeftArrowButton, -TabStrip.SCROLL_SIZE); }; /** * Lazily initializes the <code>_leftArrowButton</code> aggregation. * @private * @returns {sap.m.AccButton} The newly created control */ TabStrip.prototype._getRightArrowButton = function () { return this._getArrowButton("_rightArrowButton", oRb.getText("TABSTRIP_SCROLL_FORWARD"), TabStrip.ICON_BUTTONS.RightArrowButton, TabStrip.SCROLL_SIZE); }; /** * Lazily initializes the left or right arrows aggregation. * @private * @param {string} sButton The button to be initialized * @param {string} sTooltip The tooltip to be set * @param {string} sIcon The icon to be set * @param {int} iDelta The delta of the scroll * @returns {sap.m.AccButton} The newly created control */ TabStrip.prototype._getArrowButton = function (sButton, sTooltip, sIcon, iDelta) { var oControl = this.getAggregation(sButton), that = this; if (!oControl) { oControl = new AccButton({ type: ButtonType.Transparent, icon: IconPool.getIconURI(sIcon), tooltip: sTooltip, tabIndex: "-1", ariaHidden: "true", press: function (oEvent) { that._scroll(iDelta, TabStrip.SCROLL_ANIMATION_DURATION); } }); this.setAggregation(sButton, oControl, true); } return oControl; }; /** * Removes the item navigation delegate. * * @private */ TabStrip.prototype._removeItemNavigation = function () { if (this._oItemNavigation) { this.removeDelegate(this._oItemNavigation); this._oItemNavigation.destroy(); delete this._oItemNavigation; } }; /** * Performs horizontal scroll. * * @param {int} iDelta The target scrollLeft value * @param {int} iDuration Scroll animation duration * @private */ TabStrip.prototype._scroll = function(iDelta, iDuration) { var iScrollLeft = this.getDomRef("tabsContainer").scrollLeft, iScrollTarget; if (this._bRtl) { iScrollTarget = iScrollLeft - iDelta; if (Device.browser.firefox) { // Avoid out ofRange situation if (iScrollTarget < -this._iMaxOffsetLeft) { iScrollTarget = -this._iMaxOffsetLeft; } if (iScrollTarget > 0) { iScrollTarget = 0; } } } else { iScrollTarget = iScrollLeft + iDelta; if (iScrollTarget < 0) { iScrollTarget = 0; } if (iScrollTarget > this._iMaxOffsetLeft) { iScrollTarget = this._iMaxOffsetLeft; } } this._oScroller.scrollTo(iScrollTarget, 0, iDuration); this._iCurrentScrollLeft = iScrollTarget; }; /** * Scrolls to a particular item. * * @param {sap.m.TabStripItem} oItem The item to be scrolled to * @param {int} iDuration Duration of the scrolling animation * @private */ TabStrip.prototype._scrollIntoView = function (oItem, iDuration) { var $tabs = this.$("tabs"), $item = oItem.$(), iLeftButtonWidth = this.$("leftOverflowButtons") ? this.$("leftOverflowButtons").width() : 0, iRigtButtonWidth = this.$("rightOverflowButtons") ? this.$("rightOverflowButtons").width() : 0, iTabsPaddingWidth = $tabs.innerWidth() - $tabs.width(), iItemWidth = $item.outerWidth(true), iItemPosLeft = $item.position().left - iTabsPaddingWidth / 2, oTabsContainerDomRef = this.getDomRef("tabsContainer"), iScrollLeft = oTabsContainerDomRef.scrollLeft, iContainerWidth = this.$("tabsContainer").width(), iNewScrollLeft = iScrollLeft; // check if item is outside of viewport if (iItemPosLeft < iLeftButtonWidth || iItemPosLeft + iRigtButtonWidth > iContainerWidth - iItemWidth) { if (this._bRtl && Device.browser.firefox) { if (iItemPosLeft > iLeftButtonWidth) { // right side: make this the last item iNewScrollLeft += iItemPosLeft + iItemWidth - iContainerWidth + iRigtButtonWidth; } else { // left side: make this the first item iNewScrollLeft += iItemPosLeft - iLeftButtonWidth; } } else { if (iItemPosLeft < iLeftButtonWidth) { // left side: make this the first item iNewScrollLeft += iItemPosLeft - iRigtButtonWidth; } else { // right side: make this the last item iNewScrollLeft += iItemPosLeft + iItemWidth - iContainerWidth + iLeftButtonWidth; } } // store current scroll state to set it after re-rendering this._iCurrentScrollLeft = iNewScrollLeft; this._oScroller.scrollTo(iNewScrollLeft, 0, iDuration); } }; /** * Create the instance of the <code>TabStripSelect</code>. * * @param { Array<sap.m.TabStripItem> } aTabStripItems Array with the <code>TabStripItems</code> * @returns {CustomSelect} The created <code>CustomSelect</code> * @private */ TabStrip.prototype._createSelect = function (aTabStripItems) { var oSelect, oSelectedSelectItem, oSelectedTabStripItem, oConstructorSettings = { type: SelectType.IconOnly, autoAdjustWidth : true, maxWidth: "2.5rem", icon: IconPool.getIconURI(TabStrip.ICON_BUTTONS.DownArrowButton), tooltip: oRb.getText("TABSTRIP_OPENED_TABS"), change: function (oEvent) { oSelectedSelectItem = oEvent.getParameters()['selectedItem']; oSelectedTabStripItem = this._findTabStripItemFromSelectItem(oSelectedSelectItem); if (oSelectedTabStripItem instanceof TabStripItem) { this._activateItem(oSelectedTabStripItem, oEvent); } }.bind(this) }; oSelect = new CustomSelect(oConstructorSettings).addStyleClass("sapMTSOverflowSelect"); this._addItemsToSelect(oSelect, aTabStripItems); return oSelect; }; /** * Handles when the Space or Enter key is pressed. * * @param {jQuery.Event} oEvent The event object */ TabStrip.prototype.onsapselect = function(oEvent) { // mark the event for components that needs to know if the event was handled oEvent.setMarked(); oEvent.preventDefault(); /* Fire activate item only if select is on an Item.*/ if (oEvent.srcControl instanceof TabStripItem) { this._activateItem(oEvent.srcControl, oEvent); } }; /** * Handles the delete keyboard event. * @param {jQuery.Event} oEvent The event object */ TabStrip.prototype.onsapdelete = function(oEvent) { var oItem = Element.closestTo(oEvent.target), bShouldChangeSelection = oItem.getId() === this.getSelectedItem(), fnSelectionCallback = function() { this._moveToNextItem(bShouldChangeSelection); }; this._removeItem(oItem, fnSelectionCallback); }; /** * Calculates the next item to be focused and selected and applies the focus and selection when an item is removed. * * @param {boolean} bSetAsSelected Whether the next item to be selected * @private */ TabStrip.prototype._moveToNextItem = function (bSetAsSelected) { if (!this._oItemNavigation) { return; } var iItemsCount = this.getItems().length, iCurrentFocusedIndex = this._oItemNavigation.getFocusedIndex(), iNextIndex = iItemsCount === iCurrentFocusedIndex ? --iCurrentFocusedIndex : iCurrentFocusedIndex, oNextItem = this.getItems()[iNextIndex], fnFocusCallback = function () { if (this._oItemNavigation) { this._oItemNavigation.focusItem(iNextIndex); } }; //ToDo: Might be reconsidered when TabStrip is released for standalone usage // Selection (causes invalidation) if (bSetAsSelected) { this.setSelectedItem(oNextItem); //Notify the subscriber this.fireItemPress({item: oNextItem}); } // Focus (force to wait until invalidated) setTimeout(fnFocusCallback.bind(this), 0); }; /** * Activates an item on the <code>TabStrip</code>. * * @param {sap.m.TabStripItem} oItem The item to be activated * @param {jQuery.Event} oEvent Event object that probably will be present as the item activation is bubbling * @private */ TabStrip.prototype._activateItem = function(oItem, oEvent) { /* As the '_activateItem' is part of a bubbling selection change event, allow the final event handler * to prevent it.*/ if (this.fireItemSelect({item: oItem})) { if (!this.getSelectedItem() || this.getSelectedItem() !== oItem.getId()) { this.setSelectedItem(oItem); } this.fireItemPress({ item: oItem }); } else if (oEvent instanceof jQuery.Event && !oEvent.isDefaultPrevented()) { oEvent.preventDefault(); } }; /** * Adds an entity <code>oObject</code> to the aggregation identified by <code>sAggregationName</code>. * * @param {string} sAggregationName The name of the aggregation where the new entity is to be added * @param {any} oObject The value of the aggregation to be added * @param {boolean} bSuppressInvalidate Whether to suppress invalidation * @returns {this} <code>this</code> pointer for chaining * @override */ TabStrip.prototype.addAggregation = function(sAggregationName, oObject, bSuppressInvalidate) { if (sAggregationName === 'items') { this._handleItemsAggregation(['addAggregation', oObject, bSuppressInvalidate], true); } return Control.prototype.addAggregation.call(this, sAggregationName, oObject, bSuppressInvalidate); }; /** * Inserts an entity to the aggregation named <code>sAggregationName</code> at position <code>iIndex</code>. * * @param {string} sAggregationName The name of the aggregation * @param {any} oObject The value of the aggregation to be inserted * @param {int} iIndex The index to be inserted in * @param {boolean} bSuppressInvalidate Whether to suppress invalidation * @returns {this} <code>this</code> pointer for chaining * @override */ TabStrip.prototype.insertAggregation = function(sAggregationName, oObject, iIndex, bSuppressInvalidate) { if (sAggregationName === 'items') { this._handleItemsAggregation(['insertAggregation', oObject, iIndex, bSuppressInvalidate], true); } return Control.prototype.insertAggregation.call(this, sAggregationName, oObject, iIndex, bSuppressInvalidate); }; /** * Removes an entity from the aggregation named <code>sAggregationName</code>. * * @param {string} sAggregationName The name of the aggregation * @param {any} oObject The value of aggregation to be removed * @param {boolean} bSuppressInvalidate Whether to suppress invalidation * @returns {sap.ui.base.ManagedObject} The removed aggregated item * @override */ TabStrip.prototype.removeAggregation = function(sAggregationName, oObject, bSuppressInvalidate) { if (sAggregationName === 'items') { this._handleItemsAggregation(['removeAggregation', oObject, bSuppressInvalidate]); } return Control.prototype.removeAggregation.call(this, sAggregationName, oObject, bSuppressInvalidate); }; /** * Removes all objects from the aggregation named <code>sAggregationName</code>. * * @param {string} sAggregationName The name of aggregation * @param {boolean} bSuppressInvalidate Whether to suppress invalidation * @returns {sap.ui.base.ManagedObject[]} the removed aggregated items * @override */ TabStrip.prototype.removeAllAggregation = function(sAggregationName, bSuppressInvalidate) { if (sAggregationName === 'items') { this._handleItemsAggregation(['removeAllAggregation', null, bSuppressInvalidate]); } return Control.prototype.removeAllAggregation.call(this, sAggregationName, bSuppressInvalidate); }; /** * Destroys all the entities in the aggregation named <code>sAggregationName</code>. * * @param {string} sAggregationName The name of aggregation * @param {boolean} bSuppressInvalidate Whether to suppress invalidation * @returns {this} <code>this</code> pointer for chaining * @override */ TabStrip.prototype.destroyAggregation = function(sAggregationName, bSuppressInvalidate) { if (sAggregationName === 'items') { this._handleItemsAggregation(['destroyAggregation', bSuppressInvalidate]); } return Control.prototype.destroyAggregation.call(this, sAggregationName, bSuppressInvalidate); }; /* * Sets a <code>TabStripItem</code> as current. * * @param {sap.m.TabStripItem} oSelectedItem the item that should be set as current * @returns {this} <code>this</code> pointer for chaining * @override */ TabStrip.prototype.setSelectedItem = function(oSelectedItem) { var bNotMobile = !Device.system.phone; if (!oSelectedItem) { return this; } if (oSelectedItem.$().length > 0 && bNotMobile) { this._scrollIntoView(oSelectedItem, 500); } if (bNotMobile) { this._updateAriaSelectedAttributes(this.getItems(), oSelectedItem); this._updateSelectedItemClasses(oSelectedItem.getId()); } // propagate the selection change to the select aggregation if (this.getHasSelect()) { var oSelectItem = this._findSelectItemFromTabStripItem(oSelectedItem); this.getAggregation('_select').setSelectedItem(oSelectItem); } return this.setAssociation("selectedItem", oSelectedItem, bNotMobile); }; /** * Overrides the default method to make sure a <code>TabStripSelect</code> instance is created when needed. * * @param {string} sPropertyName The property name to be set * @param {any} vValue The property value to be set * @param {boolean} bSuppressInvalidate Whether to suppress invalidation * @returns {this} <code>this</code> pointer for chaining * @override */ TabStrip.prototype.setProperty = function(sPropertyName, vValue, bSuppressInvalidate) { var vRes; vRes = Control.prototype.setProperty.call(this, sPropertyName, vValue, bSuppressInvalidate); // handle the _select aggregation instance if (sPropertyName === 'hasSelect') { if (vValue) { if (!this.getAggregation('_select')) { vRes = this.setAggregation('_select', this._createSelect(this.getItems())); } } else { vRes = this.destroyAggregation('_select'); } } return vRes; }; /** * Attaches any previously added event handlers. * * @param {object} oObject The <code>TabStripItem</code> instance on which events will be detached/attached * @private */ TabStrip.prototype._attachItemEventListeners = function (oObject) { if (oObject instanceof TabStripItem) { var aEvents = [ 'itemClosePressed', 'itemPropertyChanged' ]; aEvents.forEach(function (sEventName) { sEventName = sEventName.charAt(0).toUpperCase() + sEventName.slice(1); // Capitalize // detach any listeners - make sure we always have one listener at a time only oObject['detach' + sEventName](this['_handle' + sEventName]); //e.g. oObject['detachItemClosePressed'](this.['_handleItemClosePressed']) // attach the listeners oObject['attach' + sEventName](this['_handle' + sEventName].bind(this)); }, this); } }; /** * Detaches any previously added event handlers. * * @param {object} oObject The <code>TabStripItem</code> instance on which events will be detached/attached. * @private */ TabStrip.prototype._detachItemEventListeners = function (oObject) { // !oObject check is needed because "null" is an object if (!oObject || typeof oObject !== 'object' || !(oObject instanceof TabStripItem)) { // in case of no concrete item object, remove the listeners from all items // ToDo: confirm that the listeners removal is needed ..? var aItems = this.getItems(); aItems.forEach(function (oItem) { if (typeof oItem !== 'object' || !(oItem instanceof TabStripItem)) { // because of recursion, make sure it never goes into endless loop return; } return this._detachItemEventListeners(oItem); }.bind(this)); } }; /** * Propagates the property change from a <code>TabStrip</code> item instance to the <code>TabStrip</code> select item copy instance. * * @param {jQuery.Event} oEvent Event object * @private */ TabStrip.prototype._handleItemPropertyChanged = function (oEvent) { var oSelectItem = this._findSelectItemFromTabStripItem(oEvent.getSource()); var sPropertyKey = oEvent['mParameters'].propertyKey; // call it directly with the setter name so overwritten functions can be called and not setProperty method directly var sMethodName = "set" + sPropertyKey.substr(0,1).toUpperCase() + sPropertyKey.substr(1); oSelectItem[sMethodName](oEvent['mParameters'].propertyValue); }; /** * Fires an item close request event based on an item close button press. * * @param {jQuery.Event} oEvent Event object * @private */ TabStrip.prototype._handleItemClosePressed = function (oEvent) { this._removeItem(oEvent.getSource()); }; /** * Request the given item to be closed and removes it from the <code>items</code> aggregation if permitted. * * @param {sap.m.TabStripItem} oItem The item which will disappear * @param {function} fnCallback A callback function to be called after the item is removed * @private */ TabStrip.prototype._removeItem = function(oItem, fnCallback) { var oTabStripItem; /* this method is handling the close pressed event on all item instances (TabStrip and the * CustomSelect copy), so when it's handling the press on the CustomSelect item, it needs to determine the TabStrip item out of the event and vice-versa */ if (!(oItem instanceof TabStripItem)) { Log.error('Expecting instance of a TabStripSelectItem, given: ', oItem); } if (oItem.getId().indexOf(TabStrip.SELECT_ITEMS_ID_SUFFIX) !== -1) { oTabStripItem = this._findTabStripItemFromSelectItem(oItem); } else { oTabStripItem = oItem; } if (this.fireItemClose({item: oTabStripItem})) { this.removeAggregation('items', oTabStripItem); // the select item will also get removed this._moveToNextItem(oItem.getId() === this.getSelectedItem()); if (fnCallback) { fnCallback.call(this); } } }; /** * Ensures proper handling of <code>TabStrip</code> <code>items</code> aggregation> and proxies to the <code>TabStripSelect</code> <code>items</code> aggregation. * * @param {array} aArgs * @param {boolean} bIsAdding * @returns {this} <code>this</code> instance for chaining */ TabStrip.prototype._handleItemsAggregation = function (aArgs, bIsAdding) { var sAggregationName = 'items', // name of the aggregation in CustomSelect sFunctionName = aArgs[0], oObject = aArgs[1], aNewArgs = [sAggregationName]; /* remove the function name from the args array */ aArgs.forEach(function (iItem, iIndex) { if (iIndex > 0) { aNewArgs.push(iItem); } }); if (bIsAdding) { // attach and detach (or only detach if not adding) event listeners for the item this._attachItemEventListeners(oObject); } else { this._detachItemEventListeners(oObject); } // no need to handle anything else for other aggregations than 'items' if (sAggregationName !== "items") { return this; } if (this.getHasSelect()) { this._handleSelectItemsAggregation(aNewArgs, bIsAdding, sFunctionName, oObject); } return this; }; /** * Ensures proper handling of <code>TabStrip</code> <code>items</code> aggregation and proxies to the <code>TabStripSelect</code> <code>items</code> aggregation. * * @param {array} aArgs * @param {boolean} bIsAdding * @param {string} sFunctionName * @param {object} oObject * @returns {*} */ TabStrip.prototype._handleSelectItemsAggregation = function (aArgs, bIsAdding, sFunctionName, oObject) { var oSelect = this.getAggregation('_select'), // a new instance, holding a copy of the TabStripItem which is given to the CustomSelect instance oDerivedObject; if (sFunctionName === 'destroyAggregation' && !oSelect) { /* ToDo : For some reason aggregation _select may be already deleted (e.g. TabStrip.destroy will destroy all children including _select */ return; } // ToDo: test this functionality // destroyAggregation and removeAllAggregation no not need oObject, action can be directly taken if (oObject === null || typeof oObject !== 'object') { return oSelect[sFunctionName]['apply'](oSelect, aArgs); } if (bIsAdding) { oDerivedObject = this._createSelectItemFromTabStripItem(oObject); } else { oDerivedObject = this._findSelectItemFromTabStripItem(oObject); } // substitute the TabStrip item instance with the TabStripSelectItem instance aArgs.forEach(function (iItem, iIndex) { if (typeof iItem === 'object') { aArgs[iIndex] = oDerivedObject; } }); return oSelect[sFunctionName]['apply'](oSelect, aArgs); }; /** * Creates <code>TabStripItem</code> in context of <code>TabStripSelect</code>. * * @param {Object} oSelect The select object to which the items will be added * @param {array} aItems The items to be added */ TabStrip.prototype._addItemsToSelect = function (oSelect, aItems) { aItems.forEach(function (oItem) { var oSelectItem = this._createSelectItemFromTabStripItem(oItem); oSelect.addAggregation('items', oSelectItem); // make sure to set the correct select item if (oItem.getId() === this.getSelectedItem()) { oSelect.setSelectedItem(oSelectItem); } }, this); }; /** * Ensures proper <code>TabStripItem</code> inheritance in context of <code>TabStripSelect</code>. * * @param {sap.m.TabStripItem} oTabStripItem The source item for which a TabStripSelect will be created * @returns {sap.ui.core.Element} The TabStripSelect item created */ TabStrip.prototype._createSelectItemFromTabStripItem = function (oTabStripItem) { var oSelectItem; if (!oTabStripItem && !(oTabStripItem instanceof sap.m.TabContainerItem)) { Log.error('Expecting instance of "sap.m.TabContainerItem": instead of ' + oTabStripItem + ' given.'); return; } oSelectItem = new TabStripItem({ id: oTabStripItem.getId() + TabStrip.SELECT_ITEMS_ID_SUFFIX, text: oTabStripItem.getText(), additionalText: oTabStripItem.getAdditionalText(), icon: oTabStripItem.getIcon(), iconTooltip: oTabStripItem.getIconTooltip(), modified: oTabStripItem.getModified(), itemClosePressed: function (oEvent) { this._handleItemClosePressed(oEvent); }.bind(this) }); oSelectItem.addEventDelegate({ ontap: function (oEvent) { var oTarget = oEvent.srcControl; // if we clicked on the image/icon change the target to be the parent, // so the change event in the select can be handled properly if (oEvent.target.id === oTarget.getParent().getId() + "-img") { oEvent.srcControl = oTarget = oTarget.getParent(); } if ((oTarget instanceof AccButton || oTarget instanceof Icon)) { this.fireItemClosePressed({item: this}); } } }, oSelectItem); return oSelectItem; }; /** * Finds the correct <code>TabStripItem</code> in context of <code>TabStrip</code> by a given <code>TabStripItem</code> instance. * * @param {sap.m.TabStripItem} oTabStripSelectItem The <code>TabStripItem</code> instance which analogue is to be found * @returns {sap.m.TabStripItem} The <code>TabStripItem</code> in context of <code>TabStripSelect</code> found (if any) */ TabStrip.prototype._findTabStripItemFromSelectItem = function (oTabStripSelectItem) { var iIndex, sTabStripItemId = oTabStripSelectItem.getId().replace(TabStrip.SELECT_ITEMS_ID_SUFFIX , ''), aTabStripItems = this.getItems(); for (iIndex = 0; iIndex < aTabStripItems.length; iIndex++) { if (aTabStripItems[iIndex].getId() === sTabStripItemId) { return aTabStripItems[iIndex]; } } }; /** * Finds the correct <code>TabStripItem</code> in context of <code>TabStripSelect</code> by a given <code>TabStripItem</code> instance. * * @param {sap.m.TabStripItem} oTabStripItem The <code>TabStripItem</code> instance which analogue is to be found * @returns {sap.m.TabStripItem} The <code>TabStripItem</code> in context of <code>TabStripSelect</code> found (if any) */ TabStrip.prototype._findSelectItemFromTabStripItem = function (oTabStripItem) { var iIndex, aSelectItems, sSelectItemId = oTabStripItem.getId() + TabStrip.SELECT_ITEMS_ID_SUFFIX; if (this.getHasSelect()) { aSelectItems = this.getAggregation('_select').getItems(); for (iIndex = 0; iIndex < aSelectItems.length; iIndex++) { if (aSelectItems[iIndex].getId() === sSelectItemId) { return aSelectItems[iIndex]; } } } }; /** * Handles ARIA-selected attributes depending on the currently selected item. * * @param {Array.<sap.m.TabStripItem>} aItems The whole set of items * @param {sap.m.TabStripItem} oSelectedItem Currently selected item * @private */ TabStrip.prototype._updateAriaSelectedAttributes = function(aItems, oSelectedItem) { var sAriaSelected; aItems.forEach(function (oItem) { sAriaSelected = "false"; if (oItem.$()) { if (oSelectedItem && oSelectedItem.getId() === oItem.getId()) { sAriaSelected = "true"; } oItem.$().attr("aria-selected", sAriaSelected); } }); }; /** * Handles the proper update of the <code>TabStripItem</code> selection class. * * @param {string} sSelectedItemId The ID of the selected item */ TabStrip.prototype._updateSelectedItemClasses = function(sSelectedItemId) { if (this.$("tabs")) { this.$("tabs").children(".sapMTabStripItemSelected").removeClass("sapMTabStripItemSelected"); jQuery("#" + sSelectedItemId).addClass("sapMTabStripItemSelected"); } }; /** * ToDo: This method doesn't work because the rendering works with ::after pseudo element - better to alter the * renderer, so this logic would work the same way for the select item and tabstrip item. */ /** * Changes the visibility of the item "state" symbol. * @param {any} vItemId The ID of the item * @param {boolean} bShowState If the state must be shown */ TabStrip.prototype.changeItemState = function(vItemId, bShowState) { var $oItemState; // optimisation to not invalidate and rerender the whole parent DOM, but only manipulate the CSS class // for invisibility on the concrete DOM element that needs to change var aItems = this.getItems(); aItems.forEach(function (oItem) { if (vItemId === oItem.getId()) { $oItemState = jQuery(oItem.$()); if (bShowState === true && !$oItemState.hasClass(TabStripItem.CSS_CLASS_MODIFIED)) { $oItemState.addClass(TabStripItem.CSS_CLASS_MODIFIED); } else { $oItemState.removeClass(TabStripItem.CSS_CLASS_MODIFIED); } } }); }; /** * Handles the <code>onTouchStart</code> event. * @param {jQuery.Event} oEvent Event object */ TabStrip.prototype.ontouchstart = function (oEvent) { var oTargetItem = Element.closestTo(oEvent.target); if (oTargetItem instanceof TabStripItem || oTargetItem instanceof AccButton || oTargetItem instanceof Icon || oTargetItem instanceof Image || oTargetItem instanceof CustomSelect) { // Support only single touch // Store the pageX coordinate for for later usage in touchend this._oTouchStartX = oEvent.changedTouches[0].pageX; } }; /** * Handles the <code>onTouchEnd</code> event. * @param {jQuery.Event} oEvent Event object */ TabStrip.prototype.ontouchend = function (oEvent) { var oTarget, iDeltaX; if (!this._oTouchStartX) { return; } oTarget = Element.closestTo(oEvent.target); // check if we click on the item Icon and if so, give the parent as a target if (oEvent.target.id === oTarget.getParent().getId() + "-img") { oTarget = oTarget.getParent(); } // Support only single touch iDeltaX = Math.abs(oEvent.changedTouches[0].pageX - this._oTouchStartX); if (iDeltaX < TabStrip.MIN_DRAG_OFFSET) { if (oTarget instanceof TabStripItem) { // TabStripItem clicked this._activateItem(oTarget, oEvent); } else if (oTarget instanceof AccButton) { // TabStripItem close button clicked if (oTarget && oTarget.getParent && oTarget.getParent() instanceof TabStripItem) { oTarget = oTarget.getParent(); this._removeItem(oTarget); } } else if (oTarget instanceof Icon) { // TabStripItem close button icon clicked if (oTarget && oTarget.getParent && oTarget.getParent().getParent && oTarget.getParent().getParent() instanceof TabStripItem) { oTarget = oTarget.getParent().getParent(); this._removeItem(oTarget); } } // Not needed anymore this._oTouchStartX = null; } }; /* * Destroys all <code>TabStripItem</code> entities from the <code>items</code> aggregation of the <code>TabStrip</code>. * * @returns {this} This instance for chaining * @override */ TabStrip.prototype.destroyItems = function() { this.setAssociation("selectedItem", null); return this.destroyAggregation("items"); }; /****************************************** CUSTOM SELECT CONTROL **********************************************/ var CustomSelectRenderer = Renderer.extend(SelectRenderer); CustomSelectRenderer.apiVersion = 2; var CustomSelect = Select.extend("sap.m.internal.TabStripSelect", { metadata: { library: "sap.m" }, renderer: CustomSelectRenderer }); CustomSelect.prototype.onAfterRendering = function (){ Select.prototype.onAfterRendering.apply(this, arguments); this.$().attr("tabindex", "-1"); }; CustomSelect.prototype.onAfterRenderingPicker = function() { var oPicker = this.getPicker(); Select.prototype.onAfterRenderingPicker.call(this); // on phone the picker is a dialog and does not have an offset if (Device.system.phone) { return; } oPicker.setOffsetX(Math.round( Configuration.getRTL() ? this.getPicker().$().width() - this.$().width() : this.$().width() - this.getPicker().$().width() )); // LTR or RTL mode considered oPicker.setOffsetY(this.$().parents().hasClass('sapUiSizeCompact') ? 2 : 3); oPicker._calcPlacement(); // needed to apply the new offset after the popup is open }; CustomSelect.prototype.createList = function() { // list to use inside the picker this._oList = new CustomSelectList({ width: "100%" }).attachSelectionChange(this.onSelectionChange, this) .addEventDelegate({ ontap: function(oEvent) { this.close(); } }, this); return this._oList; }; CustomSelect.prototype.setValue = function(sValue) { Select.prototype.setValue.apply(this, arguments); this.$("label").toggleClass("sapMTSOverflowSelectLabelModified", this.getSelectedItem() && this.getSelectedItem().getProperty("modified")); return this; }; CustomSelect.prototype._getValueIcon = function() { // our select will not show neither image nor icon on the left of the text return null; }; /****************************************** CUSTOM SELECT LIST CONTROL *****************************************/ var CustomSelectListRenderer = Renderer.extend(SelectListRenderer); CustomSelectListRenderer.apiVersion = 2; CustomSelectListRenderer.renderItem = function(oRm, oList, oItem, mStates) { oRm.openStart("li", oItem); oRm.class(SelectListRenderer.CSS_CLASS + "ItemBase"); oRm.class(SelectListRenderer.CSS_CLASS + "Item"); oRm.class("sapMTSOverflowSelectListItem"); if (oItem.getProperty("modified")) { oRm.class("sapMTSOverflowSelectListItemModified"); } if (Device.system.desktop) { oRm.class(SelectListRenderer.CSS_CLASS + "ItemBaseHoverable"); } if (oItem === oList.getSelectedItem()) { oRm.class(SelectListRenderer.CSS_CLASS + "ItemBaseSelected"); } oRm.attr("tabindex", 0); this.writeItemAccessibilityState.apply(this, arguments); oRm.openEnd(); oRm.openStart("div"); oRm.class("sapMSelectListItemText"); oRm.openEnd(); // write icon if (oItem.getIcon()) { oRm.renderControl(oItem._getImage()); } oRm.openStart("div"); // Start texts container oRm.class("sapMTSTexts"); oRm.openEnd(); // write additional text this.renderItemText(oRm, oItem.getAdditionalText(), TabStripItem.CSS_CLASS_TEXT); // write label text this.renderItemText(oRm, oItem.getText(), TabStripItem.CSS_CLASS_LABEL); oRm.close("div"); oRm.close("div"); oRm.renderControl(oItem.getAggregation('_closeButton')); oRm.close("li"); }; CustomSelectListRenderer.renderItemText = function (oRm, sItemText, sCssClass) { oRm.openStart("div"); oRm.class(sCssClass); oRm.openEnd(); oRm.text(sItemText.slice(0, (Device.system.phone ? sItemText.length : TabStripItem.DISPLAY_TEXT_MAX_LENGTH))); // add three dots "..." at the end if not the whole additional text is shown if (!Device.system.phone && sItemText.length > TabStripItem.DISPLAY_TEXT_MAX_LENGTH) { oRm.text('...'); } oRm.close("div"); }; var CustomSelectList = SelectList.extend("sap.m.internal.TabStripSelectList", { metadata: { library: "sap.m" }, renderer: CustomSelectListRenderer }); return TabStrip; });