/** * 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.define('ListItem', { * extend: 'Ext.data.Model', * config: { * fields: [{ * name: 'text', * type: 'string' * }] * } * }); * * var store = Ext.create('Ext.data.TreeStore', { * model: 'ListItem', * defaultRootProperty: 'items', * root: data * }); * * var nestedList = Ext.create('Ext.NestedList', { * fullscreen: true, * title: 'Groceries', * displayField: 'text', * store: store * }); * * @aside guide nested_list */ Ext.define('Ext.dataview.NestedList', { alternateClassName: 'Ext.NestedList', extend: 'Ext.Container', xtype : 'nestedlist', requires: [ 'Ext.List', 'Ext.TitleBar', 'Ext.Button', 'Ext.XTemplate', 'Ext.data.StoreManager', 'Ext.data.NodeStore', 'Ext.data.TreeStore' ], config: { /** * @cfg * @inheritdoc */ cls: Ext.baseCSSPrefix + 'nested-list', /** * @cfg {String/Object/Boolean} cardSwitchAnimation * Animation to be used during transitions of cards. * @removed 2.0.0 please use {@link Ext.layout.Card#animation} */ /** * @cfg {String} backText * The label to display for the back button. * @accessor */ backText: 'Back', /** * @cfg {Boolean} useTitleAsBackText * True to use title as a label for back button. * @accessor */ useTitleAsBackText: true, /** * @cfg {Boolean} updateTitleText * Update the title with the currently selected category. * @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. * @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 2.0.0 Please set the {@link #toolbar} configuration to `false` instead * @cfg {Boolean} useToolbar True to show the header toolbar. * @accessor */ useToolbar: null, /** * @cfg {Ext.Toolbar/Object/Boolean} toolbar * The configuration to be used for the toolbar displayed in this nested list. * @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 {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 */ backButton: { ui: 'back', hidden: true }, /** * @cfg {Object} listConfig An optional config object which is merged with the default * configuration used to create each nested list */ listConfig: null, // @private lastNode: null, // @private lastActiveList: null }, /** * @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 tapped * @param {Ext.dom.Element} target The element tapped * @param {Ext.data.Record} record The record 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 {Ext.dom.Element} target The element tapped * @param {Ext.data.Record} record The record 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.dataview.List that is currently active * @param {Array} selections Array of the selected nodes */ /** * @event beforeselectionchange * 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 * @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead. */ /** * @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 {Ext.dom.Element} target The element tapped * @param {Ext.data.Record} record The record 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 */ constructor: function(config) { if (Ext.isObject(config)) { if (config.getTitleTextTpl) { this.getTitleTextTpl = config.getTitleTextTpl; } if (config.getItemTextTpl) { this.getItemTextTpl = config.getItemTextTpl; } } this.callParent(arguments); }, onItemInteraction: function() { if (this.isGoingTo) { return false; } }, applyDetailContainer: function(config) { if (!config) { config = this; } return config; }, updateDetailContainer: function(newContainer, oldContainer) { newContainer.onBefore('activeitemchange', 'onBeforeDetailContainerChange', this); newContainer.onAfter('activeitemchange', 'onDetailContainerChange', this); }, onBeforeDetailContainerChange: function() { this.isGoingTo = true; }, onDetailContainerChange: function() { this.isGoingTo = false; }, /** * 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} target The list item tapped * @param {Ext.data.Record} record The record whichw as tapped * @param {Ext.event.Event} e The event */ onItemTap: function(list, index, target, record, e) { var me = this, store = list.getStore(), node = store.getAt(index); me.fireEvent('itemtap', this, list, index, target, record, e); if (node.isLeaf()) { me.fireEvent('leafitemtap', this, list, index, target, record, e); me.goToLeaf(node); } else { this.goToNode(node); } }, onBeforeSelect: function() { this.fireEvent.apply(this, [].concat('beforeselect', this, Array.prototype.slice.call(arguments))); }, onContainerTap: function() { this.fireEvent.apply(this, [].concat('containertap', this, Array.prototype.slice.call(arguments))); }, onSelectionChange: function() { this.fireEvent.apply(this, [].concat('selectionchange', this, Array.prototype.slice.call(arguments))); }, onItemDoubleTap: function() { this.fireEvent.apply(this, [].concat('itemdoubletap', this, Array.prototype.slice.call(arguments))); }, onStoreBeforeLoad: function() { var loadingText = this.getLoadingText(), scrollable = this.getScrollable(); if (loadingText) { this.setMasked({ xtype: 'loadmask', message: loadingText }); //disable scorlling while it is masked if (scrollable) { scrollable.getScroller().setDisabled(true); } } this.fireEvent.apply(this, [].concat('beforeload', this, Array.prototype.slice.call(arguments))); }, onStoreLoad: function(store, records, successful, operation) { this.setMasked(false); this.fireEvent.apply(this, [].concat('load', this, Array.prototype.slice.call(arguments))); if (store.indexOf(this.getLastNode()) === -1) { this.goToNode(store.getRoot()); } }, /** * 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); // <debug> if (!store) { Ext.Logger.warn("The specified Store cannot be found", this); } //</debug> } return store; }, storeListeners: { rootchange: 'onStoreRootChange', load: 'onStoreLoad', beforeload: 'onStoreBeforeLoad' }, updateStore: function(newStore, oldStore) { var me = this, listeners = this.storeListeners; listeners.scope = me; if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) { if (oldStore.autoDestroy) { oldStore.destroy(); } oldStore.un(listeners); } if (newStore) { me.goToNode(newStore.getRoot()); newStore.on(listeners); } }, onStoreRootChange: function(store, node) { this.goToNode(node); }, 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(); } }, updateUseToolbar: function(newUseToolbar, oldUseToolbar) { if (!newUseToolbar) { this.setToolbar(false); } }, 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.getList(node)); me.secondList = me.getActiveItem(); } else { //no lists have been created me.setActiveItem(me.getList(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 */ getList: function(node) { var me = this, nodeStore = Ext.create('Ext.data.NodeStore', { recursive: false, node: node, rootVisible: false, model: me.getStore().getModel() }); node.expand(); return Ext.Object.merge({ xtype: 'list', pressedDelay: 250, autoDestroy: true, store: nodeStore, onItemDisclosure: me.getOnItemDisclosure(), allowDeselect : me.getAllowDeselect(), listeners: [ { event: 'itemdoubletap', fn: 'onItemDoubleTap', scope: me }, { event: 'itemtap', fn: 'onItemInteraction', scope: me, order: 'before'}, { event: 'itemtouchstart', fn: 'onItemInteraction', scope: me, order: 'before'}, { event: 'itemtap', fn: 'onItemTap', scope: me }, { event: 'beforeselectionchange', fn: 'onBeforeSelect', scope: me }, { event: 'containertap', fn: 'onContainerTap', scope: me }, { event: 'selectionchange', fn: 'onSelectionChange', order: 'before', scope: me } ], itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>' }, this.getListConfig()); } }, function() { //<deprecated product=touch since=2.0> /** * @member Ext.dataview.NestedList * @method getSubList * Returns the subList for a specified node. * @removed 2.0.0 */ Ext.deprecateMethod(this, 'getSubList', null, "Ext.dataview.NestedList.getSubList() has been removed"); /** * @member Ext.dataview.NestedList * @cfg {Number} clearSelectionDelay * Number of milliseconds to show the highlight when going back in a list. * @removed 2.0.0 */ Ext.deprecateProperty(this, 'clearSelectionDelay', null, "Ext.dataview.NestedList.clearSelectionDelay has been removed"); //</deprecated> });