@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,227 lines (1,041 loc) • 83.8 kB
JavaScript
/*!
* 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.NavContainer.
sap.ui.define([
'./library',
"sap/ui/core/Configuration",
'sap/ui/core/Control',
"sap/ui/core/ControlBehavior",
"sap/ui/core/Element",
'sap/ui/core/RenderManager',
'./NavContainerRenderer',
"sap/ui/thirdparty/jquery",
"sap/base/Log",
// jQuery Plugin "firstFocusableDomRef"
"sap/ui/dom/jquery/Focusable"
], function(
library,
Configuration,
Control,
ControlBehavior,
Element,
RenderManager,
NavContainerRenderer,
jQuery,
Log
) {
"use strict";
/**
* Constructor for a new <code>NavContainer</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
* Handles hierarchical navigation between Pages or other fullscreen controls.
*
* All children of this control receive navigation events, such as {@link sap.m.NavContainerChild#event:BeforeShow BeforeShow},
* they are documented in the pseudo interface {@link sap.m.NavContainerChild sap.m.NavContainerChild}.
*
* @see {@link topic:a4afb138acf64a61a038aa5b91a4f082 Nav Container}
*
* @extends sap.ui.core.Control
*
* @author SAP SE
* @version 1.146.0
*
* @constructor
* @public
* @alias sap.m.NavContainer
*/
var NavContainer = Control.extend("sap.m.NavContainer", /** @lends sap.m.NavContainer.prototype */ {
metadata: {
interfaces: [
"sap.ui.core.IPlaceholderSupport"
],
library: "sap.m",
properties: {
/**
* Determines whether the initial focus is set automatically on first rendering and after navigating to a new page.
* This is useful when on touch devices the keyboard pops out due to the focus being automatically set on an input field.
* If necessary, the <code>AfterShow</code> event can be used to focus another element, only if <code>autoFocus</code> is set to <code>false</code>.
*
* <b>Note:</b> The following scenarios are possible, depending on where the focus
* was before navigation to a new page:
* <ul><li>If <code>autoFocus</code> is set to <code>true</code> and the focus was
* inside the current page, the focus will be moved automatically on the new page.</li>
* <li>If <code>autoFocus</code> is set to <code>false</code> and the focus was inside
* the current page, the focus will disappear.
* <li>If the focus was outside the current page, after the navigation it will remain
* unchanged regardless of what is set to the <code>autoFocus</code> property.</li>
* <li>If the <code>autoFocus</code> is set to <code>false</code> and at the same time another wrapping
* control has its own logic for focus restoring upon rerendering, the focus will still appear.</li></ul>
*
* @since 1.30
*/
autoFocus: {type: "boolean", group: "Behavior", defaultValue: true},
/**
* The height of the NavContainer. Can be changed when the NavContainer should not cover the whole available area.
*/
height: {type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue: '100%'},
/**
* The width of the NavContainer. Can be changed when the NavContainer should not cover the whole available area.
*/
width: {type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue: '100%'},
/**
* Whether the NavContainer is visible.
*/
visible: {type: "boolean", group: "Appearance", defaultValue: true},
/**
* The type of the transition/animation to apply when "to()" is called without defining a transition type to use. The default is "slide". Other options are: "baseSlide", "fade", "flip" and "show" - and the names of any registered custom transitions.
* @since 1.7.1
*/
defaultTransitionName: {type: "string", group: "Appearance", defaultValue: "slide"}
},
defaultAggregation: "pages",
aggregations: {
/**
* The content entities between which this NavContainer navigates. These can be of type sap.m.Page, sap.ui.core.mvc.View, sap.m.Carousel or any other control with fullscreen/page semantics.
*
* These aggregated controls will receive navigation events like {@link sap.m.NavContainerChild#event:BeforeShow BeforeShow}, they are documented in the pseudo interface {@link sap.m.NavContainerChild sap.m.NavContainerChild}
*/
pages: {type: "sap.ui.core.Control", multiple: true, singularName: "page"}
},
associations: {
/**
* This association can be used to define which page is displayed initially. If the given page does not exist or no page is given, the first page which has been added is considered as initial page.
* This value should be set initially and not set/modified while the application is running.
*
* This could be used not only for the initial display, but also if the user wants to navigate "up to top", so this page serves as a sort of "home/root page".
*/
initialPage: {type: "sap.ui.core.Control", multiple: false}
},
events: {
/**
* The event is fired when navigation between two pages has been triggered (before any events to the child controls are fired).
* The transition (if any) to the new page has not started yet.
* This event can be aborted by the application with preventDefault(), which means that there will be no navigation.
* @since 1.7.1
*/
navigate: {
allowPreventDefault: true,
parameters: {
/**
* The page which was shown before the current navigation.
*/
from: {type: "sap.ui.core.Control"},
/**
* The ID of the page which was shown before the current navigation.
*/
fromId: {type: "string"},
/**
* The page which will be shown after the current navigation.
*/
to: {type: "sap.ui.core.Control"},
/**
* The ID of the page which will be shown after the current navigation.
*/
toId: {type: "string"},
/**
* Whether the "to" page (more precisely: a control with the ID of the page which is currently navigated to) has not been shown/navigated to before.
*/
firstTime: {type: "boolean"},
/**
* Whether this is a forward navigation, triggered by "to()".
*/
isTo: {type: "boolean"},
/**
* Whether this is a back navigation, triggered by "back()".
*/
isBack: {type: "boolean"},
/**
* Whether this is a navigation to the root page, triggered by "backToTop()".
*/
isBackToTop: {type: "boolean"},
/**
* Whether this was a navigation to a specific page, triggered by "backToPage()".
* @since 1.7.2
*/
isBackToPage: {type: "boolean"},
/**
* How the navigation was triggered, possible values are "to", "back", "backToPage", and "backToTop".
*/
direction: {type: "string"}
}
},
/**
* The event is fired when navigation between two pages has completed (once all events to the child controls have been fired).
* In case of animated transitions this event is fired with some delay after the "navigate" event.
* This event is only fired if the DOM ref of the <code>NavContainer</code> is available.
* If the DOM ref is not available, the <code>navigationFinished</code> event should be used instead.
* @since 1.7.1
*/
afterNavigate: {
parameters: {
/**
* The page which had been shown before navigation.
*/
from: {type: "sap.ui.core.Control"},
/**
* The ID of the page which had been shown before navigation.
*/
fromId: {type: "string"},
/**
* The page which is now shown after navigation.
*/
to: {type: "sap.ui.core.Control"},
/**
* The ID of the page which is now shown after navigation.
*/
toId: {type: "string"},
/**
* Whether the "to" page (more precisely: a control with the ID of the page which has been navigated to) had not been shown/navigated to before.
*/
firstTime: {type: "boolean"},
/**
* Whether was a forward navigation, triggered by "to()".
*/
isTo: {type: "boolean"},
/**
* Whether this was a back navigation, triggered by "back()".
*/
isBack: {type: "boolean"},
/**
* Whether this was a navigation to the root page, triggered by "backToTop()".
*/
isBackToTop: {type: "boolean"},
/**
* Whether this was a navigation to a specific page, triggered by "backToPage()".
* @since 1.7.2
*/
isBackToPage: {type: "boolean"},
/**
* How the navigation was triggered, possible values are "to", "back", "backToPage", and "backToTop".
*/
direction: {type: "string"}
}
},
/**
* The event is fired when navigation between two pages has completed regardless of whether the DOM is ready or not.
* This event is useful when performing navigation without/before rendering of the <code>NavContainer</code>.
* Keep in mind that the DOM is not guaranteed to be ready when this event is fired.
* @since 1.111.0
*/
navigationFinished: {
parameters: {
/**
* The page which had been shown before navigation.
*/
from: {type: "sap.ui.core.Control"},
/**
* The ID of the page which had been shown before navigation.
*/
fromId: {type: "string"},
/**
* The page which is now shown after navigation.
*/
to: {type: "sap.ui.core.Control"},
/**
* The ID of the page which is now shown after navigation.
*/
toId: {type: "string"},
/**
* Whether the "to" page (more precisely: a control with the ID of the page which has been navigated to) had not been shown/navigated to before.
*/
firstTime: {type: "boolean"},
/**
* Whether was a forward navigation, triggered by "to()".
*/
isTo: {type: "boolean"},
/**
* Whether this was a back navigation, triggered by "back()".
*/
isBack: {type: "boolean"},
/**
* Whether this was a navigation to the root page, triggered by "backToTop()".
*/
isBackToTop: {type: "boolean"},
/**
* Whether this was a navigation to a specific page, triggered by "backToPage()".
*/
isBackToPage: {type: "boolean"},
/**
* How the navigation was triggered, possible values are "to", "back", "backToPage", and "backToTop".
*/
direction: {type: "string"}
}
}
}
},
renderer: NavContainerRenderer
});
// Delegate registered by the NavContainer#showPlaceholder function
var oPlaceholderDelegate = {
"onAfterRendering": function() {
if (this._placeholder) {
this._placeholder.show(this);
}
}
};
var fnGetDelay = function (iDelay) {
var sAnimationMode = ControlBehavior.getAnimationMode(),
bUseAnimations = sAnimationMode !== Configuration.AnimationMode.none && sAnimationMode !== Configuration.AnimationMode.minimal;
return bUseAnimations ? iDelay : 0;
},
fnHasParent = function(oControl) {
return !!(oControl && oControl.getParent());
},
fnSetAnimationDirection = function (oPage, sDirection) {
if (fnHasParent(oPage)) {
oPage.$().css({
'-webkit-animation-direction': sDirection,
'animation-direction': sDirection
});
}
};
NavContainer.TransitionDirection = {
BACK: "back",
TO: "to"
};
NavContainer.prototype.init = function () {
this._pageStack = [];
this._aQueue = [];
this._mVisitedPages = {};
this._mFocusObject = {};
this._iTransitionsCompleted = 0; // to track proper callback at the end of transitions
this._bNeverRendered = true;
this._bNavigating = false;
this._bRenderingInProgress = false;
};
NavContainer.prototype.exit = function () {
this._mFocusObject = null; // allow partial garbage collection when app code leaks the NavContainer (based on a real scenario)
this._placeholder = undefined;
};
NavContainer.prototype.onBeforeRendering = function () {
var pageToRenderFirst = this.getCurrentPage();
// for the very first rendering
if (this._bNeverRendered && pageToRenderFirst) { // will be set to false after rendering
// special handling for the page which is the first one which is rendered in this NavContainer
var pageId = pageToRenderFirst.getId();
if (!this._mVisitedPages[pageId]) { // events could already be fired by initial "to()" call
this._mVisitedPages[pageId] = true;
var oNavInfo = {
from: null,
fromId: null,
to: pageToRenderFirst,
toId: pageId,
firstTime: true,
isTo: false,
isBack: false,
isBackToPage: false,
isBackToTop: false,
direction: "initial"
};
var oEvent = jQuery.Event("BeforeFirstShow", oNavInfo);
oEvent.srcControl = this;
oEvent.data = this._oToDataBeforeRendering || {};
oEvent.backData = {};
pageToRenderFirst._handleEvent(oEvent);
oEvent = jQuery.Event("BeforeShow", oNavInfo);
oEvent.srcControl = this;
oEvent.data = this._oToDataBeforeRendering || {};
oEvent.backData = {};
pageToRenderFirst._handleEvent(oEvent);
}
}
};
NavContainer.prototype.onAfterRendering = function () {
var pageToRenderFirst = this.getCurrentPage(),
focusObject, oNavInfo, pageId, oEvent;
// for the very first rendering
if (this._bNeverRendered && pageToRenderFirst) {
this._bNeverRendered = false;
delete this._bNeverRendered;
// special handling for the page which is the first one which is rendered in this NavContainer
pageId = pageToRenderFirst.getId();
// set focus to first focusable object
// when NavContainer is inside a popup, the focus is managed by the popup and shouldn't be set here
if (!this._isInsideAPopup() && this.getAutoFocus()) {
focusObject = NavContainer._applyAutoFocusTo(pageId);
if (focusObject) {
this._mFocusObject[pageId] = focusObject;
}
}
oNavInfo = {
from: null,
fromId: null,
to: pageToRenderFirst,
toId: pageId,
firstTime: true,
isTo: false,
isBack: false,
isBackToTop: false,
isBackToPage: false,
direction: "initial"
};
oEvent = jQuery.Event("AfterShow", oNavInfo);
oEvent.srcControl = this;
oEvent.data = this._oToDataBeforeRendering || {};
oEvent.backData = {};
pageToRenderFirst._handleEvent(oEvent);
}
};
/**
* Returns the page that should act as initial page - either the one designated as such, or, if it does not exist,
* the first page (index 0 in the aggregation). Returns null if no page is aggregated.
*
* @private
*/
NavContainer.prototype._getActualInitialPage = function () {
var pageId = this.getInitialPage();
if (pageId) {
var page = Element.getElementById(pageId);
if (page) {
return page;
} else {
Log.error("NavContainer: control with ID '" + pageId + "' was set as 'initialPage' but was not found as a DIRECT child of this NavContainer (number of current children: " + this.getPages().length + ").");
}
}
var pages = this.getPages();
return (pages.length > 0 ? pages[0] : null);
};
//*** API methods ***
/**
* Returns the control with the given ID from the <code>pages</code> aggregation (if available).
*
* @param {string} pageId The ID of the aggregated control to find
* @public
* @returns {sap.ui.core.Control|null} The control with the given ID or <code>null</code> if it doesn't exist
*/
NavContainer.prototype.getPage = function (pageId) {
var aPages = this.getPages();
for (var i = 0; i < aPages.length; i++) {
if (aPages[i] && (aPages[i].getId() == pageId)) {
return aPages[i];
}
}
return null;
};
NavContainer.prototype._ensurePageStackInitialized = function (data) {
if (this._pageStack.length === 0) {
var page = this._getActualInitialPage(); // TODO: with bookmarking / deep linking this is the initial, but not the "home"/root page
if (page) {
this._pageStack.push({id: page.getId(), isInitial: true, data: data || {}});
}
}
return this._pageStack;
};
/**
* Returns the currently displayed page-level control.
*
* <b>Note:</b> Returns <code>undefined</code> if no page has been added yet,
* otherwise returns an instance of <code>sap.m.Page</code>,
* <code>sap.ui.core.mvc.View</code>, <code>sap.m.Carousel</code> or whatever is aggregated.
*
* @public
* @returns {sap.ui.core.Control} The current page
*/
NavContainer.prototype.getCurrentPage = function () {
var stack = this._ensurePageStackInitialized();
if (stack.length >= 1) {
return this.getPage(stack[stack.length - 1].id);
} else {
Log.warning(this + ": page stack is empty but should have been initialized - application failed to provide a page to display");
return undefined;
}
};
/**
* Returns the previous page (the page from which the user drilled down to the current page with "to()").
*
* <b>Note:</b> this is not the page which the user has seen before, but the page which is the target of the next "back()" navigation.
* If there is no previous page, <code>undefined</code> is returned.
*
* @public
* @since 1.7.1
* @returns {sap.ui.core.Control} The previous page
*/
NavContainer.prototype.getPreviousPage = function () {
var stack = this._ensurePageStackInitialized();
if (stack.length > 1) {
return this.getPage(stack[stack.length - 2].id);
} else if (stack.length == 1) { // the current one is the only page on the stack
return undefined;
} else {
Log.warning(this + ": page stack is empty but should have been initialized - application failed to provide a page to display");
}
};
/**
* Returns whether the current page is the top/initial page.
*
* <b>Note:</b> going to the initial page again with a row of "to" navigations causes the initial page to be displayed again,
* but logically one is not at the top level, so this method returns "false" in this case.
*
* @public
* @returns {boolean} Whether the current page is a top page
*/
NavContainer.prototype.currentPageIsTopPage = function () {
var stack = this._ensurePageStackInitialized();
return (stack.length === 1);
};
/**
* Inserts the page/control with the specified ID into the navigation history stack of the NavContainer.
*
* This can be used for deep-linking when the user directly reached a drilldown detail page using a bookmark and then wants to navigate up in the drilldown hierarchy. Normally such a back navigation would not be possible because there is no previous page in the NavContainer's history stack.
*
* @param {string} pageId
* The ID of the control/page/screen which is inserted into the history stack. The respective control must be aggregated by the NavContainer, otherwise this will cause an error.
* @param {string} [transitionName=slide]
* The type of the transition/animation which would have been used to navigate from the (inserted) previous page to the current page. When navigating back, the inverse animation will be applied.
* Options are "slide" (horizontal movement from the right), "baseSlide", "fade", "flip", and "show" and the names of any registered custom transitions.
* @param {object} data This optional object can carry any payload data which would have been given to the inserted previous page if the user would have done a normal forward navigation to it.
* @public
* @since 1.16.1
* @returns {this} The <code>sap.m.NavContainer</code> instance
*/
NavContainer.prototype.insertPreviousPage = function (pageId, transitionName, data) {
var stack = this._ensurePageStackInitialized();
if (this._pageStack.length > 0) {
var index = stack.length - 1;
var pageInfo = {id: pageId, transition: transitionName, data: data};
if (index === 0) {
pageInfo.isInitial = true;
delete stack[stack.length - 1].isInitial;
}
stack.splice(index, 0, pageInfo);
} else {
Log.warning(this + ": insertPreviousPage called with empty page stack; ignoring");
}
return this;
};
NavContainer._applyAutoFocusTo = function (sId) {
var focusSubjectDomRef = jQuery(document.getElementById(sId)).firstFocusableDomRef();
if (focusSubjectDomRef) {
focusSubjectDomRef.focus();
}
return focusSubjectDomRef;
};
NavContainer.prototype._applyAutoFocus = function (oNavInfo) {
var sPageId = oNavInfo.toId,
domRefRememberedFocusSubject,
bNavigatingBackToPreviousLocation = oNavInfo.isBack || oNavInfo.isBackToPage || oNavInfo.isBackToTop;
// BCP: 1780071998 - If focus is not inside the From page we don't do any focus manipulation
if (!oNavInfo.bFocusInsideFromPage) {
return;
}
// check navigation type (backward or forward)
if (bNavigatingBackToPreviousLocation) {
// set focus to the remembered focus object if available
// if no focus was set set focus to first focusable object in "to page"
domRefRememberedFocusSubject = this._mFocusObject != null ? this._mFocusObject[sPageId] : null;
if (domRefRememberedFocusSubject) {
domRefRememberedFocusSubject.focus();
} else {
NavContainer._applyAutoFocusTo(sPageId);
}
} else if (oNavInfo.isTo) {
// set focus to first focusable object in "to page"
NavContainer._applyAutoFocusTo(sPageId);
}
};
NavContainer.prototype._afterNavigation = function (oNavInfo, oData, oBackData) {
var oEvent = jQuery.Event("AfterShow", oNavInfo);
oEvent.data = oData || {};
oEvent.backData = oBackData || {};
oEvent.srcControl = this; // store the element on the event (aligned with jQuery syntax)
oNavInfo.to._handleEvent(oEvent);
oEvent = jQuery.Event("AfterHide", oNavInfo);
oEvent.srcControl = this; // store the element on the event (aligned with jQuery syntax)
oNavInfo.from._handleEvent(oEvent);
// BCP: 1870488179 - We call _applyAutoFocus only if autoFocus property is true
if (this.getAutoFocus()) {
this._applyAutoFocus(oNavInfo);
}
this.enhancePagesAccessibility();
this.fireNavigationFinished(oNavInfo);
this.fireAfterNavigate(oNavInfo);
this._dequeueNavigation();
};
NavContainer.prototype._afterTransitionCallback = function (oNavInfo, oData, oBackData) {
this._iTransitionsCompleted++;
this._bNavigating = false;
// TODO: destroy HTML? Remember to destroy ALL HTML of several pages when backToTop has been called
Log.info(this + ": _afterTransitionCallback called, to: " + oNavInfo.toId);
if (oNavInfo.to.hasStyleClass("sapMNavItemHidden")) {
Log.warning(this.toString() + ": target page '" + oNavInfo.toId + "' still has CSS class 'sapMNavItemHidden' after transition. This should not be the case, please check the preceding log statements.");
oNavInfo.to.removeStyleClass("sapMNavItemHidden");
}
this._afterNavigation(oNavInfo, oData, oBackData);
};
NavContainer.prototype.enhancePagesAccessibility = function () {
var oCurrentPage = this.getCurrentPage();
this.getPages().forEach(function (oPage) {
var oFocusDomRef = oPage?.getFocusDomRef();
if (oCurrentPage === oPage) {
oFocusDomRef?.removeAttribute("aria-hidden");
} else {
oFocusDomRef?.setAttribute("aria-hidden", true);
}
});
};
NavContainer.prototype._dequeueNavigation = function () {
var fnNavigate = this._aQueue.shift();
if (typeof fnNavigate === "function") {
fnNavigate();
}
};
/**
* Checks whether a page is in the history stack or not
* @param pageId
* @returns {boolean}
* @private
*/
NavContainer.prototype._isInPageStack = function (pageId) {
return this._pageStack.some(function (oPage) {
return oPage.id === pageId;
});
};
/**
* Navigates back to a page, if the page is in the history stack. Otherwise, navigates to it.
*
* This method can be used to navigate to previously visited pages which are however not in the stack any more.
* Such a situation can be observed when navigating back to a page several levels back.
* @param pageId
* @param transitionName
* @param data
* @param oTransitionParameters
* @private
*/
NavContainer.prototype._safeBackToPage = function (pageId, transitionName, data, oTransitionParameters) {
var oCurrentPage;
if (!this.getPage(pageId)) {
return this;
}
oCurrentPage = this.getCurrentPage();
if (oCurrentPage && oCurrentPage.getId() === pageId) {
return this;
}
if (this._isInPageStack(pageId)) {
return this.backToPage(pageId, data, oTransitionParameters);
} else {
// when this method calls "to", animation should be "back"
data = data || {};
data.safeBackToPage = true;
return this.to(pageId, transitionName, data, oTransitionParameters);
}
};
/**
* Check if the current focused element is a HTML child element of the control passed.
* @param {sap.ui.core.Control} oControl instance of control
* @returns {boolean} If the focus is in one of the control's child HTML elements
* @private
*/
NavContainer.prototype._isFocusInControl = function (oControl) {
return jQuery(document.activeElement).closest(oControl.$()).length > 0;
};
/**
* Navigates to the next page (with drill-down semantic) with the given (or default) animation. This creates a new history item inside the NavContainer and allows going back.
*
* Note that any modifications to the target page (like setting its title, or anything else that could cause a re-rendering) should be done BEFORE calling to(), in order to avoid unwanted side effects, e.g. related to the page animation.
*
* Available transitions currently include "slide" (default), "baseSlide", "fade", "flip", and "show". None of these is currently making use of any given transitionParameters.
*
* Calling this navigation method triggers first the (cancelable) "navigate" event on the NavContainer, then the "BeforeHide" pseudo event on the source page and "BeforeFirstShow" (if applicable) and"BeforeShow" on the target page. Later - after the transition has completed - the "AfterShow" pseudo event is triggered on the target page and "AfterHide" on the page which has been left. The given data object is available in the "BeforeFirstShow", "BeforeShow" and "AfterShow" event object as "data" property.
*
* @param {string | sap.ui.core.Control} vPageIdOrControl
* The screen to which drilldown should happen. The ID or the control itself can be given.
* @param {string} [sTransitionName=slide]
* The type of the transition/animation to apply. Options are "slide" (horizontal movement from the right), "baseSlide", "fade", "flip", and "show"
* and the names of any registered custom transitions.
*
* None of the standard transitions is currently making use of any given transition parameters.
* @param {object} [oData={}]
* Since version 1.7.1. This optional object can carry any payload data which should be made available to the target page.
* The "BeforeShow" event on the target page will contain this data object as "data" property.
* Use case: in scenarios where the entity triggering the navigation can or should not directly initialize the target page, it can fill this object and the target page itself (or a listener on it) can take over the initialization, using the given data.
*
* When the <code>oTransitionParameters</code> parameter is used, this <code>oData</code> parameter must also be given (either as object or as <code>null</code> or <code>undefined</code>) in order to have a proper parameter order.
* @param {object} [oTransitionParameters={}]
* Since version 1.7.1. This optional object can contain additional information for the transition function, like the DOM element which triggered the transition or the desired transition duration.
*
* For a proper parameter order, the <code>oData</code> parameter must be given when the <code>oTransitionParameters</code> parameter is used (it can be given as <code>null</code> or <code>undefined</code>).
*
* NOTE: it depends on the transition function how the object should be structured and which parameters are actually used to influence the transition.
* The "show", "slide", "baseSlide" and "fade" transitions do not use any parameter.
* @public
* @returns {this} The <code>sap.m.NavContainer</code> instance
*/
NavContainer.prototype.to = function (vPageIdOrControl, sTransitionName, oData, oTransitionParameters, bFromQueue) {
if (vPageIdOrControl instanceof Control) {
vPageIdOrControl = vPageIdOrControl.getId();
}
// fix parameters
if (typeof (sTransitionName) !== "string") {
// sTransitionName is omitted, shift parameters
oTransitionParameters = oData;
oData = sTransitionName;
}
sTransitionName = sTransitionName || this.getDefaultTransitionName();
oTransitionParameters = oTransitionParameters || {};
oData = oData || {};
var oFromPageInfo = {id: vPageIdOrControl, transition: sTransitionName, data: oData};
// make sure the initial page is on the stack
this._ensurePageStackInitialized(oData);
//add to the queue before checking the current page, because this might change
if (this._bNavigating) {
Log.info(this.toString() + ": Cannot navigate to page " + vPageIdOrControl + " because another navigation is already in progress. - navigation will be executed after the previous one");
this._aQueue.push(jQuery.proxy(function () {
this.to(vPageIdOrControl, sTransitionName, oData, oTransitionParameters, true);
}, this));
return this;
}
// If to is called before rendering, remember the oData so we can pass it to the events as soon as the navContainer gets rendered
if (this._bNeverRendered) {
this._oToDataBeforeRendering = oData;
}
var oFromPage = this.getCurrentPage();
if (oFromPage && (oFromPage.getId() === vPageIdOrControl)) { // cannot navigate to the page that is already current
Log.warning(this.toString() + ": Cannot navigate to page " + vPageIdOrControl + " because this is the current page.");
if (bFromQueue) {
this._dequeueNavigation();
}
// In an application when the first page is loaded its transition is not set and we set it here.
if (this._pageStack.length === 1) {
this._pageStack[0].transition = oFromPageInfo.transition;
}
return this;
}
var oToPage = this.getPage(vPageIdOrControl);
if (oToPage) {
if (!oFromPage) {
Log.warning("Navigation triggered to page with ID '" + vPageIdOrControl + "', but the current page is not known/aggregated by " + this);
return this;
}
var oNavInfo = {
from: oFromPage,
fromId: oFromPage.getId(),
to: oToPage,
toId: vPageIdOrControl,
firstTime: !this._mVisitedPages[vPageIdOrControl],
isTo: true,
isBack: false,
isBackToTop: false,
isBackToPage: false,
direction: "to",
bFocusInsideFromPage: this._isFocusInControl(oFromPage)
};
if (oNavInfo.bFocusInsideFromPage) {
// remember the focused object in "from page"
this._mFocusObject[oFromPage.getId()] = document.activeElement;
}
var bContinue = this.fireNavigate(oNavInfo);
if (bContinue) { // ok, let's do the navigation
library.closeKeyboard();
// TODO: let one of the pages also cancel navigation?
var oEvent = jQuery.Event("BeforeHide", oNavInfo);
oEvent.srcControl = this; // store the element on the event (aligned with jQuery syntax)
// no oData needed for hiding
oFromPage._handleEvent(oEvent);
if (!this._mVisitedPages[vPageIdOrControl]) { // if this page has not been shown before
oEvent = jQuery.Event("BeforeFirstShow", oNavInfo);
oEvent.srcControl = this;
oEvent.data = oData || {};
oEvent.backData = {};
oToPage._handleEvent(oEvent);
}
oEvent = jQuery.Event("BeforeShow", oNavInfo);
oEvent.srcControl = this;
oEvent.data = oData || {};
oEvent.backData = {};
oToPage._handleEvent(oEvent);
this._pageStack.push(oFromPageInfo); // this actually causes/is the navigation
Log.info(this.toString() + ": navigating to page '" + vPageIdOrControl + "': " + oToPage.toString());
this._mVisitedPages[vPageIdOrControl] = true;
if (!this.getDomRef()) { // the wanted animation has been recorded, but when the NavContainer is not rendered, we cannot animate, so just return
Log.info("'Hidden' 'to' navigation in not-rendered NavContainer " + this.toString());
this.fireNavigationFinished(oNavInfo);
// BCP: 1680140633 - Firefox issue
if (this._bRenderingInProgress) {
setTimeout(this.invalidate.bind(this), 0);
}
return this;
}
// render the page that should get visible
var oToPageDomRef;
if (!(oToPageDomRef = oToPage.getDomRef()) || oToPageDomRef.parentNode != this.getDomRef() || RenderManager.isPreservedContent(oToPageDomRef)) {
oToPage.addStyleClass("sapMNavItemRendering");
Log.debug("Rendering 'to' page '" + oToPage.toString() + "' for 'to' navigation");
var rm = new RenderManager().getInterface();
rm.render(oToPage, this.getDomRef());
rm.destroy();
oToPage.addStyleClass("sapMNavItemHidden").removeStyleClass("sapMNavItemRendering");
}
var oTransition = NavContainer.transitions[sTransitionName] || NavContainer.transitions["slide"];
// Track proper invocation of the callback TODO: only do this during development?
var iCompleted = this._iTransitionsCompleted;
var that = this;
window.setTimeout(function () {
if (that && (that._iTransitionsCompleted < iCompleted + 1)) {
Log.warning("Transition '" + sTransitionName + "' 'to' was triggered five seconds ago, but has not yet invoked the end-of-transition callback.");
}
}, fnGetDelay(5000));
this._bNavigating = true;
// check both params since they might have shifted
var sTransitionDirection = (oData.safeBackToPage || oTransitionParameters.safeBackToPage) ? "back" : "to";
this._cacheTransitionInfo(sTransitionName, sTransitionDirection);
oTransition[sTransitionDirection].call(this, oFromPage, oToPage, jQuery.proxy(function () {
this._afterTransitionCallback(oNavInfo, oData);
}, this), oTransitionParameters); // trigger the transition
} else {
Log.info("Navigation to page with ID '" + vPageIdOrControl + "' has been aborted by the application");
}
} else {
Log.warning("Navigation triggered to page with ID '" + vPageIdOrControl + "', but this page is not known/aggregated by " + this);
}
return this;
};
/**
* Navigates back one level. If already on the initial page and there is no place to go back, nothing happens.
*
* Calling this navigation method triggers first the (cancelable) "navigate" event on the NavContainer, then the "BeforeHide" pseudo event on the source page and "BeforeFirstShow" (if applicable) and"BeforeShow" on the target page. Later - after the transition has completed - the "AfterShow" pseudo event is triggered on the target page and "AfterHide" on the page which has been left. The given backData object is available in the "BeforeFirstShow", "BeforeShow" and "AfterShow" event object as "data" property. The original "data" object from the "to" navigation is also available in these event objects.
*
* @param {object} [backData={}]
* Since version 1.7.1. This optional object can carry any payload data which should be made available to the target page of the back navigation. The event on the target page will contain this data object as "backData" property. (The original data from the "to()" navigation will still be available as "data" property.)
*
* In scenarios where the entity triggering the navigation can or should not directly initialize the target page, it can fill this object and the target page itself (or a listener on it) can take over the initialization, using the given data.
* For back navigation this can be used e.g. when returning from a detail page to transfer any settings done there.
*
* When the <code>oTransitionParameters</code> parameter is used, this <code>backData</code> parameter must also be given (either as object or as <code>null</code> or <code>undefined</code>) in order to have a proper parameter order.
* @param {object} [oTransitionParameters={}]
* Since version 1.7.1. This optional object can give additional information to the transition function, like the DOM element which triggered the transition or the desired transition duration.
* The animation type can NOT be selected here - it is always the inverse of the "to" navigation.
*
* In order to use the <code>oTransitionParameters<code> parameter, the <code>backData</code> parameter must be used (at least <code>null</code> or <code>undefined</code> must be given) for a proper parameter order.
*
* NOTE: it depends on the transition function how the object should be structured and which parameters are actually used to influence the transition.
* @public
* @returns {this} The <code>sap.m.NavContainer</code> instance
*/
NavContainer.prototype.back = function (backData, oTransitionParameters) {
this._backTo("back", backData, oTransitionParameters);
return this;
};
/**
* Navigates back to the nearest previous page in the NavContainer history with the given ID. If there is no such page among the previous pages, nothing happens.
* The transition effect which had been used to get to the current page is inverted and used for this navigation.
*
* Calling this navigation method triggers first the (cancelable) "navigate" event on the NavContainer, then the "BeforeHide" pseudo event on the source page and "BeforeFirstShow" (if applicable) and"BeforeShow" on the target page. Later - after the transition has completed - the "AfterShow" pseudo event is triggered on the target page and "AfterHide" on the page which has been left. The given backData object is available in the "BeforeFirstShow", "BeforeShow" and "AfterShow" event object as "data" property. The original "data" object from the "to" navigation is also available in these event objects.
*
* @param {string} pageId
* The ID of the screen to which back navigation should happen. The ID or the control itself can be given. The nearest such page among the previous pages in the history stack will be used.
* @param {object} [backData={}]
* This optional object can carry any payload data which should be made available to the target page of the "backToPage" navigation. The event on the target page will contain this data object as "backData" property.
*
* When the <code>oTransitionParameters</code> parameter is used, this <code>backData</code> parameter must also be given (either as object or as <code>null</code> or <code>undefined</code>) in order to have a proper parameter order.
* @param {object} [oTransitionParameters={}]
* This optional object can give additional information to the transition function, like the DOM element which triggered the transition or the desired transition duration.
* The animation type can NOT be selected here - it is always the inverse of the "to" navigation.
*
* In order to use the <code>oTransitionParameters<code> parameter, the <code>backData</code> parameter must be used (at least <code>null</code> or <code>undefined</code> must be given) for a proper parameter order.
*
* NOTE: it depends on the transition function how the object should be structured and which parameters are actually used to influence the transition.
* @public
* @since 1.7.2
* @returns {this} The <code>sap.m.NavContainer</code> instance
*/
NavContainer.prototype.backToPage = function (pageId, backData, oTransitionParameters) {
this._backTo("backToPage", backData, oTransitionParameters, pageId);
return this;
};
/**
* Navigates back to the initial/top level (this is the element aggregated as "initialPage", or the first added element). If already on the initial page, nothing happens.
* The transition effect which had been used to get to the current page is inverted and used for this navigation.
*
* Calling this navigation method triggers first the (cancelable) "navigate" event on the NavContainer, then the "BeforeHide" pseudo event on the source page and "BeforeFirstShow" (if applicable) and "BeforeShow" on the target page. Later - after the transition has completed - the "AfterShow" pseudo event is triggered on the target page and "AfterHide" on the page which has been left. The given backData object is available in the "BeforeFirstShow", "BeforeShow" and "AfterShow" event object as "data" property.
*
* @param {object} [backData={}]
* This optional object can carry any payload data which should be made available to the target page of the "backToTop" navigation. The event on the target page will contain this data object as "backData" property.
*
* When the <code>oTransitionParameters</code> parameter is used, this <code>backData</code> parameter must also be given (either as object or as <code>null</code> or <code>undefined</code>) in order to have a proper parameter order.
* @param {object} [oTransitionParameters={}]
* This optional object can give additional information to the transition function, like the DOM element which triggered the transition or the desired transition duration.
* The animation type can NOT be selected here - it is always the inverse of the "to" navigation.
*
* In order to use the <code>oTransitionParameters<code> parameter, the <code>backData</code> parameter must be used (at least <code>null</code> or <code>undefined</code> must be given) for a proper parameter order.
*
* NOTE: it depends on the transition function how the object should be structured and which parameters are actually used to influence the transition.
* @type this
* @public
* @since 1.7.1
*/
NavContainer.prototype.backToTop = function (backData, oTransitionParameters) {
this._backTo("backToTop", backData, oTransitionParameters);
return this;
};
NavContainer.prototype._backTo = function (sType, backData, oTransitionParameters, sRequestedPageId) {
if (this._bNavigating) {
Log.warning(this.toString() + ": Cannot navigate back because another navigation is already in progress. - navigation will be executed after the previous one");
this._aQueue.push(jQuery.proxy(function () {
this._backTo(sType, backData, oTransitionParameters, sRequestedPageId);
}, this));
return this;
}
if (this._pageStack.length <= 1) {
// there is no place to go back
// but then the assumption is that the only page on the stack is the initial one and has not been navigated to. Check this:
if (this._pageStack.length === 1 && !this._pageStack[0].isInitial) {
throw new Error("Initial page not found on the stack. How did this happen?");
}
//clear the navigation queue as there are no other pages
this._aQueue = [];
return this;
} else { // normal back navigation
if (sRequestedPageId instanceof Control) {
sRequestedPageId = sRequestedPageId.getId();
}
var oFromPageInfo = this._pageStack[this._pageStack.length - 1];
var transition = oFromPageInfo.transition;
var oFromPage = this.getPage(oFromPageInfo.id);
var oToPage;
var oToPageData;
if (sType === "backToTop") {
oToPage = this._getActualInitialPage();
oToPageData = null;
} else if (sType === "backToPage") {
var info = this._findClosestPreviousPageInfo(sRequestedPageId);
if (!info) {
Log.error(this.toString() + ": Cannot navigate backToPage('" + sRequestedPageId + "') because target page was not found among the previous pages.");
return this;
}
oToPage = Element.getElementById(info.id);
if (!oToPage) {
Log.error(this.toString() + ": Cannot navigate backToPage('" + sRequestedPageId + "') because target page does not exist anymore.");
return this;
}
oToPageData = info.data;
} else { // normal "back"
oToPage = this.getPreviousPage();
oToPageData = this._pageStack[this._pageStack.length - 2].data;
}
if (!oToPage) {
Log.error("NavContainer back navigation: target page is not defined or not aggregated by this NavContainer. Aborting navigation.");
return;
}
var oToPageId = oToPage.getId();
backData = backData || {};
oTransitionParameters = oTransitionParameters || {};
var oNavInfo = {
from: oFromPage,
fromId: oFromPage.getId(),
to: oToPage,
toId: oToPageId,
firstTime: !this._mVisitedPages[oToPageId],
isTo: false,
isBack: (sType === "back"),
isBackToPage: (sType === "backToPage"),
isBackToTop: (sType === "backToTop"),
direction: sType,
bFocusInsideFromPage: this._isFocusInControl(oFromPage)
};
var bContinue = this.fireNavigate(oNavInfo);
if (bContinue) { // ok, let's do the navigation
library.closeKeyboard();
var oEvent = jQuery.Event("BeforeHide", oNavInfo);
oEvent.srcControl = this; // store the element on the event (aligned with jQuery syntax)
// no data needed for hiding
oFromPage._handleEvent(oEvent);
if (!this._mVisitedPages[oToPageId]) { // if this page has not been shown before
oEvent = jQuery.Event("BeforeFirstShow", oNavInfo);
oEvent.srcControl = this;
oEvent.backData = backData || {};
// the old data from the forward navigation should not exist because there was never a forward navigation
oEvent.data = {};
oToPage._handleEvent(oEvent);
}
oEvent = jQuery.Event("BeforeShow", oNavInfo);
oEvent.srcControl = this;
oEvent.backData = backData || {};
oEvent.data = oToPageData || {}; // the old data from the forward navigation
oToPage._handleEvent(oEvent);
this._pageStack.pop(); // this actually causes/is the navigation
Log.info(this.toString() + ": navigating back to page " + oToPage.toString());
this._mVisitedPages[oToPageId] = true;
if (sType === "backToTop") { // if we should navigate to top, just clean up the whole stack
this._pageStack = [];
Log.info(this.toString() + ": navigating back to top");
this.getCurrentPage(); // this properly restores the initial page on the stack
} else if (sType === "backToPage") {
var aPages = [], interimPage;
while (this._pageStack[this._pageStack.length - 1].id !== sRequestedPageId) { // by now it is guaranteed that we will find it
interimPage = this._pageStack.pop();
aPages.push(interimPage.id);
}
Log.info(this.toString() + ": navigating back to specific page " + oToPage.toString() + " across the pages: " + aPages.join(", "));
}
if (!this.getDomRef()) { // the wanted animation has been recorded, but when the NavContainer is not rendered, we cannot animate, so just return
Log.info("'Hidden' back navigation in not-rendered NavContainer " + this.toString());
this._afterNavigation(oNavInfo, oToPageData, backData);
return this;
}
var oTransition = NavContainer.transitions[transition] || NavContainer.transitions["slide"];
// Track proper invocation of the callback TODO: only do this during development?
var iCompleted = this._iTransitionsCompleted;
var that = this;
window.setTimeout(function () {
if (that && (that._iTransitionsCompleted < iCompleted + 1)) {
Log.warning("Transition '" + transition + "' 'back' was triggered five seconds ago, but has not yet invoked the end-of-transition callback.");
}
}, fnGetDelay(5000));
this._bNavigating = true;
// make sure the to-page is rendered
var oToPageDomRef;
if (!(oToPageDomRef = oToPage.getDomRef()) || oToPageDomRef.parentNode != this.getDomRef() || RenderManager.isPreservedContent(oToPageDomRef)) {
oToPage.addStyleClass("sapMNavItemRendering");
Log.debug("Rendering 'to' page '" + oToPage.toString() + "' for back navigation");
var rm = new RenderManager().getInterface();
var childPos = this.$().children().index(oFromPage.getDomRef());
rm.renderControl(oToPage);
rm.flush(this.getDomRef(), false, childPos);
rm.destroy();
oToPage.addStyleClass("sapMNavItemHidden").removeStyleClass("sapMNavItemRendering");
}
//if the from page and to page are identical, the transition is skipped.
if (oFromPage.getId() === oToPage.getId()) {
Log.info("Transition is skipped when navigating back to the same page instance" + oToPage.toString());
this._afterTransitionCallback(oNavInfo, oToPageData, backData);
return this;
}
this._cacheTransitionInfo(transition, NavContainer.TransitionDirection.BACK);
// trigger the transition
oTransition.back.call(this, oFromPage, oToPage, jQuery.proxy(function () {
this._afterTransitionCallback(oNavInfo, oToPageData, backData);
}, this), oTransitionParameters); // trigger the transition
}
}
return this;
};
NavContainer.prototype._findClosestPreviousPageInfo = function (sRequestedPreviousPageId) {
for (var i = this._pageStack.length - 2; i >= 0; i--) {
var info = this._pageStack[i];
if (info.id === sRequestedPreviousPageId) {
return info;
}
}
return null;
};
NavContainer.prototype._cacheTransitionInfo = function(sTransitionName, sTransitionDirection) {
this._sTransitionName = sTransitionName;
this._sTransitionDirection = sTransitionDirection;
};
NavContainer.prototype._fadeTransition = function(oFromPage, oToPage, fCallback /*, oTransitionParameters is unused */) {
this.oFromPage = oFromPage;
this.oToPage = oToPage;
this.fCallback = fCallback;
this._fadeOutAnimation();
};
NavContainer.prototype._fadeOutAnimation = function() {
var that = this,
oFromPage = this.oFromPage,
oToPage = this.oT