/**
* @class Ext.List
* @extends Ext.DataView
* A mechanism for displaying data using a list layout template. List uses an {@link Ext.XTemplate}
* as its internal templating mechanism, and is bound to an {@link Ext.data.Store} so that as the data
* in the store changes the view is automatically updated to reflect the changes.
* The view also provides built-in behavior for many common events that can occur for its contained items
* including itemtap, containertap, etc. as well as a built-in selection model. In order to use these
* features, an {@link #itemSelector} config must be provided for the DataView to determine what nodes it
* will be working with.
*
* Useful Properties
*
* - {@link #itemTpl}
* - {@link #store}
* - {@link #grouped}
* - {@link #indexBar}
* - {@link #singleSelect}
* - {@link #multiSelect}
*
*
* Useful Methods
*
* - {@link #bindStore}
* - {@link #getRecord}
* - {@link #getRecords}
* - {@link #getSelectedRecords}
* - {@link #getSelectedNodes}
* - {@link #indexOf}
*
*
* Useful Events
*
* - {@link #itemtap}
* - {@link #itemdoubletap}
* - {@link #itemswipe}
* - {@link #selectionchange}
*
*
* Screenshot:
*
*
* Example code:
*
Ext.regModel('Contact', {
fields: ['firstName', 'lastName']
});
var store = new Ext.data.JsonStore({
model : 'Contact',
sorters: 'lastName',
getGroupString : function(record) {
return record.get('lastName')[0];
},
data: [
{firstName: 'Tommy', lastName: 'Maintz'},
{firstName: 'Rob', lastName: 'Dougan'},
{firstName: 'Ed', lastName: 'Spencer'},
{firstName: 'Jamie', lastName: 'Avins'},
{firstName: 'Aaron', lastName: 'Conran'},
{firstName: 'Dave', lastName: 'Kaneda'},
{firstName: 'Michael', lastName: 'Mullany'},
{firstName: 'Abraham', lastName: 'Elias'},
{firstName: 'Jay', lastName: 'Robinson'}
]
});
var list = new Ext.List({
fullscreen: true,
itemTpl : '{firstName} {lastName}',
grouped : true,
indexBar: true,
store: store
});
list.show();
* @constructor
* Create a new List
* @param {Object} config The config object
* @xtype list
*/
Ext.List = Ext.extend(Ext.DataView, {
componentCls: 'x-list',
/**
* @cfg {Boolean} pinHeaders
* Whether or not to pin headers on top of item groups while scrolling for an iPhone native list experience
* Defaults to true
*/
pinHeaders: (Ext.is.Android || Ext.is.Blackberry) ? false : true,
/**
* @cfg {Boolean/Object} indexBar
* True to render an alphabet IndexBar docked on the right.
* This can also be a config object that will be passed to {@link Ext.IndexBar}
* (defaults to false)
*/
indexBar: false,
/**
* @cfg {Boolean} grouped
* True to group the list items together (defaults to false). When using grouping, you must specify a method getGroupString
* on the store so that grouping can be maintained.
*
Ext.regModel('Contact', {
fields: ['firstName', 'lastName']
});
var store = new Ext.data.JsonStore({
model : 'Contact',
sorters: 'lastName',
getGroupString : function(record) {
// Group by the last name
return record.get('lastName')[0];
},
data: [
{firstName: 'Tommy', lastName: 'Maintz'},
{firstName: 'Rob', lastName: 'Dougan'},
{firstName: 'Ed', lastName: 'Spencer'},
{firstName: 'Jamie', lastName: 'Avins'},
{firstName: 'Aaron', lastName: 'Conran'},
{firstName: 'Dave', lastName: 'Kaneda'},
{firstName: 'Michael', lastName: 'Mullany'},
{firstName: 'Abraham', lastName: 'Elias'},
{firstName: 'Jay', lastName: 'Robinson'},
{firstName: 'Tommy', lastName: 'Maintz'},
{firstName: 'Rob', lastName: 'Dougan'},
{firstName: 'Ed', lastName: 'Spencer'},
{firstName: 'Jamie', lastName: 'Avins'},
{firstName: 'Aaron', lastName: 'Conran'},
{firstName: 'Dave', lastName: 'Kaneda'},
{firstName: 'Michael', lastName: 'Mullany'},
{firstName: 'Abraham', lastName: 'Elias'},
{firstName: 'Jay', lastName: 'Robinson'}
]
});
*/
grouped: false,
/**
* @cfg {Boolean} clearSelectionOnDeactivate
* True to clear any selections on the list when the list is deactivated (defaults to true).
*/
clearSelectionOnDeactivate: true,
renderTpl: [
''
],
groupTpl : [
'',
'',
'',
'
',
'{items}',
'
',
'
',
''
],
/**
* @cfg {String} itemSelector
* @private
* @ignore
* Not to be used.
*/
itemSelector: '.x-list-item',
/**
* @cfg {String/Array} itemTpl
* The inner portion of the item template to be rendered. Follows an XTemplate
* structure and will be placed inside of a tpl for in the tpl configuration.
*/
/**
* @cfg {Boolean/Function/Object} onItemDisclosure
* True to display a disclosure icon on each list item.
* This won't bind a listener to the tap event. The list
* will still fire the disclose event though.
* By setting this config to a function, it will automatically
* add a tap event listeners to the disclosure buttons which
* will fire your function.
* Finally you can specify an object with a 'scope' and 'handler'
* property defined. This will also be bound to the tap event listener
* and is useful when you want to change the scope of the handler.
*/
onItemDisclosure: false,
// @private
initComponent : function() {
//
if (Ext.isDefined(this.dockedItems)) {
console.warn("List: List is not a Panel anymore so you can't dock items to it. Please put this list inside a Panel with layout 'fit'");
}
//
//
if (Ext.isDefined(this.components)) {
console.warn("List: The Experimental components configuration is not currently supported.");
}
//
//
if (Ext.isDefined(this.disclosure)) {
console.warn("List: The disclosure configuration has been renamed to onItemDisclosure and will be removed.");
this.onItemDisclosure = this.disclosure;
}
//
var memberFnsCombo = {};
//
if (this.tpl) {
console.warn('Ext.List: The tpl config has been removed and replaced by itemTpl. Please remove tpl and itemSelector from your Lists.');
// convert from array to string
if (Ext.isArray(this.tpl)) {
this.tpl = this.tpl.join('');
} else if (this.tpl.html) {
Ext.apply(memberFnsCombo, this.tpl.initialConfig);
this.tpl = this.tpl.html;
}
this.tpl = Ext.util.Format.trim(this.tpl);
if (this.tpl.indexOf("\"x-list-item\"") !== -1) {
throw new Error("Ext.List: Using a CSS class of x-list-item within your own tpl will break Ext.Lists. Remove the x-list-item from the tpl/itemTpl");
}
var tpl = this.tpl,
first = '',
firstLn = first.length,
end = '',
tplFirst = this.tpl.substr(0, firstLn),
tplEndIdx = this.tpl.lastIndexOf(end),
stripped;
if (tplFirst === first && tplEndIdx !== -1) {
this.itemTpl = tpl.substr(firstLn, tplEndIdx - firstLn);
this.itemSelector = Ext.List.prototype.itemSelector;
} else {
throw new Error("Ext.List: tpl to itemTpl conversion failed.");
}
}
//
if (Ext.isArray(this.itemTpl)) {
this.itemTpl = this.itemTpl.join('');
} else if (this.itemTpl && this.itemTpl.html) {
Ext.apply(memberFnsCombo, this.itemTpl.initialConfig);
this.itemTpl = this.itemTpl.html;
}
//
if (!Ext.isDefined(this.itemTpl)) {
throw new Error("Ext.List: itemTpl is a required configuration.");
}
// this check is not enitrely fool proof, does not account for spaces or multiple classes
// if the check is done without "s then things like x-list-item-entity would throw exceptions that shouldn't have.
if (this.itemTpl && this.itemTpl.indexOf("\"x-list-item\"") !== -1) {
throw new Error("Ext.List: Using a CSS class of x-list-item within your own tpl will break Ext.Lists. Remove the x-list-item from the tpl/itemTpl");
}
//
this.tpl = '' + this.itemTpl + '
';
if (this.onItemDisclosure) {
this.tpl += '
';
}
this.tpl += '
';
this.tpl = new Ext.XTemplate(this.tpl, memberFnsCombo);
if (this.grouped) {
this.listItemTpl = this.tpl;
if (Ext.isString(this.listItemTpl) || Ext.isArray(this.listItemTpl)) {
// memberFns will go away after removal of tpl configuration for itemTpl
// this copies memberFns by storing the original configuration.
this.listItemTpl = new Ext.XTemplate(this.listItemTpl, memberFnsCombo);
}
if (Ext.isString(this.groupTpl) || Ext.isArray(this.groupTpl)) {
this.tpl = new Ext.XTemplate(this.groupTpl);
}
}
else {
this.indexBar = false;
}
if (this.scroll !== false) {
this.scroll = {
direction: 'vertical',
useIndicators: !this.indexBar
};
}
Ext.List.superclass.initComponent.call(this);
if (this.onItemDisclosure) {
// disclosure can be a function that will be called when
// you tap the disclosure button
if (Ext.isFunction(this.onItemDisclosure)) {
this.onItemDisclosure = {
scope: this,
handler: this.onItemDisclosure
};
}
}
this.on('deactivate', this.onDeactivate, this);
this.addEvents(
/**
* @event disclose
* Fires when the user taps the disclosure icon on an item
* @param {Ext.data.Record} record The record associated with the item
* @param {Ext.Element} node The wrapping element of this node
* @param {Number} index The index of this list item
*/
'disclose'
);
},
// @private
onRender : function() {
if (this.grouped) {
Ext.applyIf(this.renderData, {
grouped: true
});
if (this.scroll) {
Ext.applyIf(this.renderSelectors, {
header: '.x-list-header-swap'
});
}
}
Ext.List.superclass.onRender.apply(this, arguments);
},
// @private
onDeactivate : function() {
if (this.clearSelectionOnDeactivate) {
this.getSelectionModel().deselectAll();
}
},
// @private
afterRender : function() {
if (!this.grouped) {
this.el.addCls('x-list-flat');
}
this.getTargetEl().addCls('x-list-parent');
if (this.indexBar) {
this.indexBar = new Ext.IndexBar(Ext.apply({}, Ext.isObject(this.indexBar) ? this.indexBar : {}, {
xtype: 'indexbar',
alphabet: true,
renderTo: this.el
}));
this.addCls('x-list-indexed');
}
Ext.List.superclass.afterRender.call(this);
if (this.onItemDisclosure) {
this.mon(this.getTargetEl(), 'singletap', this.handleItemDisclosure, this, {delegate: '.x-list-disclosure'});
}
},
// @private
initEvents : function() {
Ext.List.superclass.initEvents.call(this);
if (this.grouped) {
if (this.pinHeaders && this.scroll) {
this.mon(this.scroller, {
scrollstart: this.onScrollStart,
scroll: this.onScroll,
scope: this
});
}
if (this.indexBar) {
this.mon(this.indexBar, {
index: this.onIndex,
scope: this
});
}
}
},
//@private
handleItemDisclosure : function(e, t) {
var node = this.findItemByChild(t),
record, index;
if (node) {
record = this.getRecord(node);
index = this.indexOf(node);
this.fireEvent('disclose', record, node, index);
if (Ext.isObject(this.onItemDisclosure) && this.onItemDisclosure.handler) {
this.onItemDisclosure.handler.call(this, record, node, index);
}
}
},
/**
* Set the current active group
* @param {Object} group The group to set active
*/
setActiveGroup : function(group) {
var me = this;
if (group) {
if (!me.activeGroup || me.activeGroup.header != group.header) {
me.header.setHTML(group.header.getHTML());
me.header.show();
}
}
else {
me.header.hide();
}
this.activeGroup = group;
},
// @private
getClosestGroups : function(pos) {
// force update if not already done
if (!this.groupOffsets) {
this.updateOffsets();
}
var groups = this.groupOffsets,
ln = groups.length,
group, i,
current, next;
for (i = 0; i < ln; i++) {
group = groups[i];
if (group.offset > pos.y) {
next = group;
break;
}
current = group;
}
return {
current: current,
next: next
};
},
updateIndexes : function() {
Ext.List.superclass.updateIndexes.apply(this, arguments);
this.updateOffsets();
},
afterComponentLayout : function() {
Ext.List.superclass.afterComponentLayout.apply(this, arguments);
this.updateOffsets();
},
updateOffsets : function() {
if (this.grouped) {
this.groupOffsets = [];
var headers = this.getTargetEl().query('h3.x-list-header'),
ln = headers.length,
header, i;
for (i = 0; i < ln; i++) {
header = Ext.get(headers[i]);
header.setDisplayMode(Ext.Element.VISIBILITY);
this.groupOffsets.push({
header: header,
offset: header.dom.offsetTop
});
}
}
},
// @private
onScrollStart : function() {
var offset = this.scroller.getOffset();
this.closest = this.getClosestGroups(offset);
this.setActiveGroup(this.closest.current);
},
// @private
onScroll : function(scroller, pos, options) {
if (!this.closest) {
this.closest = this.getClosestGroups(pos);
}
if (!this.headerHeight) {
this.headerHeight = this.header.getHeight();
}
if (pos.y <= 0) {
if (this.activeGroup) {
this.setActiveGroup(false);
this.closest.next = this.closest.current;
}
return;
}
else if (
(this.closest.next && pos.y > this.closest.next.offset) ||
(pos.y < this.closest.current.offset)
) {
this.closest = this.getClosestGroups(pos);
this.setActiveGroup(this.closest.current);
}
if (this.closest.next && pos.y > 0 && this.closest.next.offset - pos.y <= this.headerHeight) {
var transform = this.headerHeight - (this.closest.next.offset - pos.y);
Ext.Element.cssTransform(this.header, {translate: [0, -transform]});
this.transformed = true;
}
else if (this.transformed) {
this.header.setStyle('-webkit-transform', null);
this.transformed = false;
}
},
// @private
onIndex : function(record, target, index) {
var key = record.get('key').toLowerCase(),
groups = this.store.getGroups(),
ln = groups.length,
group, i, closest, id;
for (i = 0; i < ln; i++) {
group = groups[i];
id = this.getGroupId(group);
if (id == key || id > key) {
closest = id;
break;
}
else {
closest = id;
}
}
closest = this.getTargetEl().down('.x-group-' + id);
if (closest) {
this.scroller.scrollTo({x: 0, y: closest.getOffsetsTo(this.scrollEl)[1]}, false, null, true);
}
},
getGroupId : function(group) {
return group.name.toLowerCase();
},
// @private
collectData : function(records, startIndex) {
if (!this.grouped) {
return Ext.List.superclass.collectData.call(this, records, startIndex);
}
var results = [],
groups = this.store.getGroups(),
ln = groups.length,
children, cln, c,
group, i;
for (i = 0, ln = groups.length; i < ln; i++) {
group = groups[i];
children = group.children;
for (c = 0, cln = children.length; c < cln; c++) {
children[c] = children[c].data;
}
results.push({
group: group.name,
id: this.getGroupId(group),
items: this.listItemTpl.apply(children)
});
}
return results;
},
// Because the groups might change by an update/add/remove we refresh the whole dataview
// in each one of them
// @private
onUpdate : function(store, record) {
if (this.grouped) {
this.refresh();
}
else {
Ext.List.superclass.onUpdate.apply(this, arguments);
}
},
// @private
onAdd : function(ds, records, index) {
if (this.grouped) {
this.refresh();
}
else {
Ext.List.superclass.onAdd.apply(this, arguments);
}
},
// @private
onRemove : function(ds, record, index) {
if (this.grouped) {
this.refresh();
}
else {
Ext.List.superclass.onRemove.apply(this, arguments);
}
}
});
Ext.reg('list', Ext.List);