3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.view.AbstractView
17 * @extends Ext.Component
18 * This is an abstract superclass and should not be used directly. Please see {@link Ext.view.View}.
21 Ext.define('Ext.view.AbstractView', {
22 extend: 'Ext.Component',
23 alternateClassName: 'Ext.view.AbstractView',
26 'Ext.data.StoreManager',
27 'Ext.CompositeElementLite',
29 'Ext.selection.DataViewModel'
33 getRecord: function(node) {
34 return this.getBoundView(node).getRecord(node);
37 getBoundView: function(node) {
38 return Ext.getCmp(node.boundView);
43 * @cfg {String/String[]/Ext.XTemplate} tpl (required)
44 * The HTML fragment or an array of fragments that will make up the template used by this DataView. This should
45 * be specified in the same format expected by the constructor of {@link Ext.XTemplate}.
48 * @cfg {Ext.data.Store} store (required)
49 * The {@link Ext.data.Store} to bind this DataView to.
53 * @cfg {Boolean} deferInitialRefresh
54 * <p>Defaults to <code>true</code> to defer the initial refresh of the view.</p>
55 * <p>This allows the View to execute its render and initial layout more quickly because the process will not be encumbered
56 * by the expensive update of the view structure.</p>
57 * <p><b>Important: </b>Be aware that this will mean that the View's item elements will not be available immediately upon render, so
58 * <i>selection</i> may not take place at render time. To access a View's item elements as soon as possible, use the {@link #viewready} event.
59 * Or set <code>deferInitialrefresh</code> to false, but this will be at the cost of slower rendering.</p>
61 deferInitialRefresh: true,
64 * @cfg {String} itemSelector (required)
65 * <b>This is a required setting</b>. A simple CSS selector (e.g. <tt>div.some-class</tt> or
66 * <tt>span:first-child</tt>) that will be used to determine what nodes this DataView will be
67 * working with. The itemSelector is used to map DOM nodes to records. As such, there should
68 * only be one root level element that matches the selector for each record.
72 * @cfg {String} itemCls
73 * Specifies the class to be assigned to each element in the view when used in conjunction with the
74 * {@link #itemTpl} configuration.
76 itemCls: Ext.baseCSSPrefix + 'dataview-item',
79 * @cfg {String/String[]/Ext.XTemplate} itemTpl
80 * The inner portion of the item template to be rendered. Follows an XTemplate
81 * structure and will be placed inside of a tpl.
85 * @cfg {String} overItemCls
86 * A CSS class to apply to each item in the view on mouseover.
87 * Ensure {@link #trackOver} is set to `true` to make use of this.
91 * @cfg {String} loadingText
92 * A string to display during data load operations. If specified, this text will be
93 * displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's
94 * contents will continue to display normally until the new data is loaded and the contents are replaced.
96 loadingText: 'Loading...',
99 * @cfg {Boolean/Object} loadMask
100 * False to disable a load mask from displaying will the view is loading. This can also be a
101 * {@link Ext.LoadMask} configuration object.
106 * @cfg {String} loadingCls
107 * The CSS class to apply to the loading message element. Defaults to Ext.LoadMask.prototype.msgCls "x-mask-loading".
111 * @cfg {Boolean} loadingUseMsg
112 * Whether or not to use the loading message.
119 * @cfg {Number} loadingHeight
120 * If specified, gives an explicit height for the data view when it is showing the {@link #loadingText},
121 * if that is specified. This is useful to prevent the view's height from collapsing to zero when the
122 * loading mask is applied and there are no other contents in the data view.
126 * @cfg {String} [selectedItemCls='x-view-selected']
127 * A CSS class to apply to each selected item in the view.
129 selectedItemCls: Ext.baseCSSPrefix + 'item-selected',
132 * @cfg {String} emptyText
133 * The text to display in the view when there is no data to display.
134 * Note that when using local data the emptyText will not be displayed unless you set
135 * the {@link #deferEmptyText} option to false.
140 * @cfg {Boolean} deferEmptyText
141 * True to defer emptyText being applied until the store's first load.
143 deferEmptyText: true,
146 * @cfg {Boolean} trackOver
147 * True to enable mouseenter and mouseleave events
152 * @cfg {Boolean} blockRefresh
153 * Set this to true to ignore datachanged events on the bound store. This is useful if
154 * you wish to provide custom transition animations via a plugin
159 * @cfg {Boolean} disableSelection
160 * True to disable selection within the DataView. This configuration will lock the selection model
161 * that the DataView uses.
168 triggerEvent: 'itemclick',
169 triggerCtEvent: 'containerclick',
171 addCmpEvents: function() {
176 initComponent : function(){
178 isDef = Ext.isDefined,
179 itemTpl = me.itemTpl,
183 if (Ext.isArray(itemTpl)) {
185 itemTpl = itemTpl.join('');
186 } else if (Ext.isObject(itemTpl)) {
188 memberFn = Ext.apply(memberFn, itemTpl.initialConfig);
189 itemTpl = itemTpl.html;
192 if (!me.itemSelector) {
193 me.itemSelector = '.' + me.itemCls;
196 itemTpl = Ext.String.format('<tpl for="."><div class="{0}">{1}</div></tpl>', me.itemCls, itemTpl);
197 me.tpl = Ext.create('Ext.XTemplate', itemTpl, memberFn);
201 if (!isDef(me.tpl) || !isDef(me.itemSelector)) {
203 sourceClass: 'Ext.view.View',
205 itemSelector: me.itemSelector,
206 msg: "DataView requires both tpl and itemSelector configurations to be defined."
212 if(Ext.isString(me.tpl) || Ext.isArray(me.tpl)){
213 me.tpl = Ext.create('Ext.XTemplate', me.tpl);
217 // backwards compat alias for overClass/selectedClass
218 // TODO: Consider support for overCls generation Ext.Component config
219 if (isDef(me.overCls) || isDef(me.overClass)) {
220 if (Ext.isDefined(Ext.global.console)) {
221 Ext.global.console.warn('Ext.view.View: Using the deprecated overCls or overClass configuration. Use overItemCls instead.');
223 me.overItemCls = me.overCls || me.overClass;
228 if (me.overItemCls) {
232 if (isDef(me.selectedCls) || isDef(me.selectedClass)) {
233 if (Ext.isDefined(Ext.global.console)) {
234 Ext.global.console.warn('Ext.view.View: Using the deprecated selectedCls or selectedClass configuration. Use selectedItemCls instead.');
236 me.selectedItemCls = me.selectedCls || me.selectedClass;
237 delete me.selectedCls;
238 delete me.selectedClass;
244 * @event beforerefresh
245 * Fires before the view is refreshed
246 * @param {Ext.view.View} this The DataView object
251 * Fires when the view is refreshed
252 * @param {Ext.view.View} this The DataView object
257 * Fires when the View's item elements representing Store items has been rendered. If the {@link #deferInitialRefresh} flag
258 * was set (and it is <code>true</code> by default), this will be <b>after</b> initial render, and no items will be available
259 * for selection until this event fires.
260 * @param {Ext.view.View} this
265 * Fires when the node associated with an individual record is updated
266 * @param {Ext.data.Model} record The model instance
267 * @param {Number} index The index of the record/node
268 * @param {HTMLElement} node The node that has just been updated
273 * Fires when the nodes associated with an recordset have been added to the underlying store
274 * @param {Ext.data.Model[]} records The model instance
275 * @param {Number} index The index at which the set of record/nodes starts
276 * @param {HTMLElement[]} node The node that has just been updated
281 * Fires when the node associated with an individual record is removed
282 * @param {Ext.data.Model} record The model instance
283 * @param {Number} index The index of the record/node
290 // Look up the configured Store. If none configured, use the fieldless, empty Store defined in Ext.data.Store.
291 me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');
292 me.all = new Ext.CompositeElementLite();
295 onRender: function() {
300 msgCls: me.loadingCls,
301 useMsg: me.loadingUseMsg
304 me.callParent(arguments);
307 // either a config object
308 if (Ext.isObject(mask)) {
309 cfg = Ext.apply(cfg, mask);
311 // Attach the LoadMask to a *Component* so that it can be sensitive to resizing during long loads.
312 // If this DataView is floating, then mask this DataView.
313 // Otherwise, mask its owning Container (or this, if there *is* no owning Container).
314 // LoadMask captures the element upon render.
315 me.loadMask = Ext.create('Ext.LoadMask', me, cfg);
318 beforeshow: me.onMaskBeforeShow,
324 onMaskBeforeShow: function(){
325 var loadingHeight = this.loadingHeight;
327 this.getSelectionModel().deselectAll();
329 this.setCalculatedSize(undefined, loadingHeight);
333 onMaskHide: function(){
336 if (!me.destroying && me.loadingHeight) {
337 me.setHeight(me.height);
341 afterRender: function() {
342 this.callParent(arguments);
344 // Init the SelectionModel after any on('render') listeners have been added.
345 // Drag plugins create a DragDrop instance in a render listener, and that needs
346 // to see an itemmousedown event first.
347 this.getSelectionModel().bindComponent(this);
351 * Gets the selection model for this view.
352 * @return {Ext.selection.Model} The selection model
354 getSelectionModel: function(){
362 if (me.simpleSelect) {
364 } else if (me.multiSelect) {
368 Ext.applyIf(me.selModel, {
369 allowDeselect: me.allowDeselect,
373 if (!me.selModel.events) {
374 me.selModel = Ext.create('Ext.selection.DataViewModel', me.selModel);
377 if (!me.selModel.hasRelaySetup) {
378 me.relayEvents(me.selModel, [
379 'selectionchange', 'beforeselect', 'beforedeselect', 'select', 'deselect'
381 me.selModel.hasRelaySetup = true;
384 // lock the selection model if user
385 // has disabled selection
386 if (me.disableSelection) {
387 me.selModel.locked = true;
394 * Refreshes the view by reloading the data from the store and re-rendering the template.
396 refresh: function() {
401 if (!me.rendered || me.isDestroyed) {
405 me.fireEvent('beforerefresh', me);
406 el = me.getTargetEl();
407 records = me.store.getRange();
410 if (records.length < 1) {
411 if (!me.deferEmptyText || me.hasSkippedEmptyText) {
412 el.update(me.emptyText);
416 me.tpl.overwrite(el, me.collectData(records, 0));
417 me.all.fill(Ext.query(me.getItemSelector(), el.dom));
421 me.selModel.refresh();
422 me.hasSkippedEmptyText = true;
423 me.fireEvent('refresh', me);
425 // Upon first refresh, fire the viewready event.
426 // Reconfiguring the grid "renews" this event.
428 // Fire an event when deferred content becomes available.
429 // This supports grid Panel's deferRowRender capability
431 me.fireEvent('viewready', me);
436 * Function which can be overridden to provide custom formatting for each Record that is used by this
437 * DataView's {@link #tpl template} to render each node.
438 * @param {Object/Object[]} data The raw data object that was used to create the Record.
439 * @param {Number} recordIndex the index number of the Record being prepared for rendering.
440 * @param {Ext.data.Model} record The Record being prepared for rendering.
441 * @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s overwrite() method.
442 * (either an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}))
444 prepareData: function(data, index, record) {
446 Ext.apply(data, record.getAssociatedData());
452 * <p>Function which can be overridden which returns the data object passed to this
453 * DataView's {@link #tpl template} to render the whole DataView.</p>
454 * <p>This is usually an Array of data objects, each element of which is processed by an
455 * {@link Ext.XTemplate XTemplate} which uses <tt>'<tpl for=".">'</tt> to iterate over its supplied
456 * data object as an Array. However, <i>named</i> properties may be placed into the data object to
457 * provide non-repeating data such as headings, totals etc.</p>
458 * @param {Ext.data.Model[]} records An Array of {@link Ext.data.Model}s to be rendered into the DataView.
459 * @param {Number} startIndex the index number of the Record being prepared for rendering.
460 * @return {Object[]} An Array of data objects to be processed by a repeating XTemplate. May also
461 * contain <i>named</i> properties.
463 collectData : function(records, startIndex){
466 len = records.length,
471 r[r.length] = this.prepareData(record[record.persistenceProperty], startIndex + i, record);
477 bufferRender : function(records, index){
478 var div = document.createElement('div');
479 this.tpl.overwrite(div, this.collectData(records, index));
480 return Ext.query(this.getItemSelector(), div);
484 onUpdate : function(ds, record){
486 index = me.store.indexOf(record),
490 node = me.bufferRender([record], index)[0];
491 // ensure the node actually exists in the DOM
492 if (me.getNode(record)) {
493 me.all.replaceElement(index, node, true);
494 me.updateIndexes(index, index);
495 // Maintain selection after update
496 // TODO: Move to approriate event handler.
497 me.selModel.refresh();
498 me.fireEvent('itemupdate', record, index, node);
505 onAdd : function(ds, records, index) {
509 if (me.all.getCount() === 0) {
514 nodes = me.bufferRender(records, index);
515 me.doAdd(nodes, records, index);
517 me.selModel.refresh();
518 me.updateIndexes(index);
519 me.fireEvent('itemadd', records, index, nodes);
522 doAdd: function(nodes, records, index) {
525 if (index < all.getCount()) {
526 all.item(index).insertSibling(nodes, 'before', true);
528 all.last().insertSibling(nodes, 'after', true);
531 Ext.Array.insert(all.elements, index, nodes);
535 onRemove : function(ds, record, index) {
538 me.doRemove(record, index);
539 me.updateIndexes(index);
540 if (me.store.getCount() === 0){
543 me.fireEvent('itemremove', record, index);
546 doRemove: function(record, index) {
547 this.all.removeElement(index, true);
551 * Refreshes an individual node's data from the store.
552 * @param {Number} index The item's data index in the store
554 refreshNode : function(index){
555 this.onUpdate(this.store, this.store.getAt(index));
559 updateIndexes : function(startIndex, endIndex) {
560 var ns = this.all.elements,
561 records = this.store.getRange(),
564 startIndex = startIndex || 0;
565 endIndex = endIndex || ((endIndex === 0) ? 0 : (ns.length - 1));
566 for(i = startIndex; i <= endIndex; i++){
568 ns[i].viewRecordId = records[i].internalId;
569 if (!ns[i].boundView) {
570 ns[i].boundView = this.id;
576 * Returns the store associated with this DataView.
577 * @return {Ext.data.Store} The store
579 getStore : function(){
584 * Changes the data store bound to this view and refreshes it.
585 * @param {Ext.data.Store} store The store to bind to this view
587 bindStore : function(store, initial) {
591 if (!initial && me.store) {
592 if (store !== me.store && me.store.autoDestroy) {
593 me.store.destroyStore();
598 datachanged: me.onDataChanged,
606 // Ensure we have an instantiated LoadMask before we unbind it.
607 if (me.loadMask && me.loadMask.bindStore) {
608 me.loadMask.bindStore(null);
614 store = Ext.data.StoreManager.lookup(store);
617 datachanged: me.onDataChanged,
623 // Ensure we have an instantiated LoadMask before we bind it.
624 if (me.loadMask && me.loadMask.bindStore) {
625 // View's store is a NodeStore, use owning TreePanel's Store
626 if (Ext.Array.contains(store.alias, 'store.node')) {
627 maskStore = this.ownerCt.store;
631 me.loadMask.bindStore(maskStore);
635 // Flag to say that initial refresh has not been performed.
636 // Set here rather than at initialization time, so that a reconfigure with a new store will refire viewready
637 me.viewReady = false;
640 // Bind the store to our selection model
641 me.getSelectionModel().bind(store);
644 * This code used to have checks for:
645 * if (store && (!initial || store.getCount() || me.emptyText)) {
646 * Instead, just trigger a refresh and let the view itself figure out
647 * what needs to happen. It can cause incorrect display if our store
651 if (initial && me.deferInitialRefresh) {
652 Ext.Function.defer(function () {
653 if (!me.isDestroyed) {
665 * Calls this.refresh if this.blockRefresh is not true
667 onDataChanged: function() {
668 if (this.blockRefresh !== true) {
669 this.refresh.apply(this, arguments);
674 * Returns the template node the passed child belongs to, or null if it doesn't belong to one.
675 * @param {HTMLElement} node
676 * @return {HTMLElement} The template node
678 findItemByChild: function(node){
679 return Ext.fly(node).findParent(this.getItemSelector(), this.getTargetEl());
683 * Returns the template node by the Ext.EventObject or null if it is not found.
684 * @param {Ext.EventObject} e
686 findTargetByEvent: function(e) {
687 return e.getTarget(this.getItemSelector(), this.getTargetEl());
692 * Gets the currently selected nodes.
693 * @return {HTMLElement[]} An array of HTMLElements
695 getSelectedNodes: function(){
697 records = this.selModel.getSelection(),
701 for (; i < ln; i++) {
702 nodes.push(this.getNode(records[i]));
709 * Gets an array of the records from an array of nodes
710 * @param {HTMLElement[]} nodes The nodes to evaluate
711 * @return {Ext.data.Model[]} records The {@link Ext.data.Model} objects
713 getRecords: function(nodes) {
717 data = this.store.data;
719 for (; i < len; i++) {
720 records[records.length] = data.getByKey(nodes[i].viewRecordId);
727 * Gets a record from a node
728 * @param {Ext.Element/HTMLElement} node The node to evaluate
730 * @return {Ext.data.Model} record The {@link Ext.data.Model} object
732 getRecord: function(node){
733 return this.store.data.getByKey(Ext.getDom(node).viewRecordId);
738 * Returns true if the passed node is selected, else false.
739 * @param {HTMLElement/Number/Ext.data.Model} node The node, node index or record to check
740 * @return {Boolean} True if selected, else false
742 isSelected : function(node) {
743 // TODO: El/Idx/Record
744 var r = this.getRecord(node);
745 return this.selModel.isSelected(r);
749 * Selects a record instance by record instance or index.
750 * @param {Ext.data.Model[]/Number} records An array of records or an index
751 * @param {Boolean} [keepExisting] True to keep existing selections
752 * @param {Boolean} [suppressEvent] Set to true to not fire a select event
754 select: function(records, keepExisting, suppressEvent) {
755 this.selModel.select(records, keepExisting, suppressEvent);
759 * Deselects a record instance by record instance or index.
760 * @param {Ext.data.Model[]/Number} records An array of records or an index
761 * @param {Boolean} [suppressEvent] Set to true to not fire a deselect event
763 deselect: function(records, suppressEvent) {
764 this.selModel.deselect(records, suppressEvent);
768 * Gets a template node.
769 * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node,
770 * the id of a template node or the record associated with the node.
771 * @return {HTMLElement} The node or null if it wasn't found
773 getNode : function(nodeInfo) {
774 if (!this.rendered) {
777 if (Ext.isString(nodeInfo)) {
778 return document.getElementById(nodeInfo);
780 if (Ext.isNumber(nodeInfo)) {
781 return this.all.elements[nodeInfo];
783 if (nodeInfo instanceof Ext.data.Model) {
784 return this.getNodeByRecord(nodeInfo);
786 return nodeInfo; // already an HTMLElement
792 getNodeByRecord: function(record) {
793 var ns = this.all.elements,
797 for (; i < ln; i++) {
798 if (ns[i].viewRecordId === record.internalId) {
807 * Gets a range nodes.
808 * @param {Number} start (optional) The index of the first node in the range
809 * @param {Number} end (optional) The index of the last node in the range
810 * @return {HTMLElement[]} An array of nodes
812 getNodes: function(start, end) {
813 var ns = this.all.elements,
818 end = !Ext.isDefined(end) ? Math.max(ns.length - 1, 0) : end;
820 for (i = start; i <= end && ns[i]; i++) {
824 for (i = start; i >= end && ns[i]; i--) {
832 * Finds the index of the passed node.
833 * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node, the id of a template node
834 * or a record associated with a node.
835 * @return {Number} The index of the node or -1
837 indexOf: function(node) {
838 node = this.getNode(node);
839 if (Ext.isNumber(node.viewIndex)) {
840 return node.viewIndex;
842 return this.all.indexOf(node);
845 onDestroy : function() {
851 me.selModel.destroy();
854 // invoked by the selection model to maintain visual UI cues
855 onItemSelect: function(record) {
856 var node = this.getNode(record);
859 Ext.fly(node).addCls(this.selectedItemCls);
863 // invoked by the selection model to maintain visual UI cues
864 onItemDeselect: function(record) {
865 var node = this.getNode(record);
868 Ext.fly(node).removeCls(this.selectedItemCls);
872 getItemSelector: function() {
873 return this.itemSelector;
876 // all of this information is available directly
877 // from the SelectionModel itself, the only added methods
878 // to DataView regarding selection will perform some transformation/lookup
879 // between HTMLElement/Nodes to records and vice versa.
880 Ext.deprecate('extjs', '4.0', function() {
881 Ext.view.AbstractView.override({
883 * @cfg {Boolean} [multiSelect=false]
884 * True to allow selection of more than one item at a time, false to allow selection of only a single item
885 * at a time or no selection at all, depending on the value of {@link #singleSelect}.
888 * @cfg {Boolean} [singleSelect=false]
889 * True to allow selection of exactly one item at a time, false to allow no selection at all.
890 * Note that if {@link #multiSelect} = true, this value will be ignored.
893 * @cfg {Boolean} [simpleSelect=false]
894 * True to enable multiselection by clicking on multiple items without requiring the user to hold Shift or Ctrl,
895 * false to force the user to hold Ctrl or Shift to select more than on item.
899 * Gets the number of selected nodes.
900 * @return {Number} The node count
902 getSelectionCount : function(){
903 if (Ext.global.console) {
904 Ext.global.console.warn("DataView: getSelectionCount will be removed, please interact with the Ext.selection.DataViewModel");
906 return this.selModel.getSelection().length;
910 * Gets an array of the selected records
911 * @return {Ext.data.Model[]} An array of {@link Ext.data.Model} objects
913 getSelectedRecords : function(){
914 if (Ext.global.console) {
915 Ext.global.console.warn("DataView: getSelectedRecords will be removed, please interact with the Ext.selection.DataViewModel");
917 return this.selModel.getSelection();
920 select: function(records, keepExisting, supressEvents) {
921 if (Ext.global.console) {
922 Ext.global.console.warn("DataView: select will be removed, please access select through a DataView's SelectionModel, ie: view.getSelectionModel().select()");
924 var sm = this.getSelectionModel();
925 return sm.select.apply(sm, arguments);
928 clearSelections: function() {
929 if (Ext.global.console) {
930 Ext.global.console.warn("DataView: clearSelections will be removed, please access deselectAll through DataView's SelectionModel, ie: view.getSelectionModel().deselectAll()");
932 var sm = this.getSelectionModel();
933 return sm.deselectAll();