/** * NestedList provides a miller column interface to navigate between nested sets * and provide a clean interface with limited screen real-estate. * * @example miniphone preview * var data = { * text: 'Groceries', * items: [{ * text: 'Drinks', * items: [{ * text: 'Water', * items: [{ * text: 'Sparkling', * leaf: true * }, { * text: 'Still', * leaf: true * }] * }, { * text: 'Coffee', * leaf: true * }, { * text: 'Espresso', * leaf: true * }, { * text: 'Redbull', * leaf: true * }, { * text: 'Coke', * leaf: true * }, { * text: 'Diet Coke', * leaf: true * }] * }, { * text: 'Fruit', * items: [{ * text: 'Bananas', * leaf: true * }, { * text: 'Lemon', * leaf: true * }] * }, { * text: 'Snacks', * items: [{ * text: 'Nuts', * leaf: true * }, { * text: 'Pretzels', * leaf: true * }, { * text: 'Wasabi Peas', * leaf: true * }] * }] * }; * * Ext.regModel('ListItem', { * fields: [{ * name: 'text', * type: 'string' * }] * }); * * var store = new Ext.data.TreeStore({ * model: 'ListItem', * defaultRootProperty: 'items', * root: data * }); * * var nestedList = new Ext.NestedList({ * fullscreen: true, * title: 'Groceries', * displayField: 'text', * store: store * }); * */ Ext.define('Ext.dataview.NestedList', { alternateClassName: 'Ext.NestedList', extend: 'Ext.Container', xtype : 'nestedlist', requires: [ 'Ext.List', 'Ext.Toolbar', 'Ext.Button', 'Ext.XTemplate', 'Ext.data.StoreManager', 'Ext.data.NodeStore', 'Ext.data.TreeStore' ], config: { // @inherit cls: Ext.baseCSSPrefix + 'nested-list', /** * @cfg {String/Object/Boolean} cardSwitchAnimation * Animation to be used during transitions of cards. * Any valid value from Ext.anims can be used ('fade', 'slide', 'flip', 'cube', 'pop', 'wipe'). * This animation will be automatically reversed when navigating to a previous card in the * nested list. * Defaults to 'slide'. * @accessor */ cardSwitchAnimation: 'slide', /** * @cfg {String} backText * The label to display for the back button. Defaults to "Back". * @accessor */ backText: 'Back', /** * @cfg {Boolean} useTitleAsBackText * @accessor */ useTitleAsBackText: true, /** * @cfg {Boolean} updateTitleText * Update the title with the currently selected category. Defaults to true. * @accessor */ updateTitleText: true, /** * @cfg {String} displayField * Display field to use when setting item text and title. * This configuration is ignored when overriding getItemTextTpl or * getTitleTextTpl for the item text or title. (Defaults to 'text') * @accessor */ displayField: 'text', /** * @cfg {String} loadingText * Loading text to display when a subtree is loading. * @accessor */ loadingText: 'Loading...', /** * @cfg {String} emptyText * Empty text to display when a subtree is empty. * @accessor */ emptyText: 'No items available.', /** * @cfg {Boolean/Function} onItemDisclosure * Maps to the Ext.List onItemDisclosure configuration for individual lists. (Defaults to false) * @accessor */ onItemDisclosure: false, /** * @cfg {Boolean} allowDeselect * Set to true to alow the user to deselect leaf items via interaction. * Defaults to false. * @accessor */ allowDeselect: false, /** * @deprecated * @cfg {Boolean} useToolbar True to show the header toolbar. * @accessor */ useToolbar: null, /** * @cfg {Object} * @accessor */ toolbar: { docked: 'top', xtype: 'titlebar', ui: 'light', inline: true }, /** * @cfg {String} title The title of the toolbar * @accessor */ title: '', /** * @cfg {String} layout * @hide * @accessor */ layout: { type: 'card', animation: { type: 'slide', duration: 250, direction: 'left' } }, /** * @cfg {Object} data The initial set of data to be used to display content in this nested list. */ data: null, /** * @cfg {Ext.data.TreeStore} store The tree store to be used for this nested list. */ store: null, /** * @cfg {Ext.Container} detailContainer The container of the detailCard. * @accessor */ detailContainer: undefined, /** * @cfg {Ext.Component} detailCard to provide a final card for leaf nodes. * @accessor */ detailCard: null, /** * @cfg {Object} backButton The configuration for the back button used in the nested list * @private */ backButton: { ui: 'back', hidden: true }, lastNode: null, lastActiveList: null, pressedDelay: 0 }, /** * @event itemtap * Fires when a node is tapped on * @param {Ext.dataview.NestedList} this * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active * @param {Number} index The index of the item that was tapped * @param {Object} item The item tapped * @param {Ext.event.Event} e The event object */ /** * @event itemdoubletap * Fires when a node is double tapped on * @param {Ext.dataview.NestedList} this * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active * @param {Number} index The index of the item that was tapped * @param {Object} item The item tapped * @param {Ext.event.Event} e The event object */ /** * @event containertap * Fires when a tap occurs and it is not on a template node. * @param {Ext.dataview.NestedList} this * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active * @param {Ext.event.Event} e The raw event object */ /** * @event selectionchange * Fires when the selected nodes change. * @param {Ext.dataview.NestedList} this * @param {Ext.dataview.List} list The Ext.datavaie.List that is currently active * @param {Array} selections Array of the selected nodes */ /** * @event beforeselect * Fires before a selection is made. * @param {Ext.dataview.NestedList} this * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active * @param {HTMLElement} node The node to be selected * @param {Array} selections Array of currently selected nodes */ /** * @event listchange * Fires when the user taps a list item * @param {Ext.dataview.NestedList} this * @param {Object} listitem The new active list */ /** * @event leafitemtap * Fires when the user taps a leaf list item * @param {Ext.dataview.NestedList} this * @param {Ext.List} list The subList the item is on * @param {Number} index The index of the item tapped * @param {Object} item The item tapped * @param {Ext.event.Event} e The event */ /** * @event back * @preventable doBack * Fires when the user taps Back * @param {Ext.dataview.NestedList} this * @param {HTMLElement} node The node to be selected * @param {Ext.dataview.List} lastActiveList The Ext.dataview.List that was last active * @param {Boolean} detailCardActive Flag set if the detail card is currently active */ /** * @event beforeload * Fires before a request is made for a new data object. * @param {Ext.dataview.NestedList} this * @param {Ext.data.Store} store The store instance * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to * load the Store */ /** * @event load * Fires whenever records have been loaded into the store. * @param {Ext.dataview.NestedList} this * @param {Ext.data.Store} store The store instance * @param {Ext.util.Grouper[]} records An array of records * @param {Boolean} successful True if the operation was successful. * @param {Ext.data.Operation} operation The associated operation */ //@private initialize: function() { var me = this; me.callParent(); me.on({ delegate: '> list', itemdoubletap: 'onItemDoubleTap', itemtap: 'onItemTap', beforeselect: 'onBeforeSelect', containertap: 'onContainerTap', selectionchange: 'onSelectionChange', scope: me }); }, applyDetailContainer: function(config) { if (!config) { config = this; } return config; }, /** * Called when an list item has been tapped * @param {Ext.List} list The subList the item is on * @param {Number} index The id of the item tapped * @param {Ext.Element} item The list item tapped * @param {Ext.event.Event} e The event */ onItemTap: function(list, index, item, e) { var me = this, store = list.getStore(), node = store.getAt(index); me.fireEvent('itemtap', this, list, index, item, e); if (node.isLeaf()) { me.fireEvent('leafitemtap', this, list, index, item, e); me.goToLeaf(node); } else { this.goToNode(node); } }, onBeforeSelect: function() { this.fireEvent('beforeselect', [this, Array.prototype.slice.call(arguments)]); }, onContainerTap: function() { this.fireEvent('containertap', [this, Array.prototype.slice.call(arguments)]); }, onSelectionChange: function() { this.fireEvent('selectionchange', [this, Array.prototype.slice.call(arguments)]); }, onItemDoubleTap: function() { this.fireEvent('itemdoubletap', [this, Array.prototype.slice.call(arguments)]); }, onStoreBeforeLoad: function() { this.fireEvent('beforeload', [this, Array.prototype.slice.call(arguments)]); }, onStoreLoad: function() { this.fireEvent('load', [this, Array.prototype.slice.call(arguments)]); }, /** * Called when the backButton has been tapped */ onBackTap: function() { var me = this, node = me.getLastNode(), detailCard = me.getDetailCard(), detailCardActive = detailCard && me.getActiveItem() == detailCard, lastActiveList = me.getLastActiveList(); this.fireAction('back', [this, node, lastActiveList, detailCardActive], 'doBack'); }, doBack: function(me, node, lastActiveList, detailCardActive) { var layout = me.getLayout(), animation = (layout) ? layout.getAnimation() : null; if (detailCardActive && lastActiveList) { if (animation) { animation.setReverse(true); } me.setActiveItem(lastActiveList); me.setLastNode(node.parentNode); me.syncToolbar(); } else { this.goToNode(node.parentNode); } }, updateData: function(data) { if (!this.getStore()) { this.setStore(new Ext.data.TreeStore({ root: data })); } }, applyStore: function(store) { if (store) { store = Ext.data.StoreManager.lookup(store); } return store; }, updateStore: function(newStore, oldStore) { var me = this, rootNode; if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) { if (oldStore.autoDestroy) { oldStore.destroy(); } else { oldStore.un({ rootchange: 'goToNode', scope: me }); } } if (newStore) { rootNode = newStore.getRoot(); if (rootNode) { me.goToNode(rootNode); } else { newStore.on({ load: 'onLoad', single: true, scope: this }); newStore.load(); } newStore.on({ beforeload: 'onStoreBeforeLoad', load: 'onStoreLoad', rootchange: 'goToNode', scope: this }); } }, onLoad: function(store) { this.goToNode(store.getRootNode()); }, applyBackButton: function(config) { return Ext.factory(config, Ext.Button, this.getBackButton()); }, applyDetailCard: function(config) { return this.factoryItem(config); }, updateBackButton: function(newButton, oldButton) { if (newButton) { var me = this; newButton.on('tap', me.onBackTap, me); newButton.setText(me.getBackText()); me.getToolbar().insert(0, newButton); } else if (oldButton) { oldButton.destroy(); } }, applyToolbar: function(config) { return Ext.factory(config, Ext.TitleBar, this.getToolbar()); }, updateToolbar: function(newToolbar, oldToolbar) { var me = this; if (newToolbar) { newToolbar.setTitle(me.getTitle()); if (!newToolbar.getParent()) { me.add(newToolbar); } } else if (oldToolbar) { oldToolbar.destroy(); } }, setUseToolbar: function(config) { //<debug warn> Ext.Logger.deprecate("The 'useToolbar' config is deprecated, use the 'toolbar' config instead", this); //</debug> }, updateTitle: function(newTitle) { var me = this, toolbar = me.getToolbar(); if (toolbar) { if (me.getUpdateTitleText()) { toolbar.setTitle(newTitle); } } }, /** * Override this method to provide custom template rendering of individual * nodes. The template will receive all data within the Record and will also * receive whether or not it is a leaf node. * @param {Ext.data.Record} node */ getItemTextTpl: function(node) { return '{' + this.getDisplayField() + '}'; }, /** * Override this method to provide custom template rendering of titles/back * buttons when useTitleAsBackText is enabled. * @param {Ext.data.Record} node */ getTitleTextTpl: function(node) { return '{' + this.getDisplayField() + '}'; }, /** * @private */ renderTitleText: function(node, forBackButton) { if (!node.titleTpl) { node.titleTpl = Ext.create('Ext.XTemplate', this.getTitleTextTpl(node)); } if (node.isRoot()) { var initialTitle = this.getInitialConfig('title'); return (forBackButton && initialTitle === '') ? this.getInitialConfig('backText') : initialTitle; } return node.titleTpl.applyTemplate(node.data); }, /** * Method to handle going to a specific node within this nested list. Node must be part of the * internal {@link #store}. * @param {Ext.data.NodeInterface} node The specified node to navigate to */ goToNode: function(node) { if (!node) { return; } var me = this, activeItem = me.getActiveItem(), detailCard = me.getDetailCard(), detailCardActive = detailCard && me.getActiveItem() == detailCard, reverse = me.goToNodeReverseAnimation(node), firstList = me.firstList, secondList = me.secondList, layout = me.getLayout(), animation = (layout) ? layout.getAnimation() : null, list; //if the node is a leaf, throw an error if (node.isLeaf()) { throw new Error('goToNode: passed a node which is a leaf.'); } //if we are currently at the passed node, do nothing. if (node == me.getLastNode() && !detailCardActive) { return; } if (detailCardActive) { if (animation) { animation.setReverse(true); } me.setActiveItem(me.getLastActiveList()); } else { if (firstList && secondList) { //firstList and secondList have both been created activeItem = me.getActiveItem(); me.setLastActiveList(activeItem); list = (activeItem == firstList) ? secondList : firstList; list.getStore().setNode(node); node.expand(); if (animation) { animation.setReverse(reverse); } me.setActiveItem(list); list.deselectAll(); } else if (firstList) { //only firstList has been created me.setLastActiveList(me.getActiveItem()); me.setActiveItem(me.getListConfig(node)); me.secondList = me.getActiveItem(); } else { //no lists have been created me.setActiveItem(me.getListConfig(node)); me.firstList = me.getActiveItem(); } } me.fireEvent('listchange', this, me.getActiveItem()); me.setLastNode(node); me.syncToolbar(); }, /** * The leaf you want to navigate to. You should pass a node instance. * @param {Ext.data.NodeInterface} node The specified node to navigate to */ goToLeaf: function(node) { if (!node.isLeaf()) { throw new Error('goToLeaf: passed a node which is not a leaf.'); } var me = this, card = me.getDetailCard(node), container = me.getDetailContainer(), sharedContainer = container == this, layout = me.getLayout(), animation = (layout) ? layout.getAnimation() : false; if (card) { if (container.getItems().indexOf(card) === -1) { container.add(card); } if (sharedContainer) { if (me.getActiveItem() instanceof Ext.dataview.List) { me.setLastActiveList(me.getActiveItem()); } me.setLastNode(node); } if (animation) { animation.setReverse(false); } container.setActiveItem(card); me.syncToolbar(); } }, /** * @private * Method which updates the {@link #backButton} and {@link #toolbar} with the latest information from * the current node. */ syncToolbar: function(forceDetail) { var me = this, detailCard = me.getDetailCard(), node = me.getLastNode(), detailActive = forceDetail || (detailCard && (me.getActiveItem() == detailCard)), parentNode = (detailActive) ? node : node.parentNode, backButton = me.getBackButton(); //show/hide the backButton, and update the backButton text, if one exists if (backButton) { backButton[parentNode ? 'show' : 'hide'](); if (parentNode && me.getUseTitleAsBackText()) { backButton.setText(me.renderTitleText(node.parentNode, true)); } } if (node) { me.setTitle(me.renderTitleText(node)); } }, updateBackText: function(newText) { this.getBackButton().setText(newText); }, /** * @private * Returns true if the passed node should have a reverse animation from the previous current node * @param {Ext.data.NodeInterface} node */ goToNodeReverseAnimation: function(node) { var me = this, lastNode = me.getLastNode(); if (!lastNode) { return false; } return (!lastNode.contains(node) && lastNode.isAncestor(node)) ? true : false; }, /** * @private * Returns the list config for a specified node. * @param {HTMLElement} node The node for the list config */ getListConfig: function(node) { var me = this, nodeStore = Ext.create('Ext.data.NodeStore', { recursive: false, node: node, rootVisible: false, model: me.getStore().getModel() }); node.expand(); return { xtype: 'list', autoDestroy: true, clearSelectionOnDeactivate: false, disclosure: false, store: nodeStore, onItemDisclosure: me.getOnItemDisclosure(), allowDeselect : me.getAllowDeselect(), itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>' }; } });