/** * NavigationView is basically a {@link Ext.Container} with a {@link Ext.layout.Card card} layout, * so only one view can be visible at a time. However, NavigationView also adds extra functionality * on top of this to allow you to `push` and `pop` views at any time. When you do this, your * NavigationView will automatically animate between your current active view, and the new view you * want to `push`, or the previous view you want to `pop`. * * Using the NavigationView is very simple. Here is a basic example of it in action: * * ```javascript * @example({ framework: 'extjs' }) * var view = Ext.create('Ext.NavigationView', { * fullscreen: true, * * items: [{ * title: 'First', * items: [{ * xtype: 'button', * text: 'Push a new view!', * handler: function() { * // use the push() method to push another view. It works much like * // add() or setActiveItem(). it accepts a view instance, or you can give it * // a view config. * view.push({ * title: 'Second', * html: 'Second view!' * }); * } * }] * }] * }); * ``` * * Now, here comes the fun part: you can push any view/item into the NavigationView, at any time, * and it will automatically handle the animations between the two views, including adding a * back button (if necessary) and showing the new title. * * view.push({ * title: 'A new view', * html: 'Some new content' * }); * * As you can see, it is as simple as calling the {@link #method-push} method, with a new view * (instance or object). Done. * * You can also `pop` a view at any time. This will remove the top-most view from the * NavigationView, and animate back to the previous view. You can do this using the * {@link #method-pop} method (which requires no arguments). * * view.pop(); * * Applications that need compatibility with ##Older Android## devices will want to see * the {@link #layout} config for details on disabling navigation view animations as these devices * have poor animation support and performance. */Ext.define('Ext.navigation.View', { extend: 'Ext.Container', alternateClassName: 'Ext.NavigationView', xtype: 'navigationview', requires: ['Ext.navigation.Bar'], config: { /** * @cfg {Boolean/Object} navigationBar * The NavigationBar used in this navigation view. It defaults to be docked to the top. * * You can just pass in a normal object if you want to customize the NavigationBar. * For example: * * navigationBar: { * ui: 'dark', * docked: 'bottom' * } * * You **cannot** specify a *title* property in this configuration. The title of the * navigationBar is taken from the configuration of this views children: * * view.push({ * title: 'This views title which will be shown in the navigation bar', * html: 'Some HTML' * }); * * @accessor */ navigationBar: { docked: 'top' }, /** * @cfg {String} defaultBackButtonText * The text to be displayed on the back button if: * * - The previous view does not have a title. * - The {@link #useTitleForBackButtonText} configuration is `true`. * @accessor */ defaultBackButtonText: 'Back', /** * @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. * @accessor */ useTitleForBackButtonText: false, /** * @cfg {Array/Object} items The child items to add to this NavigationView. This is usually * an array of Component configurations or instances, for example: * * Ext.create('Ext.Container', { * items: [ * { * xtype: 'panel', * title: 'My title', * html: 'This is an item' * } * ] * }); * * If you want a title to be displayed in the {@link #navigationBar}, you must specify a * `title` configuration in your view, like above. * * __Note:__ Only one view will be visible at a time. If you want to change to another * view, use the {@link #method-push} or {@link #setActiveItem} methods. * @accessor */ /** * @cfg {Object} * Layout used in this navigation view, type must be set to 'card'. * **Android NOTE:** Older Android devices have poor animation performance. It is * recommended to set the animation to null, for example: * * layout: { * type: 'card', * animation: null * } * * @accessor */ layout: { type: 'card', animation: { duration: 300, easing: 'ease-out', type: 'slide', direction: 'left' } } }, baseCls: Ext.baseCSSPrefix + 'navigationview', /** * @event push * Fires when a view is pushed into this navigation view * @param {Ext.navigation.View} this The component instance * @param {Mixed} view The view that has been pushed */ /** * @event pop * Fires when a view is popped from this navigation view * @param {Ext.navigation.View} this The component instance * @param {Mixed} view The view that has been popped */ /** * @event back * Fires when the back button in the navigation view was tapped. * @param {Ext.navigation.View} this The component instance\ */ /** * @private */ initialize: function() { var me = this, navBar = me.getNavigationBar(), layout; me.callParent(); // add a listener onto the back button in the navigationbar if (navBar) { navBar.on({ back: me.onBackButtonTap, scope: me }); me.relayEvents(navBar, 'rightbuttontap'); me.relayEvents(me, { add: 'push', remove: 'pop' }); } //<debug> layout = me.getLayout(); if (layout && !layout.isCard) { Ext.Logger.error('The base layout for a NavigationView must always be a Card Layout'); } //</debug> }, /** * @private * Called when the user taps on the back button */ onBackButtonTap: function() { this.pop(); this.fireEvent('back', this); }, /** * Pushes a new view into this navigation view using the default animation that this view has. * @param {Object} view The view to push. * @return {Ext.Component} The new item you just pushed. */ push: function(view) { return this.add(view); }, /** * Removes the current active view from the stack and sets the previous view using the default * animation of this view. You can also pass a {@link Ext.ComponentQuery} selector to target * what inner item to pop to. @param {Number/String/Object} count If a Number, the number of * views you want to pop. If a String, the pops to a matching component query. If an Object, * the pops to a matching view instance. * @return {Ext.Component} The new active item */ pop: function(count) { if (this.beforePop(count)) { return this.doPop(); } }, /** * @private * Calculates whether it needs to remove any items from the stack when you are popping more * than 1 item. If it does, it removes those views from the stack and returns `true`. * @return {Boolean} `true` if it has removed views. */ beforePop: function(count) { var me = this, innerItems = me.getInnerItems(), last, i, ln, toRemove; if (Ext.isString(count) || Ext.isObject(count)) { last = innerItems.length - 1; for (i = last; i >= 0; i--) { if ( (Ext.isString(count) && Ext.ComponentQuery.is(innerItems[i], count)) || (Ext.isObject(count) && count === innerItems[i]) ) { count = last - i; break; } } if (!Ext.isNumber(count)) { return false; } } ln = innerItems.length; // default to 1 pop if (!Ext.isNumber(count) || count < 1) { count = 1; } // check if we are trying to remove more items than we have count = Math.min(count, ln - 1); if (count) { // we need to reset the backButtonStack in the navigation bar me.getNavigationBar().beforePop(count); // get the items we need to remove from the view and remove theme toRemove = innerItems.splice(-count, count - 1); for (i = 0; i < toRemove.length; i++) { this.remove(toRemove[i]); } return true; } return false; }, /** * @private */ doPop: function() { var me = this, innerItems = this.getInnerItems(), item; // set the new active item to be the new last item of the stack me.remove(innerItems[innerItems.length - 1]); // Hide the backButton if (innerItems.length < 3 && this.$backButton) { this.$backButton.hide(); } // Update the title container if (this.$titleContainer) { //<debug> if (!this.$titleContainer.setTitle) { Ext.Logger.error([ 'You have selected to display a title in a component that does not support', 'titles in NavigationView. Please remove the `title` configuration from your ', 'NavigationView item, or change it to a component that has a `setTitle` method.' ].join('')); } //</debug> item = innerItems[innerItems.length - 2]; this.$titleContainer.setTitle((item.getTitle) ? item.getTitle() : item.config.title); } return this.getActiveItem(); }, /** * Returns the previous item, if one exists. * @return {Mixed} The previous view */ getPreviousItem: function() { var innerItems = this.getInnerItems(); return innerItems[innerItems.length - 2]; }, /** * Updates the backbutton text accordingly in the {@link #navigationBar} * @private */ updateUseTitleForBackButtonText: function(useTitleForBackButtonText) { var navigationBar = this.getNavigationBar(); if (navigationBar) { navigationBar.setUseTitleForBackButtonText(useTitleForBackButtonText); } }, /** * Updates the backbutton text accordingly in the {@link #navigationBar} * @private */ updateDefaultBackButtonText: function(defaultBackButtonText) { var navigationBar = this.getNavigationBar(); if (navigationBar) { navigationBar.setDefaultBackButtonText(defaultBackButtonText); } }, /** * This is called when an Item is added to the BackButtonContainer of a SplitNavigation View * @private * * @param toolbar * @param item */ onBackButtonContainerAdd: function(toolbar, item) { item.on({ scope: this, show: this.refreshBackButtonContainer, hide: this.refreshBackButtonContainer }); this.refreshBackButtonContainer(); }, /** * This is called when an Item is removed from the BackButtonContainer of a SplitNavigation View * @private * * @param toolbar * @param item */ onBackButtonContainerRemove: function(toolbar, item) { item.un({ scope: this, show: this.refreshBackButtonContainer, hide: this.refreshBackButtonContainer }); this.refreshBackButtonContainer(); }, /** * This is used for Blackberry SplitNavigation to monitor the state of child items in * the bottom toolbar. If no visible children exist the toolbar will be hidden. * @private */ refreshBackButtonContainer: function() { var backButtonContainer, items, item, i; if (!this.$backButtonContainer) { return; } backButtonContainer = this.$backButtonContainer; items = backButtonContainer.items; for (i = 0; i < items.length; i++) { item = items.get(i); if (!item.isHidden()) { this.$backButtonContainer.show(); return; } } this.$backButtonContainer.hide(); }, /** * @private */ applyNavigationBar: function(config) { var me = this, containerConfig; if (!config) { config = { hidden: true, docked: 'top' }; } // Call the getter for items on this view to insure that they will be // available (via innerItems) for the navigationBar created below. me.getItems(); if (config.title) { delete config.title; //<debug> Ext.Logger.warn("Ext.navigation.View: The 'navigationBar' configuration does not " + "accept a 'title' property. You set the title of the navigationBar by giving " + "this navigation view's children a 'title' property."); //</debug> } config.view = me; config.useTitleForBackButtonText = me.getUseTitleForBackButtonText(); // Blackberry specific nav setup where title is on the top title bar and the bottom toolbar // is used for buttons and BACK if (config.splitNavigation) { me.$titleContainer = me.add({ docked: 'top', xtype: 'titlebar', ui: 'light', title: me.$currentTitle || '' }); containerConfig = (config.splitNavigation === true) ? {} : config.splitNavigation; me.$backButtonContainer = me.add({ xtype: 'toolbar', docked: 'bottom', hidden: true }); // Any item that is added to the BackButtonContainer should be monitored for visibility // this will allow the toolbar to be hidden when no items exist in it. me.$backButtonContainer.on({ scope: me, add: me.onBackButtonContainerAdd, remove: me.onBackButtonContainerRemove }); me.$backButton = me.$backButtonContainer.add({ xtype: 'button', text: 'Back', hidden: true, ui: 'back' }); // Default config items go into the bottom bar if (config.items) { me.$backButtonContainer.add(config.items); } // If the user provided items and splitNav items, default items go into the bottom bar, // split nav items go into the top if (containerConfig.items) { me.$titleContainer.add(containerConfig.items); } me.$backButton.on({ scope: me, tap: me.onBackButtonTap }); config = { hidden: true, docked: 'top' }; } return Ext.factory(config, Ext.navigation.Bar, this.getNavigationBar()); }, /** * @private */ updateNavigationBar: function(newNavigationBar, oldNavigationBar) { if (oldNavigationBar) { this.remove(oldNavigationBar, true); } if (newNavigationBar) { this.add(newNavigationBar); } }, /** * @private */ applyActiveItem: function(activeItem, currentActiveItem) { var me = this, innerItems = me.getInnerItems(); // Make sure the items are already initialized me.getItems(); // If we are not initialzed yet, we should set the active item to the last item in the stack if (!me.initialized) { activeItem = innerItems.length - 1; } return this.callParent([activeItem, currentActiveItem]); }, doResetActiveItem: function(innerIndex) { var me = this, innerItems = me.getInnerItems(), animation = me.getLayout().getAnimation(); if (innerIndex > 0) { if (animation && animation.isAnimation) { animation.setReverse(true); } me.setActiveItem(innerIndex - 1); me.getNavigationBar().onViewRemove(me, innerItems[innerIndex], innerIndex); } }, /** * @private */ doRemove: function() { var animation = this.getLayout().getAnimation(); if (animation && animation.isAnimation) { animation.setReverse(false); } this.callParent(arguments); }, /** * @private */ onItemAdd: function(item, index) { var me = this, initialized = me.initialized, navigationBar; // Check for title configuration if (item && item.getDocked() && item.config.title === true) { me.$titleContainer = item; } me.doItemLayoutAdd(item, index); if (initialized && item.isInnerItem()) { me.setActiveItem(item); navigationBar = this.getNavigationBar(); if (navigationBar) { this.getNavigationBar().onViewAdd(me, item, index); } // Update the custom backButton if (me.$backButtonContainer) { me.$backButton.show(); } } if (item && item.isInnerItem()) { // Update the title container title me.updateTitleContainerTitle((item.getTitle) ? item.getTitle() : item.config.title); } if (initialized) { me.fireEvent('add', me, item, index); } }, /** * @private * Updates the title of the titleContainer, if it exists */ updateTitleContainerTitle: function(title) { if (this.$titleContainer) { //<debug> if (!this.$titleContainer.setTitle) { Ext.Logger.error([ 'You have selected to display a title in a component that does not support ', 'titles in NavigationView. Please remove the `title` configuration from your ', 'NavigationView item, or change it to a component that has a `setTitle` method.' ].join('')); } //</debug> this.$titleContainer.setTitle(title); } else { this.$currentTitle = title; } }, /** * Resets the view by removing all items between the first and last item. * @return {Ext.Component} The view that is now active */ reset: function() { return this.pop(this.getInnerItems().length); }});