/**
* This component is used in {@link Ext.navigation.View} to control animations in the toolbar. You should never need to
* interact with the component directly, unless you are subclassing it.
* @private
* @author Robert Dougan <rob@sencha.com>
*/
Ext.define('Ext.navigation.Bar', {
extend: 'Ext.Container',
requires: [
'Ext.Button',
'Ext.TitleBar',
'Ext.Spacer'
],
// private
isToolbar: true,
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'toolbar',
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'navigation-bar',
/**
* @cfg {String} ui
* Style options for Toolbar. Either 'light' or 'dark'.
* @accessor
*/
ui: 'dark',
/**
* @cfg {String} title
* The title of the toolbar. You should NEVER set this, it is used internally. You set the title of the
* navigation bar by giving a navigation views children a title configuration.
* @private
* @accessor
*/
title: null,
/**
* @cfg
* @hide
* @accessor
*/
defaultType: 'button',
/**
* @cfg
* @hide
* @accessor
*/
layout: {
type: 'hbox'
},
/**
* @cfg {Array/Object} items The child items to add to this NavigationBar. The {@link #cfg-defaultType} of
* a NavigationBar is {@link Ext.Button}, so you do not need to specify an `xtype` if you are adding
* buttons.
*
* You can also give items a `align` configuration which will align the item to the `left` or `right` of
* the NavigationBar.
* @hide
* @accessor
*/
/**
* @cfg {String} defaultBackButtonText
* The text to be displayed on the back button if:
* a) The previous view does not have a title
* b) The {@link #useTitleForBackButtonText} configuration is true.
* @private
* @accessor
*/
defaultBackButtonText: 'Back',
/**
* @cfg {Object} animation
* @private
* @accessor
*/
animation: {
duration: 300
},
/**
* @cfg {Boolean} useTitleForBackButtonText
* Set to false if you always want to display the {@link #defaultBackButtonText} as the text
* on the back button. True if you want to use the previous views title.
* @private
* @accessor
*/
useTitleForBackButtonText: null,
/**
* @cfg {Ext.navigation.View} view A reference to the navigation view this bar is linked to.
* @private
* @accessor
*/
view: null,
/**
* @cfg {Ext.Button/Object} backButton The configuration for the back button
* @private
* @accessor
*/
backButton: {
align: 'left',
ui: 'back',
hidden: true
}
},
/**
* @event back
* Fires when the back button was tapped.
* @param {Ext.navigation.Bar} this This bar
*/
/**
* The minmum back button width allowed.
* @private
*/
minBackButtonWidth: 80,
constructor: function(config) {
config = config || {};
if (!config.items) {
config.items = [];
}
this.backButtonStack = [];
this.callParent([config]);
},
initialize: function() {
this.on({
painted: 'refreshProxy',
resize: 'refreshProxy'
});
},
/**
* @private
*/
updateView: function(newView) {
var me = this,
backButton = me.getBackButton(),
innerItems, i, backButtonText;
me.getItems();
if (newView) {
//update the back button stack with the current inner items of the view
innerItems = newView.getInnerItems();
for (i = 0; i < innerItems.length; i++) {
me.backButtonStack.push(innerItems[i].config.title || ' ');
}
me.titleComponent.setTitle(me.getTitleText());
backButtonText = me.getBackButtonText();
if (backButtonText) {
backButton.setText(backButtonText);
backButton.show();
}
}
},
/**
* @private
*/
onViewAdd: function(view, item) {
var me = this,
animation = view.getLayout().getAnimation(),
backButtonStack = me.backButtonStack,
animations = [],
hasPrevious, titleText, backButtonText;
me.endAnimation();
backButtonStack.push(item.config.title || ' ');
titleText = me.getTitleText();
backButtonText = me.getBackButtonText();
hasPrevious = backButtonStack.length > 1;
me.refreshNavigationBarProxy();
if (animation && animation.isAnimation && view.isPainted()) {
if (hasPrevious) {
animations = animations.concat(me.pushBackButtonAnimated(backButtonText));
}
animations = animations.concat(me.pushTitleAnimated(titleText));
me.activeAnimations = animations;
}
else {
if (hasPrevious) {
me.pushBackButton(backButtonText);
}
me.pushTitle(titleText);
}
},
/**
* @private
*/
onViewRemove: function(view) {
var me = this,
animation = view.getLayout().getAnimation(),
animations = [],
titleText, backButtonText;
me.endAnimation();
me.backButtonStack.pop();
titleText = me.getTitleText();
backButtonText = me.getBackButtonText();
me.refreshNavigationBarProxy();
if (animation && animation.isAnimation && view.isPainted()) {
animations = animations.concat(me.popBackButtonAnimated(backButtonText));
animations = animations.concat(me.popTitleAnimated(titleText));
me.activeAnimations = animations;
}
else {
me.popBackButton(backButtonText);
me.popTitle(titleText);
}
},
endAnimation: function() {
var activeAnimations = this.activeAnimations,
animation, i, ln;
if (activeAnimations) {
ln = activeAnimations.length;
for (i = 0; i < ln; i++) {
animation = activeAnimations[i];
if (animation.isAnimating) {
animation.stopAnimation();
}
else {
animation.destroy();
}
}
delete this.activeAnimations;
}
},
/**
* @private
*/
applyBackButton: function(config) {
return Ext.factory(config, Ext.Button, this.getBackButton());
},
/**
* @private
*/
updateBackButton: function(newBackButton, oldBackButton) {
if (oldBackButton) {
this.remove(oldBackButton);
}
if (newBackButton) {
this.add(newBackButton);
newBackButton.on({
scope: this,
tap: this.onBackButtonTap
});
}
},
onBackButtonTap: function() {
this.fireEvent('back', this);
},
updateUseTitleForBackButtonText: function() {
var backButton = this.getBackButton();
if (backButton) {
backButton.setText(this.getBackButtonText());
}
this.refreshProxy();
},
applyItems: function(items) {
var me = this;
if (!me.initialized) {
var defaults = me.getDefaults() || {};
me.leftBox = me.add({
xtype: 'container',
style: 'position: relative',
layout: {
type: 'hbox',
align: 'center'
}
});
me.spacer = me.add({
xtype: 'component',
style: 'position: relative',
flex: 1
});
me.rightBox = me.add({
xtype: 'container',
style: 'position: relative',
layout: {
type: 'hbox',
align: 'center'
}
});
me.titleComponent = me.add({
xtype: 'title',
hidden: defaults.hidden,
centered: true
});
me.doAdd = me.doBoxAdd;
me.doInsert = me.doBoxInsert;
}
me.callParent(arguments);
},
doBoxAdd: function(item) {
if (item.config.align == 'right') {
this.rightBox.add(item);
}
else {
this.leftBox.add(item);
}
},
doBoxInsert: function(index, item) {
if (item.config.align == 'right') {
this.rightBox.add(item);
}
else {
this.leftBox.add(item);
}
},
/**
* Called when any size of this component changes.
* It refreshes the navigation bar proxy so that the title and back button is in the correct location.
* @private
*/
refreshProxy: function() {
if (!this.rendered) {
return;
}
var backButton = this.getBackButton(),
titleComponent = this.titleComponent;
if (backButton && backButton.rendered) {
backButton.setWidth(null);
}
this.refreshNavigationBarProxy();
var properties = this.getNavigationBarProxyProperties();
if (backButton && backButton.rendered) {
backButton.setWidth(properties.backButton.width);
}
titleComponent.setStyle('-webkit-transform', null);
titleComponent.setWidth(properties.title.width);
titleComponent.element.setLeft(properties.title.left);
},
/**
* Calculates and returns the position values needed for the back button when you are pushing a title.
* @private
*/
getBackButtonAnimationProperties: function(reverse) {
var me = this,
element = me.element,
backButtonElement = me.getBackButton().element,
titleElement = me.titleComponent.element,
minButtonOffset = Math.min(element.getWidth() / 3, 200),
proxyProperties = this.getNavigationBarProxyProperties(),
proxyBackButtonWidth = proxyProperties.backButton.width,
elementX = element.getX(),
titleX = titleElement.getX(),
backButtonX = backButtonElement.getX(),
backButtonWidth = backButtonElement.getWidth(),
buttonOffset, buttonGhostOffset;
if (reverse) {
buttonOffset = elementX - backButtonX - backButtonWidth;
buttonGhostOffset = Math.min(titleX - backButtonWidth, minButtonOffset);
}
else {
buttonGhostOffset = elementX - backButtonX - backButtonWidth;
buttonOffset = Math.min(titleX - elementX, minButtonOffset);
}
return {
element: {
from: {
left: buttonOffset,
width: proxyBackButtonWidth,
opacity: 0
},
to: {
left: 0,
width: proxyBackButtonWidth,
opacity: 1
}
},
ghost: {
from: null,
to: {
left: buttonGhostOffset,
opacity: 0
}
}
};
},
/**
* Calculates and returns the position values needed for the title when you are pushing a title.
* @private
*/
getTitleAnimationProperties: function(reverse) {
var me = this,
element = me.element,
titleElement = me.titleComponent.element,
proxyProperties = this.getNavigationBarProxyProperties(),
ghostLeft = titleElement.getLeft(),
elementX = element.getX(),
elementWidth = element.getWidth(),
titleX = titleElement.getX(),
titleWidth = titleElement.getWidth(),
backLeft = proxyProperties.backButton.left,
backWidth = proxyProperties.backButton.width,
proxyTitleWidth = proxyProperties.title.width,
titleOffset, titleGhostOffset;
if (reverse) {
titleElement.setLeft(0);
titleOffset = elementX - titleX + backWidth;
titleGhostOffset = elementX + elementWidth;
if ((backLeft + titleWidth) > titleX) {
titleOffset = elementX - titleX - titleWidth;
}
}
else {
titleOffset = elementWidth - titleX;
titleGhostOffset = elementX - titleX + backWidth;
if ((backLeft + titleWidth) > titleX) {
titleGhostOffset = elementX - titleX - titleWidth;
}
}
return {
element: {
from: {
left: titleOffset,
width: proxyTitleWidth,
opacity: 0
},
to: {
left: proxyProperties.title.left,
width: proxyTitleWidth,
opacity: 1
}
},
ghost: {
from: ghostLeft,
to: {
left: titleGhostOffset,
opacity: 0
}
}
};
},
/**
* Helper method used to animate elements.
* You pass it an element, objects for the from and to positions an option onEnd callback called when the animation is over.
* Normally this method is passed configurations returned from the methods such as {@link #getTitleAnimationProperties}(true) etc.
* It is called from the {@link #pushBackButtonAnimated}, {@link #pushTitleAnimated}, {@link #popBackButtonAnimated} and {@link #popTitleAnimated}
* methods.
*
* If the current device is Android, it will use top/left to animate.
* If it is anything else, it will use transform.
* @private
*/
animate: function(component, element, from, to, callback) {
var me = this,
config = {
element: element,
easing: 'ease-in-out',
duration: this.getAnimation().duration,
replacePrevious: true,
preserveEndState: true
},
animation, fn;
//reset the left of the element
element.setLeft(0);
if (Ext.os.is.Android) {
if (from) {
config.from = {
left: from.left,
opacity: from.opacity
};
if (from.width) {
config.from.width = from.width;
}
}
if (to) {
config.to = {
left: to.left,
opacity: to.opacity
};
if (to.width) {
config.to.width = to.width;
}
}
}
else {
if (from) {
config.from = {
transform: {
translateX: from.left
},
opacity: from.opacity
};
if (from.width) {
config.from.width = from.width;
}
}
if (to) {
config.to = {
transform: {
translateX: to.left
},
opacity: to.opacity
};
if (to.width) {
config.to.width = to.width;
}
}
}
fn = function() {
if (callback) {
callback.call(me);
}
if (component && Ext.isNumber(to.width)) {
component.setWidth(to.width);
}
};
animation = new Ext.fx.Animation(config);
animation.on('animationend', fn, this);
Ext.Animator.run(animation);
return animation;
},
/**
* Returns the text needed for the current back button at anytime.
* @private
*/
getBackButtonText: function() {
var text = this.backButtonStack[this.backButtonStack.length - 2],
useTitleForBackButtonText = this.getUseTitleForBackButtonText();
if (!useTitleForBackButtonText) {
if (text) {
text = this.getDefaultBackButtonText();
}
}
return text;
},
/**
* Returns the text needed for the current title at anytime.
* @private
*/
getTitleText: function() {
return this.backButtonStack[this.backButtonStack.length - 1];
},
/**
* Pushes a back button into the bar with no animations
* @private
*/
pushBackButton: function(title) {
var backButton = this.getBackButton(),
to;
backButton.setText(title);
backButton.show();
to = this.getBackButtonAnimationProperties().element.to;
if (to.left) {
backButton.setLeft(to.left);
}
if (to.width) {
backButton.setWidth(to.width);
}
},
/**
* Pushes a new back button into the bar with animations
* @private
*/
pushBackButtonAnimated: function() {
var me = this,
backButton = me.getBackButton(),
previousTitle = backButton.getText(),
backButtonElement = backButton.element,
properties = me.getBackButtonAnimationProperties(),
animations = [],
buttonGhost;
//if there is a previoustitle, there should be a buttonGhost. so create it.
if (previousTitle) {
buttonGhost = me.createProxy(backButton);
}
//update the back button, and make sure it is visible
backButton.setText(this.getBackButtonText());
backButton.show();
//animate the backButton, which always has the new title
animations.push(me.animate(backButton, backButtonElement, properties.element.from, properties.element.to));
//if there is a buttonGhost, we must animate it too.
if (buttonGhost) {
animations.push(me.animate(null, buttonGhost, properties.ghost.from, properties.ghost.to, function() {
buttonGhost.destroy();
}));
}
return animations;
},
/**
* Pops the back button with no animations
* @private
*/
popBackButton: function(title) {
var backButton = this.getBackButton();
backButton.setText(null);
if (title) {
backButton.setText(this.getBackButtonText());
} else {
backButton.hide();
}
var properties = this.getBackButtonAnimationProperties(true),
to = properties.element.to;
if (to.left) {
backButton.setLeft(to.left);
}
if (to.width) {
backButton.setWidth(to.width);
}
},
/**
* Pops the current back button with animations.
* It will automatically know whether or not it should show the previous backButton or not. And proceed accordingly
* @private
*/
popBackButtonAnimated: function(title) {
var me = this,
backButton = me.getBackButton(),
previousTitle = backButton.getText(),
backButtonElement = backButton.element,
properties = me.getBackButtonAnimationProperties(true),
animations = [],
buttonGhost;
//if there is a previoustitle, there should be a buttonGhost. so create it.
if (previousTitle) {
buttonGhost = me.createProxy(backButton);
}
//update the back button, and make sure it is visible
if (title && me.backButtonStack.length) {
backButton.setText(me.getBackButtonText());
backButton.show();
animations.push(me.animate(backButton, backButtonElement, properties.element.from, properties.element.to));
} else {
backButton.hide();
}
//if there is a buttonGhost, we must animate it too.
if (buttonGhost) {
animations.push(me.animate(null, buttonGhost, properties.ghost.from, properties.ghost.to, function() {
buttonGhost.destroy();
if (!title) {
backButton.setText(null);
}
}));
}
return animations;
},
/**
* Pushes a new title into the bar without any animations
* @private
*/
pushTitle: function(newTitle) {
var title = this.titleComponent,
titleElement = title.element,
properties = this.getTitleAnimationProperties(),
to = properties.element.to;
title.setTitle(newTitle);
if (to.left) {
titleElement.setLeft(to.left);
}
if (to.width) {
title.setWidth(to.width);
}
},
/**
* Pushs a new title into the navigation bar, animating as it goes.
* @private
*/
pushTitleAnimated: function(newTitle) {
var me = this,
backButton = me.getBackButton(),
previousTitle = (backButton) ? backButton.getText() : null,
title = me.titleComponent,
titleElement = title.element,
animations = [],
properties, titleGhost;
//if there is a previoustitle, there should be a buttonGhost. so create it.
if (previousTitle) {
titleGhost = me.createProxy(title, true);
}
title.setTitle(newTitle);
properties = me.getTitleAnimationProperties();
//animate the new title
animations.push(me.animate(title, titleElement, properties.element.from, properties.element.to));
//if there is a titleGhost, we must animate it too.
if (titleGhost) {
animations.push(me.animate(null, titleGhost, properties.ghost.from, properties.ghost.to, function() {
titleGhost.destroy();
}));
}
return animations;
},
/**
* Pops the title without any animation.
* Simply gets the correct positions for the title and sets it on the dom.
* @private
*/
popTitle: function(newTitle) {
var title = this.titleComponent,
titleElement = title.element,
properties = this.getTitleAnimationProperties(true),
to = properties.element.to;
title.setTitle(newTitle);
if (to.left) {
titleElement.setLeft(to.left);
}
if (to.width) {
title.setWidth(to.width);
}
},
/**
* Method which pops the current title and animates it. It will automatically know whether or not to use a titleGhost
* element, and how to animate it.
* @private
*/
popTitleAnimated: function(newTitle) {
var me = this,
previousTitle = me.titleComponent.getTitle(),
title = me.titleComponent,
titleElement = title.element,
properties = me.getTitleAnimationProperties(true),
animations = [],
titleGhost;
//if there is a previoustitle, there should be a buttonGhost. so create it.
if (previousTitle) {
titleGhost = me.createProxy(title, true);
}
title.setTitle(newTitle || '');
//animate the new title
animations.push(me.animate(title, titleElement, properties.element.from, properties.element.to));
//if there is a titleGhost, we must animate it too.
if (titleGhost) {
animations.push(me.animate(null, titleGhost, properties.ghost.from, properties.ghost.to, function() {
titleGhost.destroy();
}));
}
return animations;
},
/**
* This creates a proxy of the whole navigation bar and positions it out of the view.
* This is used so we know where the back button and title needs to be at any time, either if we are
* animating, not animating, or resizing.
* @private
*/
createNavigationBarProxy: function() {
var proxy = this.proxy;
if (proxy) {
return;
}
//create a titlebar for the proxy
this.proxy = proxy = Ext.create('Ext.TitleBar', {
items: [{
xtype: 'button',
ui: 'back',
text: ''
}],
style: {
position: 'absolute',
visibility: 'hidden',
left: 0,
top: '-1000px'
},
title: this.backButtonStack[0]
});
proxy.backButton = proxy.down('button[ui=back]');
//add the proxy to the body
this.element.appendChild(proxy.element);
},
/**
* A Simple helper method which returns the current positions and sizes of the title and back button
* in the navigation bar proxy.
* @private
*/
getNavigationBarProxyProperties: function() {
var proxy = this.proxy,
titleElement = proxy.titleComponent.element,
buttonElement = proxy.backButton.element;
return {
title: {
left: titleElement.getLeft(),
width: titleElement.getWidth()
},
backButton: {
left: buttonElement.getLeft(),
width: buttonElement.getWidth()
}
};
},
/**
* Refreshes the navigation bar proxy with the latest data available in the backButtonStack.
* @private
*/
refreshNavigationBarProxy: function() {
var me = this,
proxy = me.proxy,
element = me.element,
backButtonStack = me.backButtonStack,
title = backButtonStack[backButtonStack.length - 1],
oldTitle = me.getBackButtonText(),
proxyBackButton;
if (!proxy) {
me.createNavigationBarProxy();
proxy = me.proxy;
}
proxyBackButton = proxy.backButton;
proxy.setWidth(element.getWidth());
proxy.setHeight(element.getHeight());
proxy.setTitle(title);
if (oldTitle) {
proxyBackButton.setText(oldTitle);
proxyBackButton.show();
}
else {
proxyBackButton.hide();
}
proxy.refreshTitlePosition();
},
/**
* Handles removing back button stacks from this bar
* @private
*/
beforePop: function(count) {
count--;
for (var i = 0; i < count; i++) {
this.backButtonStack.pop();
}
},
/**
* We override the hidden method because we don't want to remove it from the view using display:none. Instead we just position it off
* the screen, much like the navigation bar proxy. This means that all animations, pushing, popping etc. all still work when if you hide/show
* this bar at any time.
* @private
*/
doSetHidden: function(hidden) {
if (!hidden) {
this.element.setStyle({
position: 'relative',
top: 'auto',
left: 'auto',
width: 'auto'
});
} else {
this.element.setStyle({
position: 'absolute',
top: '-1000px',
left: '-1000px',
width: this.element.getWidth() + 'px'
});
}
},
/**
* Creates a proxy element of the passed element, and positions it in the same position, using absolute positioning.
* The createNavigationBarProxy method uses this to create proxies of the backButton and the title elements.
* @private
*/
createProxy: function(component, useParent) {
var element = (useParent) ? component.element.getParent() : component.element,
ghost = Ext.get(element.id + '-proxy');
if (!ghost) {
ghost = element.dom.cloneNode(true);
ghost.id = element.id + '-proxy';
//insert it into the toolbar
element.getParent().dom.appendChild(ghost);
//set the x/y
ghost = Ext.get(ghost);
ghost.setStyle('position', 'absolute');
ghost.setY(element.getY());
ghost.setX(element.getX());
}
return ghost;
},
destroy: function() {
Ext.destroy(this.proxy);
delete this.proxy;
}
});