/*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2018 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.Breadcrumbs. sap.ui.define([ "sap/ui/core/Control", "sap/m/Text", "sap/m/Link", "sap/m/Select", "sap/ui/core/Item", "sap/ui/core/delegate/ItemNavigation", "sap/ui/core/ResizeHandler", "sap/ui/core/IconPool", "sap/ui/Device", "sap/m/library", "./BreadcrumbsRenderer" ], function( Control, Text, Link, Select, Item, ItemNavigation, ResizeHandler, IconPool, Device, library, BreadcrumbsRenderer ) { "use strict"; // shortcut for sap.m.SelectType var SelectType = library.SelectType; /** * Constructor for a new Breadcrumbs. * * @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 * Enables users to navigate between items by providing a list of links to previous steps in the user's * navigation path. The last three steps can be accessed as links directly, while the remaining links prior to them * are available in a drop-down menu. * * @see {@link fiori:https://experience.sap.com/fiori-design-web/breadcrumb/ Breadcrumbs} * * @extends sap.ui.core.Control * * @author SAP SE * @version 1.60.23 * * @constructor * @public * @since 1.34 * @alias sap.m.Breadcrumbs * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel * */ var Breadcrumbs = Control.extend("sap.m.Breadcrumbs", { metadata: { library: "sap.m", interfaces: ["sap.m.IBreadcrumbs"], designtime: "sap/m/designtime/Breadcrumbs.designtime", properties: { /** * Determines the text of current/last element in the Breadcrumbs path. * @since 1.34 */ currentLocationText: {type: "string", group: "Behavior", defaultValue: null} }, aggregations: { /** * A list of all the active link elements in the Breadcrumbs control. * Note: Enabling the property wrapping of the link will not work * since it's incompatible with the concept of the control. * The other properties will work, but their effect may be undesirable. * @since 1.34 */ links: {type: "sap.m.Link", multiple: true, singularName: "link"}, /** * Private aggregations */ _currentLocation: {type: "sap.m.Text", multiple: false, visibility: "hidden"}, _select: {type: "sap.m.Select", multiple: false, visibility: "hidden"} }, defaultAggregation: "links" } }); /*************************************** Framework lifecycle events ******************************************/ Breadcrumbs.prototype.onBeforeRendering = function () { this.bRenderingPhase = true; if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } if (this._bControlsInfoCached) { this._updateSelect(true); } }; Breadcrumbs.prototype.onAfterRendering = function () { if (!this._sResizeListenerId) { this._sResizeListenerId = ResizeHandler.register(this, this._handleScreenResize.bind(this)); } if (!this._bControlsInfoCached) { this._updateSelect(true); return; } this._configureKeyboardHandling(); this.bRenderingPhase = false; }; Breadcrumbs.prototype.onThemeChanged = function () { this._resetControl(); }; Breadcrumbs.prototype.exit = function () { this._resetControl(); this._destroyItemNavigation(); }; /*************************************** Static members ******************************************/ Breadcrumbs.PAGEUP_AND_PAGEDOWN_JUMP_SIZE = 5; /*************************************** Internal aggregation handling ******************************************/ Breadcrumbs.prototype._getAugmentedId = function (sSuffix) { return this.getId() + "-" + sSuffix; }; Breadcrumbs.prototype._getSelect = function () { if (!this.getAggregation("_select")) { this.setAggregation("_select", this._decorateSelect(new Select({ id: this._getAugmentedId("select"), change: this._selectChangeHandler.bind(this), forceSelection: false, autoAdjustWidth: true, icon: IconPool.getIconURI("slim-arrow-down"), type: SelectType.IconOnly, tooltip: BreadcrumbsRenderer._getResourceBundleText("BREADCRUMB_SELECT_TOOLTIP") }))); } return this.getAggregation("_select"); }; Breadcrumbs.prototype._getCurrentLocation = function () { if (!this.getAggregation("_currentLocation")) { this.setAggregation("_currentLocation", new Text({ id: this._getAugmentedId("currentText"), text: this.getCurrentLocationText(), wrapping: false }).addStyleClass("sapMBreadcrumbsCurrentLocation")); } return this.getAggregation("_currentLocation"); }; function fnConvertArguments(sAggregationName, aArguments) { var aConvertedArguments = Array.prototype.slice.apply(aArguments); aConvertedArguments.unshift(sAggregationName); return aConvertedArguments; } Breadcrumbs.prototype.insertLink = function (oLink, iIndex) { var vResult = this.insertAggregation.apply(this, fnConvertArguments("links", arguments)); this._registerControlListener(oLink); this._resetControl(); return vResult; }; Breadcrumbs.prototype.addLink = function (oLink) { var vResult = this.addAggregation.apply(this, fnConvertArguments("links", arguments)); this._registerControlListener(oLink); this._resetControl(); return vResult; }; Breadcrumbs.prototype.removeLink = function (vObject) { var vResult = this.removeAggregation.apply(this, fnConvertArguments("links", arguments)); this._deregisterControlListener(vResult); this._resetControl(); return vResult; }; Breadcrumbs.prototype.removeAllLinks = function () { var aLinks = this.getAggregation("links", []); var vResult = this.removeAllAggregation.apply(this, fnConvertArguments("links", arguments)); aLinks.forEach(this._deregisterControlListener, this); this._resetControl(); return vResult; }; Breadcrumbs.prototype.destroyLinks = function () { var aLinks = this.getAggregation("links", []); var vResult = this.destroyAggregation.apply(this, fnConvertArguments("links", arguments)); aLinks.forEach(this._deregisterControlListener, this); this._resetControl(); return vResult; }; /*************************************** Select Handling ******************************************/ Breadcrumbs.prototype._decorateSelect = function (oSelect) { oSelect.getPicker() .attachAfterOpen(this._removeItemNavigation, this) .attachBeforeClose(this._restoreItemNavigation, this); oSelect._onBeforeOpenDialog = this._onSelectBeforeOpenDialog.bind(this); oSelect._onBeforeOpenPopover = this._onSelectBeforeOpenPopover.bind(this); oSelect.onsapescape = this._onSelectEscPress.bind(this); return oSelect; }; Breadcrumbs.prototype._removeItemNavigation = function () { this.removeDelegate(this._getItemNavigation()); }; Breadcrumbs.prototype._onSelectBeforeOpenDialog = function () { var oSelect = this._getSelect(); if (this.getCurrentLocationText() && Device.system.phone) { oSelect.setSelectedIndex(0); } else { oSelect.setSelectedItem(null); } Select.prototype._onBeforeOpenDialog.call(oSelect); this._removeItemNavigation(); }; Breadcrumbs.prototype._onSelectBeforeOpenPopover = function () { this._getSelect().setSelectedItem(null); this._removeItemNavigation(); }; Breadcrumbs.prototype._restoreItemNavigation = function () { this.addDelegate(this._getItemNavigation()); }; Breadcrumbs.prototype._onSelectEscPress = function () { this._getSelect().close(); }; /** * Retrieves selected item item using an sap.m.Link or sap.m.Text * * @param {control} oItem * @returns {sap.ui.core.Item} * @private */ Breadcrumbs.prototype._createSelectItem = function (oItem) { return new Item({ key: oItem.getId(), text: oItem.getText() }); }; /** * Handles the "select" event * * @param {jQuery.Event} oEvent * @private */ Breadcrumbs.prototype._selectChangeHandler = function (oEvent) { var oLink, sLinkHref, sLinkTarget, oSelectedItem = oEvent.getParameter("selectedItem"); /* there's no selected item, nothing to do in this case (the selected item is often set to null) */ if (!oSelectedItem) { return; } /* The select change event is fired every time a selection is made, in Icon mode (in which we're using it) the user doesn't see this selection change and we shouldn't act on it */ if (!this._getSelect().isOpen()) { return; } oLink = sap.ui.getCore().byId(oSelectedItem.getKey()); /* if it's not a link, then it must be only the current location text, we shouldn't do anything */ if (!(oLink instanceof Link)) { return; } sLinkHref = oLink.getHref(); sLinkTarget = oLink.getTarget(); oLink.firePress(); if (sLinkHref) { if (sLinkTarget) { window.open(sLinkHref, sLinkTarget); } else { window.location.href = sLinkHref; } } }; Breadcrumbs.prototype._getItemsForMobile = function () { var oItems = this.getLinks(); if (this.getCurrentLocationText()) { oItems.push(this._getCurrentLocation()); } return oItems; }; /** * Updates the select with the current "distribution" of controls. * * @private * @param {boolean} bInvalidateDistribution */ Breadcrumbs.prototype._updateSelect = function (bInvalidateDistribution) { var oSelect = this._getSelect(), aControlsForSelect, oControlsDistribution = this._getControlDistribution(); if (!this._bControlDistributionCached || bInvalidateDistribution) { oSelect.destroyItems(); aControlsForSelect = Device.system.phone ? this._getItemsForMobile() : oControlsDistribution.aControlsForSelect; aControlsForSelect.map(this._createSelectItem).reverse().forEach(oSelect.insertItem, oSelect); this._bControlDistributionCached = true; this.invalidate(this); } oSelect.setVisible(!!oControlsDistribution.aControlsForSelect.length); if (!this._sResizeListenerId && !this.bRenderingPhase) { this._sResizeListenerId = ResizeHandler.register(this, this._handleScreenResize.bind(this)); } }; Breadcrumbs.prototype._getControlsForBreadcrumbTrail = function () { var aVisibleControls; if (this._bControlDistributionCached && this._oDistributedControls) { return this._oDistributedControls.aControlsForBreadcrumbTrail; } aVisibleControls = this.getLinks().filter(function (oLink) { return oLink.getVisible(); }); if (this.getCurrentLocationText()) { return aVisibleControls.concat([this._getCurrentLocation()]); } return aVisibleControls; }; Breadcrumbs.prototype._getControlInfo = function (oControl) { return { id: oControl.getId(), control: oControl, width: oControl.$().parent().outerWidth(true), bCanOverflow: oControl instanceof Link }; }; Breadcrumbs.prototype._getControlDistribution = function (iMaxContentSize) { iMaxContentSize = iMaxContentSize || this._iContainerSize; this._iContainerSize = iMaxContentSize; this._oDistributedControls = this._determineControlDistribution(iMaxContentSize); return this._oDistributedControls; }; Breadcrumbs.prototype._getSelectWidth = function() { return this._getSelect().getVisible() && this._iSelectWidth || 0; }; Breadcrumbs.prototype._determineControlDistribution = function (iMaxContentSize) { var index, oControlInfo, aControlInfo = this._getControlsInfo().aControlInfo, iSelectWidth = this._getSelectWidth(), aControlsForSelect = [], aControlsForBreadcrumbTrail = [], iUsedSpace = iSelectWidth; // account for the selectWidth initially; // The rightmost controls should go into overflow first, hence iterating the controls in reverse for (index = aControlInfo.length - 1; index >= 0; index--) { oControlInfo = aControlInfo[index]; iUsedSpace += oControlInfo.width; // put the last item of the array in the breadcrumb trail if (aControlInfo.length - 1 === index) { aControlsForBreadcrumbTrail.push(oControlInfo.control); continue; } // we've reached the last item and we've not used a select then we will not need to take it into account if (index === 0) { iUsedSpace -= iSelectWidth; } if (iUsedSpace > iMaxContentSize && oControlInfo.bCanOverflow) { aControlsForSelect.unshift(oControlInfo.control); } else { aControlsForBreadcrumbTrail.unshift(oControlInfo.control); } } return { aControlsForBreadcrumbTrail: aControlsForBreadcrumbTrail, aControlsForSelect: aControlsForSelect }; }; /** * Stores the sizes and other info of controls so they don't need to be recalculated again until they change. * @private * @returns {Object} The Breadcrumbs control information */ Breadcrumbs.prototype._getControlsInfo = function () { if (!this._bControlsInfoCached) { this._iSelectWidth = this._getSelect().$().parent().outerWidth(true) || 0; this._aControlInfo = this._getControlsForBreadcrumbTrail().map(this._getControlInfo); this._iContainerSize = this.$().outerWidth(true); this._bControlsInfoCached = true; } return { aControlInfo: this._aControlInfo, iSelectWidth: this._iSelectWidth, iContentSize: this._iContainerSize }; }; /** * Handles the resize event of the Breadcrumbs control container * * @param {jQuery.Event} oEvent * @returns {object} this * @private */ Breadcrumbs.prototype._handleScreenResize = function (oEvent) { var iCachedControlsForBreadcrumbTrailCount = this._oDistributedControls.aControlsForBreadcrumbTrail.length, oControlsDistribution = this._getControlDistribution(oEvent.size.width), iCalculatedControlsForBreadcrumbTrailCount = oControlsDistribution.aControlsForBreadcrumbTrail.length; if (iCachedControlsForBreadcrumbTrailCount !== iCalculatedControlsForBreadcrumbTrailCount) { this._updateSelect(true); } return this; }; /** * Retrieves the items which should be included in navigation. * * @private * @returns {array} aItemsToNavigate */ Breadcrumbs.prototype._getItemsToNavigate = function () { var aItemsToNavigate = this._getControlsForBreadcrumbTrail().slice(), oSelect = this._getSelect(); if (oSelect.getVisible()) { aItemsToNavigate.unshift(oSelect); } return aItemsToNavigate; }; Breadcrumbs.prototype._getItemNavigation = function () { if (!this._itemNavigation) { this._itemNavigation = new ItemNavigation(); } return this._itemNavigation; }; Breadcrumbs.prototype._destroyItemNavigation = function () { if (this._itemNavigation) { this.removeEventDelegate(this._itemNavigation); this._itemNavigation.destroy(); this._itemNavigation = null; } }; /** * Configures the Keyboard handling for the control * * @private * @returns {object} this */ Breadcrumbs.prototype._configureKeyboardHandling = function () { var oItemNavigation = this._getItemNavigation(), iSelectedDomIndex = -1, aItemsToNavigate = this._getItemsToNavigate(), aNavigationDomRefs = []; if (aItemsToNavigate.length === 0) { return; } aItemsToNavigate.forEach(function (oItem, iIndex) { if (iIndex === 0) { oItem.$().attr("tabIndex", "0"); } oItem.$().attr("tabIndex", "-1"); aNavigationDomRefs.push(oItem.getDomRef()); }); this.addDelegate(oItemNavigation); oItemNavigation.setDisabledModifiers({ sapnext : ["alt"], sapprevious : ["alt"], saphome : ["alt"], sapend : ["alt"] }); oItemNavigation.setCycling(false); oItemNavigation.setPageSize(Breadcrumbs.PAGEUP_AND_PAGEDOWN_JUMP_SIZE); oItemNavigation.setRootDomRef(this.getDomRef()); oItemNavigation.setItemDomRefs(aNavigationDomRefs); oItemNavigation.setSelectedIndex(iSelectedDomIndex); return this; }; /** * Every time a control is inserted in the breadcrumb, it must be monitored for size/visibility changes * @param oControl * @private */ Breadcrumbs.prototype._registerControlListener = function (oControl) { if (oControl) { oControl.attachEvent("_change", this._resetControl, this); } }; /** * Each time a control is removed from the breadcrumb, detach listeners * @param oControl * @private */ Breadcrumbs.prototype._deregisterControlListener = function (oControl) { if (oControl) { oControl.detachEvent("_change", this._resetControl, this); } }; Breadcrumbs.prototype.setCurrentLocationText = function (sText) { var oCurrentLocation = this._getCurrentLocation(), vResult = this.setProperty("currentLocationText", sText, true); if (oCurrentLocation.getText() !== sText) { oCurrentLocation.setText(sText); this._resetControl(); } return vResult; }; /** * Resets all of the internally cached values used by the control and invalidates it * * @returns {object} this * @private */ Breadcrumbs.prototype._resetControl = function () { this._aControlInfo = null; this._iContainerSize = null; this._bControlsInfoCached = null; this._bControlDistributionCached = null; this._oDistributedControls = null; if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } this.removeDelegate(this._getItemNavigation()); this.invalidate(this); return this; }; return Breadcrumbs; });