/**
* 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
* @aside example nested-list
* @aside example navigation-view
*/
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 {@link #getItemTextTpl} or
* {@link #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 {@link Ext.List#onItemDisclosure} configuration for individual lists.
* @accessor
*/
onItemDisclosure: false,
/**
* @cfg {Boolean} allowDeselect
* Set to `true` to allow the user to deselect leaf items via interaction.
* @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/String} 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) {
if (newContainer) {
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 which 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 scrolling 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) {
if (Ext.isString(store)) {
// store id
store = Ext.data.StoreManager.get(store);
} else {
// store instance or store config
if (!(store instanceof Ext.data.TreeStore)) {
store = Ext.factory(store, Ext.data.TreeStore, null);
}
}
//
if (!store) {
Ext.Logger.warn("The specified Store cannot be found", this);
}
//
}
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, oldDetailCard) {
if (config === null) {
return Ext.factory(config, Ext.Component, oldDetailCard);
} else {
return Ext.factory(config, Ext.Component);
}
},
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
* @return {String}
*/
getItemTextTpl: function (node) {
return '{' + this.getDisplayField() + '}';
},
/**
* Override this method to provide custom template rendering of titles/back
* buttons when {@link #useTitleAsBackText} is enabled.
* @param {Ext.data.Record} node
* @return {String}
*/
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);
}
list = me.getLastActiveList();
list.getStore().setNode(node);
node.expand();
me.setActiveItem(list);
}
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(),
variableHeights: false,
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: ' class="x-list-item-leaf">' + me.getItemTextTpl(node) + ''
}, this.getListConfig());
}
}, function () {
//
/**
* @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");
//
});