X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/c930e9176a5a85509c5b0230e2bff5c22a591432..25ef3491bd9ae007ff1fc2b0d7943e6eaaccf775:/examples/ux/ux-all-debug.js diff --git a/examples/ux/ux-all-debug.js b/examples/ux/ux-all-debug.js index 6ef28d4d..695e1cec 100644 --- a/examples/ux/ux-all-debug.js +++ b/examples/ux/ux-all-debug.js @@ -1,5 +1,5 @@ /*! - * Ext JS Library 3.0.0 + * Ext JS Library 3.0.3 * Copyright(c) 2006-2009 Ext JS, LLC * licensing@extjs.com * http://www.extjs.com/license @@ -157,6 +157,13 @@ Ext.ux.grid.BufferView = Ext.extend(Ext.grid.GridView, { this.doUpdate(); } }, + + onRemove : function(ds, record, index, isUpdate){ + Ext.ux.grid.BufferView.superclass.onRemove.apply(this, arguments); + if(isUpdate !== true){ + this.update(); + } + }, doUpdate: function(){ if (this.getVisibleRowCount() > 0) { @@ -539,6 +546,9 @@ Ext.DataView.DragSelector = function(cfg){ if(!proxy){ proxy = view.el.createChild({cls:'x-view-selector'}); }else{ + if(proxy.dom.parentNode !== view.el.dom){ + view.el.dom.appendChild(proxy.dom); + } proxy.setDisplayed('block'); } fillRegions(); @@ -653,15 +663,7 @@ Ext.ux.form.FileUploadField = Ext.extend(Ext.form.TextField, { this.wrap = this.el.wrap({cls:'x-form-field-wrap x-form-file-wrap'}); this.el.addClass('x-form-file-text'); this.el.dom.removeAttribute('name'); - - this.fileInput = this.wrap.createChild({ - id: this.getFileInputId(), - name: this.name||this.getId(), - cls: 'x-form-file', - tag: 'input', - type: 'file', - size: 1 - }); + this.createFileInput(); var btnCfg = Ext.applyIf(this.buttonCfg || {}, { text: this.buttonText @@ -676,11 +678,49 @@ Ext.ux.form.FileUploadField = Ext.extend(Ext.form.TextField, { this.wrap.setWidth(this.button.getEl().getWidth()); } - this.fileInput.on('change', function(){ - var v = this.fileInput.dom.value; - this.setValue(v); - this.fireEvent('fileselected', this, v); - }, this); + this.bindListeners(); + this.resizeEl = this.positionEl = this.wrap; + }, + + bindListeners: function(){ + this.fileInput.on({ + scope: this, + mouseenter: function() { + this.button.addClass(['x-btn-over','x-btn-focus']) + }, + mouseleave: function(){ + this.button.removeClass(['x-btn-over','x-btn-focus','x-btn-click']) + }, + mousedown: function(){ + this.button.addClass('x-btn-click') + }, + mouseup: function(){ + this.button.removeClass(['x-btn-over','x-btn-focus','x-btn-click']) + }, + change: function(){ + var v = this.fileInput.dom.value; + this.setValue(v); + this.fireEvent('fileselected', this, v); + } + }); + }, + + createFileInput : function() { + this.fileInput = this.wrap.createChild({ + id: this.getFileInputId(), + name: this.name||this.getId(), + cls: 'x-form-file', + tag: 'input', + type: 'file', + size: 1 + }); + }, + + reset : function(){ + this.fileInput.remove(); + this.createFileInput(); + this.bindListeners(); + Ext.ux.form.FileUploadField.superclass.reset.call(this); }, // private @@ -705,20 +745,27 @@ Ext.ux.form.FileUploadField = Ext.extend(Ext.form.TextField, { Ext.ux.form.FileUploadField.superclass.onDestroy.call(this); Ext.destroy(this.fileInput, this.button, this.wrap); }, + + onDisable: function(){ + Ext.ux.form.FileUploadField.superclass.onDisable.call(this); + this.doDisable(true); + }, + + onEnable: function(){ + Ext.ux.form.FileUploadField.superclass.onEnable.call(this); + this.doDisable(false); - - // private - preFocus : Ext.emptyFn, - + }, + // private - getResizeEl : function(){ - return this.wrap; + doDisable: function(disabled){ + this.fileInput.dom.disabled = disabled; + this.button.setDisabled(disabled); }, + // private - getPositionEl : function(){ - return this.wrap; - }, + preFocus : Ext.emptyFn, // private alignErrorIcon : function(){ @@ -731,1020 +778,2305 @@ Ext.reg('fileuploadfield', Ext.ux.form.FileUploadField); // backwards compat Ext.form.FileUploadField = Ext.ux.form.FileUploadField; -(function(){ -Ext.ns('Ext.a11y'); +/** + * @class Ext.ux.GMapPanel + * @extends Ext.Panel + * @author Shea Frederick + */ +Ext.ux.GMapPanel = Ext.extend(Ext.Panel, { + initComponent : function(){ + + var defConfig = { + plain: true, + zoomLevel: 3, + yaw: 180, + pitch: 0, + zoom: 0, + gmapType: 'map', + border: false + }; + + Ext.applyIf(this,defConfig); + + Ext.ux.GMapPanel.superclass.initComponent.call(this); -Ext.a11y.Frame = Ext.extend(Object, { - initialized: false, - - constructor: function(size, color){ - this.setSize(size || 1); - this.setColor(color || '15428B'); }, - - init: function(){ - if (!this.initialized) { - this.sides = []; - - var s, i; - - this.ct = Ext.DomHelper.append(document.body, { - cls: 'x-a11y-focusframe' - }, true); - - for (i = 0; i < 4; i++) { - s = Ext.DomHelper.append(this.ct, { - cls: 'x-a11y-focusframe-side', - style: 'background-color: #' + this.color - }, true); - s.visibilityMode = Ext.Element.DISPLAY; - this.sides.push(s); - } - - this.frameTask = new Ext.util.DelayedTask(function(el){ - var newEl = Ext.get(el); - if (newEl != this.curEl) { - var w = newEl.getWidth(); - var h = newEl.getHeight(); - this.sides[0].show().setSize(w, this.size).anchorTo(el, 'tl', [0, -1]); - this.sides[2].show().setSize(w, this.size).anchorTo(el, 'bl', [0, -1]); - this.sides[1].show().setSize(this.size, h).anchorTo(el, 'tr', [-1, 0]); - this.sides[3].show().setSize(this.size, h).anchorTo(el, 'tl', [-1, 0]); - this.curEl = newEl; + afterRender : function(){ + + var wh = this.ownerCt.getSize(); + Ext.applyIf(this, wh); + + Ext.ux.GMapPanel.superclass.afterRender.call(this); + + if (this.gmapType === 'map'){ + this.gmap = new GMap2(this.body.dom); + } + + if (this.gmapType === 'panorama'){ + this.gmap = new GStreetviewPanorama(this.body.dom); + } + + if (typeof this.addControl == 'object' && this.gmapType === 'map') { + this.gmap.addControl(this.addControl); + } + + if (typeof this.setCenter === 'object') { + if (typeof this.setCenter.geoCodeAddr === 'string'){ + this.geoCodeLookup(this.setCenter.geoCodeAddr); + }else{ + if (this.gmapType === 'map'){ + var point = new GLatLng(this.setCenter.lat,this.setCenter.lng); + this.gmap.setCenter(point, this.zoomLevel); } - }, this); - - this.unframeTask = new Ext.util.DelayedTask(function(){ - if (this.initialized) { - this.sides[0].hide(); - this.sides[1].hide(); - this.sides[2].hide(); - this.sides[3].hide(); - this.curEl = null; + if (typeof this.setCenter.marker === 'object' && typeof point === 'object'){ + this.addMarker(point,this.setCenter.marker,this.setCenter.marker.clear); } - }, this); - this.initialized = true; + } + if (this.gmapType === 'panorama'){ + this.gmap.setLocationAndPOV(new GLatLng(this.setCenter.lat,this.setCenter.lng), {yaw: this.yaw, pitch: this.pitch, zoom: this.zoom}); + } } + + GEvent.bind(this.gmap, 'load', this, function(){ + this.onMapReady(); + }); + }, - - frame: function(el){ - this.init(); - this.unframeTask.cancel(); - this.frameTask.delay(2, false, false, [el]); - }, - - unframe: function(){ - this.init(); - this.unframeTask.delay(2); - }, - - setSize: function(size){ - this.size = size; + onMapReady : function(){ + this.addMarkers(this.markers); + this.addMapControls(); + this.addOptions(); }, - - setColor: function(color){ - this.color = color; - } -}); - -Ext.a11y.FocusFrame = new Ext.a11y.Frame(2, '15428B'); -Ext.a11y.RelayFrame = new Ext.a11y.Frame(1, '6B8CBF'); + onResize : function(w, h){ -Ext.a11y.Focusable = Ext.extend(Ext.util.Observable, { - constructor: function(el, relayTo, noFrame, frameEl){ - Ext.a11y.Focusable.superclass.constructor.call(this); + if (typeof this.getMap() == 'object') { + this.gmap.checkResize(); + } - this.addEvents('focus', 'blur', 'left', 'right', 'up', 'down', 'esc', 'enter', 'space'); + Ext.ux.GMapPanel.superclass.onResize.call(this, w, h); + + }, + setSize : function(width, height, animate){ - if (el instanceof Ext.Component) { - this.el = el.el; - this.setComponent(el); - } - else { - this.el = Ext.get(el); - this.setComponent(null); + if (typeof this.getMap() == 'object') { + this.gmap.checkResize(); } - this.setRelayTo(relayTo) - this.setNoFrame(noFrame); - this.setFrameEl(frameEl); - - this.init(); + Ext.ux.GMapPanel.superclass.setSize.call(this, width, height, animate); - Ext.a11y.FocusMgr.register(this); - }, - - init: function(){ - this.el.dom.tabIndex = '1'; - this.el.addClass('x-a11y-focusable'); - this.el.on({ - focus: this.onFocus, - blur: this.onBlur, - keydown: this.onKeyDown, - scope: this - }); - }, - - setRelayTo: function(relayTo){ - this.relayTo = relayTo ? Ext.a11y.FocusMgr.get(relayTo) : null; - }, - - setNoFrame: function(noFrame){ - this.noFrame = (noFrame === true) ? true : false; - }, - - setFrameEl: function(frameEl){ - this.frameEl = frameEl && Ext.get(frameEl) || this.el; - }, - - setComponent: function(cmp){ - this.component = cmp || null; }, - - onKeyDown: function(e, t){ - var k = e.getKey(), SK = Ext.a11y.Focusable.SpecialKeys, ret, tf; + getMap : function(){ - tf = (t !== this.el.dom) ? Ext.a11y.FocusMgr.get(t, true) : this; - if (!tf) { - // this can happen when you are on a focused item within a panel body - // that is not a Ext.a11y.Focusable - tf = Ext.a11y.FocusMgr.get(Ext.fly(t).parent('.x-a11y-focusable')); - } + return this.gmap; - if (SK[k] !== undefined) { - ret = this.fireEvent(SK[k], e, t, tf, this); - } - if (ret === false || this.fireEvent('keydown', e, t, tf, this) === false) { - e.stopEvent(); - } }, - - focus: function(){ - this.el.dom.focus(); + getCenter : function(){ + + return this.getMap().getCenter(); + }, - - blur: function(){ - this.el.dom.blur(); + getCenterLatLng : function(){ + + var ll = this.getCenter(); + return {lat: ll.lat(), lng: ll.lng()}; + }, - - onFocus: function(e, t){ - this.el.addClass('x-a11y-focused'); - if (this.relayTo) { - this.relayTo.el.addClass('x-a11y-focused-relay'); - if (!this.relayTo.noFrame) { - Ext.a11y.FocusFrame.frame(this.relayTo.frameEl); - } - if (!this.noFrame) { - Ext.a11y.RelayFrame.frame(this.frameEl); - } - } - else { - if (!this.noFrame) { - Ext.a11y.FocusFrame.frame(this.frameEl); + addMarkers : function(markers) { + + if (Ext.isArray(markers)){ + for (var i = 0; i < markers.length; i++) { + var mkr_point = new GLatLng(markers[i].lat,markers[i].lng); + this.addMarker(mkr_point,markers[i].marker,false,markers[i].setCenter, markers[i].listeners); } } - this.fireEvent('focus', e, t, this); }, - - onBlur: function(e, t){ - if (this.relayTo) { - this.relayTo.el.removeClass('x-a11y-focused-relay'); - Ext.a11y.RelayFrame.unframe(); + addMarker : function(point, marker, clear, center, listeners){ + + Ext.applyIf(marker,G_DEFAULT_ICON); + + if (clear === true){ + this.getMap().clearOverlays(); } - this.el.removeClass('x-a11y-focused'); - Ext.a11y.FocusFrame.unframe(); - this.fireEvent('blur', e, t, this); - }, - - destroy: function(){ - this.el.un('keydown', this.onKeyDown); - this.el.un('focus', this.onFocus); - this.el.un('blur', this.onBlur); - this.el.removeClass('x-a11y-focusable'); - this.el.removeClass('x-a11y-focused'); - if (this.relayTo) { - this.relayTo.el.removeClass('x-a11y-focused-relay'); + if (center === true) { + this.getMap().setCenter(point, this.zoomLevel); } - } -}); -Ext.a11y.FocusItem = Ext.extend(Object, { - constructor: function(el, enableTabbing){ - Ext.a11y.FocusItem.superclass.constructor.call(this); - - this.el = Ext.get(el); - this.fi = new Ext.a11y.Focusable(el); - this.fi.setComponent(this); - - this.fi.on('tab', this.onTab, this); - - this.enableTabbing = enableTabbing === true ? true : false; - }, - - getEnterItem: function(){ - if (this.enableTabbing) { - var items = this.getFocusItems(); - if (items && items.length) { - return items[0]; + var mark = new GMarker(point,marker); + if (typeof listeners === 'object'){ + for (evt in listeners) { + GEvent.bind(mark, evt, this, listeners[evt]); } } + this.getMap().addOverlay(mark); + }, - - getFocusItems: function(){ - if (this.enableTabbing) { - return this.el.query('a, button, input, select'); - } - return null; - }, - - onTab: function(e, t){ - var items = this.getFocusItems(), i; + addMapControls : function(){ - if (items && items.length && (i = items.indexOf(t)) !== -1) { - if (e.shiftKey && i > 0) { - e.stopEvent(); - items[i - 1].focus(); - Ext.a11y.FocusFrame.frame.defer(20, Ext.a11y.FocusFrame, [this.el]); - return; - } - else - if (!e.shiftKey && i < items.length - 1) { - e.stopEvent(); - items[i + 1].focus(); - Ext.a11y.FocusFrame.frame.defer(20, Ext.a11y.FocusFrame, [this.el]); - return; + if (this.gmapType === 'map') { + if (Ext.isArray(this.mapControls)) { + for(i=0;i
Level '+accuracy+' Accuracy (8 = Exact Match, 1 = Vague Match)'); + }else{ + point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]); + if (typeof this.setCenter.marker === 'object' && typeof point === 'object'){ + this.addMarker(point,this.setCenter.marker,this.setCenter.marker.clear,true, this.setCenter.listeners); + } + } + } } - e.stopPropagation(); - }, - - onEsc: function(e, t){ - e.preventDefault(); - // check to see if esc is pressed while "inside" the panel - // or while "on" the panel - if (t === this.el.dom) { - // "on" the panel, check if this panel has an owner panel and focus that - // we dont stop the event in this case so that this same check will be - // done for this ownerCt - if (this.ownerCt) { - this.ownerCt.focus(); + } + +}); + +Ext.reg('gmappanel', Ext.ux.GMapPanel); Ext.namespace('Ext.ux.grid'); + +/** + * @class Ext.ux.grid.GridFilters + * @extends Ext.util.Observable + *

GridFilter is a plugin (ptype='gridfilters') for grids that + * allow for a slightly more robust representation of filtering than what is + * provided by the default store.

+ *

Filtering is adjusted by the user using the grid's column header menu + * (this menu can be disabled through configuration). Through this menu users + * can configure, enable, and disable filters for each column.

+ *

Features:

+ *
+ *

Example usage:

+ *
    
+var store = new Ext.data.GroupingStore({
+    ...
+});
+ 
+var filters = new Ext.ux.grid.GridFilters({
+    autoReload: false, //don't reload automatically
+    local: true, //only filter locally
+    // filters may be configured through the plugin,
+    // or in the column definition within the column model configuration
+    filters: [{
+        type: 'numeric',
+        dataIndex: 'id'
+    }, {
+        type: 'string',
+        dataIndex: 'name'
+    }, {
+        type: 'numeric',
+        dataIndex: 'price'
+    }, {
+        type: 'date',
+        dataIndex: 'dateAdded'
+    }, {
+        type: 'list',
+        dataIndex: 'size',
+        options: ['extra small', 'small', 'medium', 'large', 'extra large'],
+        phpMode: true
+    }, {
+        type: 'boolean',
+        dataIndex: 'visible'
+    }]
+});
+var cm = new Ext.grid.ColumnModel([{
+    ...
+}]);
+ 
+var grid = new Ext.grid.GridPanel({
+     ds: store,
+     cm: cm,
+     view: new Ext.grid.GroupingView(),
+     plugins: [filters],
+     height: 400,
+     width: 700,
+     bbar: new Ext.PagingToolbar({
+         store: store,
+         pageSize: 15,
+         plugins: [filters] //reset page to page 1 if filters change
+     })
+ });
+
+store.load({params: {start: 0, limit: 15}});
+
+// a filters property is added to the grid
+grid.filters
+ * 
+ */ +Ext.ux.grid.GridFilters = Ext.extend(Ext.util.Observable, { + /** + * @cfg {Boolean} autoReload + * Defaults to true, reloading the datasource when a filter change happens. + * Set this to false to prevent the datastore from being reloaded if there + * are changes to the filters. See {@link updateBuffer}. + */ + autoReload : true, + /** + * @cfg {Boolean} encode + * Specify true for {@link #buildQuery} to use Ext.util.JSON.encode to + * encode the filter query parameter sent with a remote request. + * Defaults to false. + */ + /** + * @cfg {Array} filters + * An Array of filters config objects. Refer to each filter type class for + * configuration details specific to each filter type. Filters for Strings, + * Numeric Ranges, Date Ranges, Lists, and Boolean are the standard filters + * available. + */ + /** + * @cfg {String} filterCls + * The css class to be applied to column headers with active filters. + * Defaults to 'ux-filterd-column'. + */ + filterCls : 'ux-filtered-column', + /** + * @cfg {Boolean} local + * true to use Ext.data.Store filter functions (local filtering) + * instead of the default (false) server side filtering. + */ + local : false, + /** + * @cfg {String} menuFilterText + * defaults to 'Filters'. + */ + menuFilterText : 'Filters', + /** + * @cfg {String} paramPrefix + * The url parameter prefix for the filters. + * Defaults to 'filter'. + */ + paramPrefix : 'filter', + /** + * @cfg {Boolean} showMenu + * Defaults to true, including a filter submenu in the default header menu. + */ + showMenu : true, + /** + * @cfg {String} stateId + * Name of the value to be used to store state information. + */ + stateId : undefined, + /** + * @cfg {Integer} updateBuffer + * Number of milliseconds to defer store updates since the last filter change. + */ + updateBuffer : 500, + + /** @private */ + constructor : function (config) { + this.deferredUpdate = new Ext.util.DelayedTask(this.reload, this); + this.filters = new Ext.util.MixedCollection(); + this.filters.getKey = function (o) { + return o ? o.dataIndex : null; + }; + this.addFilters(config.filters); + delete config.filters; + Ext.apply(this, config); + }, + + /** @private */ + init : function (grid) { + if (grid instanceof Ext.grid.GridPanel) { + this.grid = grid; + + this.bindStore(this.grid.getStore(), true); + + this.grid.filters = this; + + this.grid.addEvents({'filterupdate': true}); + + grid.on({ + scope: this, + beforestaterestore: this.applyState, + beforestatesave: this.saveState, + beforedestroy: this.destroy, + reconfigure: this.onReconfigure + }); + + if (grid.rendered){ + this.onRender(); + } else { + grid.on({ + scope: this, + single: true, + render: this.onRender + }); } + + } else if (grid instanceof Ext.PagingToolbar) { + this.toolbar = grid; } - else { - // we were inside the panel when esc was pressed, - // so go back "on" the panel - if (this.ownerCt && this.ownerCt.isFocusable) { - var si = this.ownerCt.getFocusItems(); - - if (si && si.getCount() > 1) { - e.stopEvent(); + }, + + /** + * @private + * Handler for the grid's beforestaterestore event (fires before the state of the + * grid is restored). + * @param {Object} grid The grid object + * @param {Object} state The hash of state values returned from the StateProvider. + */ + applyState : function (grid, state) { + var key, filter; + this.applyingState = true; + this.clearFilters(); + if (state.filters) { + for (key in state.filters) { + filter = this.filters.get(key); + if (filter) { + filter.setValue(state.filters[key]); + filter.setActive(true); } } - this.focus(); } + this.deferredUpdate.cancel(); + if (this.local) { + this.reload(); + } + delete this.applyingState; }, - getFocusItems: function(){ - return this.items && - this.items.filterBy(function(o){ - return o.isFocusable; - }) || - null; - }, - - getEnterItem: function(){ - var ci = this.getFocusItems(), length = ci ? ci.getCount() : 0; - - if (length === 1) { - return ci.first().getEnterItem && ci.first().getEnterItem() || ci.first(); - } - else - if (length > 1) { - return ci.first(); + /** + * Saves the state of all active filters + * @param {Object} grid + * @param {Object} state + * @return {Boolean} + */ + saveState : function (grid, state) { + var filters = {}; + this.filters.each(function (filter) { + if (filter.active) { + filters[filter.dataIndex] = filter.getValue(); } + }); + return (state.filters = filters); }, - getNextFocus: function(current){ - var items = this.getFocusItems(), next = current, i = items.indexOf(current), length = items.getCount(); - - if (i === length - 1) { - next = items.first(); + /** + * @private + * Handler called when the grid is rendered + */ + onRender : function () { + this.grid.getView().on('refresh', this.onRefresh, this); + this.createMenu(); + }, + + /** + * @private + * Handler called by the grid 'beforedestroy' event + */ + destroy : function () { + this.removeAll(); + this.purgeListeners(); + + if(this.filterMenu){ + Ext.menu.MenuMgr.unregister(this.filterMenu); + this.filterMenu.destroy(); + this.filterMenu = this.menu.menu = null; } - else { - next = items.get(i + 1); + }, + + /** + * Remove all filters, permanently destroying them. + */ + removeAll : function () { + if(this.filters){ + Ext.destroy.apply(Ext, this.filters.items); + // remove all items from the collection + this.filters.clear(); } - return next; }, - - getPreviousFocus: function(current){ - var items = this.getFocusItems(), prev = current, i = items.indexOf(current), length = items.getCount(); - - if (i === 0) { - prev = items.last(); + + + /** + * Changes the data store bound to this view and refreshes it. + * @param {Store} store The store to bind to this view + */ + bindStore : function(store, initial){ + if(!initial && this.store){ + if (this.local) { + store.un('load', this.onLoad, this); + } else { + store.un('beforeload', this.onBeforeLoad, this); + } } - else { - prev = items.get(i - 1); + if(store){ + if (this.local) { + store.on('load', this.onLoad, this); + } else { + store.on('beforeload', this.onBeforeLoad, this); + } } - return prev; + this.store = store; }, - - getFocusable : function() { - return this.fi; - } -}); -Ext.override(Ext.Panel, { /** - * @cfg {Boolean} enableTabbing true to enable tabbing. Default is false. - */ - getFocusItems: function(){ - // items gets all the items inside the body - var items = Ext.Panel.superclass.getFocusItems.call(this), bodyFocus = null; - - if (!items) { - items = new Ext.util.MixedCollection(); - this.bodyFocus = this.bodyFocus || new Ext.a11y.FocusItem(this.body, this.enableTabbing); - items.add('body', this.bodyFocus); - } - // but panels can also have tbar, bbar, fbar - if (this.tbar && this.topToolbar) { - items.insert(0, this.topToolbar); + * @private + * Handler called when the grid reconfigure event fires + */ + onReconfigure : function () { + this.bindStore(this.grid.getStore()); + this.store.clearFilter(); + this.removeAll(); + this.addFilters(this.grid.getColumnModel()); + this.updateColumnHeadings(); + }, + + createMenu : function () { + var view = this.grid.getView(), + hmenu = view.hmenu; + + if (this.showMenu && hmenu) { + + this.sep = hmenu.addSeparator(); + this.filterMenu = new Ext.menu.Menu({ + id: this.grid.id + '-filters-menu' + }); + this.menu = hmenu.add({ + checked: false, + itemId: 'filters', + text: this.menuFilterText, + menu: this.filterMenu + }); + + this.menu.on({ + scope: this, + checkchange: this.onCheckChange, + beforecheckchange: this.onBeforeCheck + }); + hmenu.on('beforeshow', this.onMenu, this); } - if (this.bbar && this.bottomToolbar) { - items.add(this.bottomToolbar); + this.updateColumnHeadings(); + }, + + /** + * @private + * Get the filter menu from the filters MixedCollection based on the clicked header + */ + getMenuFilter : function () { + var view = this.grid.getView(); + if (!view || view.hdCtxIndex === undefined) { + return null; } - if (this.fbar) { - items.add(this.fbar); + return this.filters.get( + view.cm.config[view.hdCtxIndex].dataIndex + ); + }, + + /** + * @private + * Handler called by the grid's hmenu beforeshow event + */ + onMenu : function (filterMenu) { + var filter = this.getMenuFilter(); + + if (filter) { +/* +TODO: lazy rendering + if (!filter.menu) { + filter.menu = filter.createMenu(); + } +*/ + this.menu.menu = filter.menu; + this.menu.setChecked(filter.active, false); + // disable the menu if filter.disabled explicitly set to true + this.menu.setDisabled(filter.disabled === true); } - return items; - } -}); - -Ext.override(Ext.TabPanel, { - // private - initFocus: function(){ - Ext.TabPanel.superclass.initFocus.call(this); - this.mon(this.fi, { - left: this.onLeft, - right: this.onRight, - scope: this - }); + this.menu.setVisible(filter !== undefined); + this.sep.setVisible(filter !== undefined); }, - onLeft: function(e){ - if (!this.activeTab) { - return; - } - e.stopEvent(); - var prev = this.items.itemAt(this.items.indexOf(this.activeTab) - 1); - if (prev) { - this.setActiveTab(prev); - } - return false; + /** @private */ + onCheckChange : function (item, value) { + this.getMenuFilter().setActive(value); }, - onRight: function(e){ - if (!this.activeTab) { + /** @private */ + onBeforeCheck : function (check, value) { + return !value || this.getMenuFilter().isActivatable(); + }, + + /** + * @private + * Handler for all events on filters. + * @param {String} event Event name + * @param {Object} filter Standard signature of the event before the event is fired + */ + onStateChange : function (event, filter) { + if (event === 'serialize') { return; } - e.stopEvent(); - var next = this.items.itemAt(this.items.indexOf(this.activeTab) + 1); - if (next) { - this.setActiveTab(next); - } - return false; - } -}); - -Ext.override(Ext.tree.TreeNodeUI, { - // private - focus: function(){ - this.node.getOwnerTree().bodyFocus.focus(); - } -}); - -Ext.override(Ext.tree.TreePanel, { - // private - afterRender : function(){ - Ext.tree.TreePanel.superclass.afterRender.call(this); - this.root.render(); - if(!this.rootVisible){ - this.root.renderChildren(); - } - this.bodyFocus = new Ext.a11y.FocusItem(this.body.down('.x-tree-root-ct')); - this.bodyFocus.fi.setFrameEl(this.body); - } -}); -Ext.override(Ext.grid.GridPanel, { - initFocus: function(){ - Ext.grid.GridPanel.superclass.initFocus.call(this); - this.bodyFocus = new Ext.a11y.FocusItem(this.view.focusEl); - this.bodyFocus.fi.setFrameEl(this.body); - } -}); - -Ext.override(Ext.Button, { - isFocusable: true, - noFocus: false, - - initFocus: function(){ - Ext.Button.superclass.initFocus.call(this); - this.fi = this.fi || new Ext.a11y.Focusable(this.btnEl, null, null, this.el); - this.fi.setComponent(this); - - this.mon(this.fi, { - focus: this.onFocus, - blur: this.onBlur, - scope: this - }); - - if (this.menu) { - this.mon(this.fi, 'down', this.showMenu, this); - this.on('menuhide', this.focus, this); + if (filter == this.getMenuFilter()) { + this.menu.setChecked(filter.active, false); } - - if (this.hidden) { - this.isFocusable = false; + + if ((this.autoReload || this.local) && !this.applyingState) { + this.deferredUpdate.delay(this.updateBuffer); } - - this.on('show', function(){ - this.isFocusable = true; - }, this); - this.on('hide', function(){ - this.isFocusable = false; - }, this); - }, - - focus: function(){ - this.fi.focus(); + this.updateColumnHeadings(); + + if (!this.applyingState) { + this.grid.saveState(); + } + this.grid.fireEvent('filterupdate', this, filter); }, - blur: function(){ - this.fi.blur(); + /** + * @private + * Handler for store's beforeload event when configured for remote filtering + * @param {Object} store + * @param {Object} options + */ + onBeforeLoad : function (store, options) { + options.params = options.params || {}; + this.cleanParams(options.params); + var params = this.buildQuery(this.getFilterData()); + Ext.apply(options.params, params); }, - onFocus: function(){ - if (!this.disabled) { - this.el.addClass("x-btn-focus"); - } + /** + * @private + * Handler for store's load event when configured for local filtering + * @param {Object} store + * @param {Object} options + */ + onLoad : function (store, options) { + store.filterBy(this.getRecordFilter()); }, - - onBlur: function(){ - this.el.removeClass("x-btn-focus"); - } -}); -Ext.override(Ext.Toolbar, { - initFocus: function(){ - Ext.Toolbar.superclass.initFocus.call(this); - this.mon(this.fi, { - left: this.onLeft, - right: this.onRight, - scope: this - }); - - this.on('focus', this.onButtonFocus, this, { - stopEvent: true - }); + /** + * @private + * Handler called when the grid's view is refreshed + */ + onRefresh : function () { + this.updateColumnHeadings(); }, - - addItem: function(item){ - Ext.Toolbar.superclass.add.apply(this, arguments); - if (item.rendered && item.fi !== undefined) { - item.fi.setRelayTo(this.el); - this.relayEvents(item.fi, ['focus']); - } - else { - item.on('render', function(){ - if (item.fi !== undefined) { - item.fi.setRelayTo(this.el); - this.relayEvents(item.fi, ['focus']); + + /** + * Update the styles for the header row based on the active filters + */ + updateColumnHeadings : function () { + var view = this.grid.getView(), + hds, i, len, filter; + if (view.mainHd) { + hds = view.mainHd.select('td').removeClass(this.filterCls); + for (i = 0, len = view.cm.config.length; i < len; i++) { + filter = this.getFilter(view.cm.config[i].dataIndex); + if (filter && filter.active) { + hds.item(i).addClass(this.filterCls); } - }, this, { - single: true - }); + } } - return item; }, - onFocus: function(){ - var items = this.getFocusItems(); - if (items && items.getCount() > 0) { - if (this.lastFocus && items.indexOf(this.lastFocus) !== -1) { - this.lastFocus.focus(); - } - else { - items.first().focus(); + /** @private */ + reload : function () { + if (this.local) { + this.grid.store.clearFilter(true); + this.grid.store.filterBy(this.getRecordFilter()); + } else { + var start, + store = this.grid.store; + this.deferredUpdate.cancel(); + if (this.toolbar) { + start = store.paramNames.start; + if (store.lastOptions && store.lastOptions.params && store.lastOptions.params[start]) { + store.lastOptions.params[start] = 0; + } } + store.reload(); } }, - onButtonFocus: function(e, t, tf){ - this.lastFocus = tf.component || null; - }, - - onLeft: function(e, t, tf){ - e.stopEvent(); - this.getPreviousFocus(tf.component).focus(); + /** + * Method factory that generates a record validator for the filters active at the time + * of invokation. + * @private + */ + getRecordFilter : function () { + var f = [], len, i; + this.filters.each(function (filter) { + if (filter.active) { + f.push(filter); + } + }); + + len = f.length; + return function (record) { + for (i = 0; i < len; i++) { + if (!f[i].validateRecord(record)) { + return false; + } + } + return true; + }; }, - onRight: function(e, t, tf){ - e.stopEvent(); - this.getNextFocus(tf.component).focus(); + /** + * Adds a filter to the collection and observes it for state change. + * @param {Object/Ext.ux.grid.filter.Filter} config A filter configuration or a filter object. + * @return {Ext.ux.grid.filter.Filter} The existing or newly created filter object. + */ + addFilter : function (config) { + var Cls = this.getFilterClass(config.type), + filter = config.menu ? config : (new Cls(config)); + this.filters.add(filter); + + Ext.util.Observable.capture(filter, this.onStateChange, this); + return filter; }, - - getEnterItem: Ext.emptyFn, - onTab: Ext.emptyFn, - onEsc: Ext.emptyFn -}); -Ext.override(Ext.menu.BaseItem, { - initFocus: function(){ - this.fi = new Ext.a11y.Focusable(this, this.parentMenu && this.parentMenu.el || null, true); - } -}); + /** + * Adds filters to the collection. + * @param {Array/Ext.grid.ColumnModel} filters Either an Array of + * filter configuration objects or an Ext.grid.ColumnModel. The columns + * of a passed Ext.grid.ColumnModel will be examined for a filter + * property and, if present, will be used as the filter configuration object. + */ + addFilters : function (filters) { + if (filters) { + var i, len, filter, cm = false, dI; + if (filters instanceof Ext.grid.ColumnModel) { + filters = filters.config; + cm = true; + } + for (i = 0, len = filters.length; i < len; i++) { + filter = false; + if (cm) { + dI = filters[i].dataIndex; + filter = filters[i].filter || filters[i].filterable; + if (filter){ + filter = (filter === true) ? {} : filter; + Ext.apply(filter, {dataIndex:dI}); + // filter type is specified in order of preference: + // filter type specified in config + // type specified in store's field's type config + filter.type = filter.type || this.store.fields.get(dI).type; + } + } else { + filter = filters[i]; + } + // if filter config found add filter for the column + if (filter) { + this.addFilter(filter); + } + } + } + }, + + /** + * Returns a filter for the given dataIndex, if one exists. + * @param {String} dataIndex The dataIndex of the desired filter object. + * @return {Ext.ux.grid.filter.Filter} + */ + getFilter : function (dataIndex) { + return this.filters.get(dataIndex); + }, -Ext.override(Ext.menu.Menu, { - initFocus: function(){ - this.fi = new Ext.a11y.Focusable(this); - this.focusEl = this.fi; + /** + * Turns all filters off. This does not clear the configuration information + * (see {@link #removeAll}). + */ + clearFilters : function () { + this.filters.each(function (filter) { + filter.setActive(false); + }); + }, + + /** + * Returns an Array of the currently active filters. + * @return {Array} filters Array of the currently active filters. + */ + getFilterData : function () { + var filters = [], i, len; + + this.filters.each(function (f) { + if (f.active) { + var d = [].concat(f.serialize()); + for (i = 0, len = d.length; i < len; i++) { + filters.push({ + field: f.dataIndex, + data: d[i] + }); + } + } + }); + return filters; + }, + + /** + * Function to take the active filters data and build it into a query. + * The format of the query depends on the {@link #encode} + * configuration: + *
+ * Override this method to customize the format of the filter query for remote requests. + * @param {Array} filters A collection of objects representing active filters and their configuration. + * Each element will take the form of {field: dataIndex, data: filterConf}. dataIndex is not assured + * to be unique as any one filter may be a composite of more basic filters for the same dataIndex. + * @return {Object} Query keys and values + */ + buildQuery : function (filters) { + var p = {}, i, f, root, dataPrefix, key, tmp, + len = filters.length; + + if (!this.encode){ + for (i = 0; i < len; i++) { + f = filters[i]; + root = [this.paramPrefix, '[', i, ']'].join(''); + p[root + '[field]'] = f.field; + + dataPrefix = root + '[data]'; + for (key in f.data) { + p[[dataPrefix, '[', key, ']'].join('')] = f.data[key]; + } + } + } else { + tmp = []; + for (i = 0; i < len; i++) { + f = filters[i]; + tmp.push(Ext.apply( + {}, + {field: f.field}, + f.data + )); + } + // only build if there is active filter + if (tmp.length > 0){ + p[this.paramPrefix] = Ext.util.JSON.encode(tmp); + } + } + return p; + }, + + /** + * Removes filter related query parameters from the provided object. + * @param {Object} p Query parameters that may contain filter related fields. + */ + cleanParams : function (p) { + // if encoding just delete the property + if (this.encode) { + delete p[this.paramPrefix]; + // otherwise scrub the object of filter data + } else { + var regex, key; + regex = new RegExp('^' + this.paramPrefix + '\[[0-9]+\]'); + for (key in p) { + if (regex.test(key)) { + delete p[key]; + } + } + } + }, + + /** + * Function for locating filter classes, overwrite this with your favorite + * loader to provide dynamic filter loading. + * @param {String} type The type of filter to load ('Filter' is automatically + * appended to the passed type; eg, 'string' becomes 'StringFilter'). + * @return {Class} The Ext.ux.grid.filter.Class + */ + getFilterClass : function (type) { + // map the supported Ext.data.Field type values into a supported filter + switch(type) { + case 'auto': + type = 'string'; + break; + case 'int': + case 'float': + type = 'numeric'; + break; + } + return Ext.ux.grid.filter[type.substr(0, 1).toUpperCase() + type.substr(1) + 'Filter']; } }); -Ext.a11y.WindowMgr = new Ext.WindowGroup(); +// register ptype +Ext.preg('gridfilters', Ext.ux.grid.GridFilters); +Ext.namespace('Ext.ux.grid.filter'); + +/** + * @class Ext.ux.grid.filter.Filter + * @extends Ext.util.Observable + * Abstract base class for filter implementations. + */ +Ext.ux.grid.filter.Filter = Ext.extend(Ext.util.Observable, { + /** + * @cfg {Boolean} active + * Indicates the initial status of the filter (defaults to false). + */ + active : false, + /** + * True if this filter is active. Use setActive() to alter after configuration. + * @type Boolean + * @property active + */ + /** + * @cfg {String} dataIndex + * The {@link Ext.data.Store} dataIndex of the field this filter represents. + * The dataIndex does not actually have to exist in the store. + */ + dataIndex : null, + /** + * The filter configuration menu that will be installed into the filter submenu of a column menu. + * @type Ext.menu.Menu + * @property + */ + menu : null, + /** + * @cfg {Number} updateBuffer + * Number of milliseconds to wait after user interaction to fire an update. Only supported + * by filters: 'list', 'numeric', and 'string'. Defaults to 500. + */ + updateBuffer : 500, + + constructor : function (config) { + Ext.apply(this, config); + + this.addEvents( + /** + * @event activate + * Fires when an inactive filter becomes active + * @param {Ext.ux.grid.filter.Filter} this + */ + 'activate', + /** + * @event deactivate + * Fires when an active filter becomes inactive + * @param {Ext.ux.grid.filter.Filter} this + */ + 'deactivate', + /** + * @event serialize + * Fires after the serialization process. Use this to attach additional parameters to serialization + * data before it is encoded and sent to the server. + * @param {Array/Object} data A map or collection of maps representing the current filter configuration. + * @param {Ext.ux.grid.filter.Filter} filter The filter being serialized. + */ + 'serialize', + /** + * @event update + * Fires when a filter configuration has changed + * @param {Ext.ux.grid.filter.Filter} this The filter object. + */ + 'update' + ); + Ext.ux.grid.filter.Filter.superclass.constructor.call(this); -Ext.apply(Ext.WindowMgr, { - bringToFront: function(win){ - Ext.a11y.WindowMgr.bringToFront.call(this, win); - if (win.modal) { - win.enter(); + this.menu = new Ext.menu.Menu(); + this.init(config); + if(config && config.value){ + this.setValue(config.value); + this.setActive(config.active !== false, true); + delete config.value; } - else { - win.focus(); + }, + + /** + * Destroys this filter by purging any event listeners, and removing any menus. + */ + destroy : function(){ + if (this.menu){ + this.menu.destroy(); } - } -}); + this.purgeListeners(); + }, -Ext.override(Ext.Window, { - initFocus: function(){ - Ext.Window.superclass.initFocus.call(this); - this.on('beforehide', function(){ - Ext.a11y.RelayFrame.unframe(); - Ext.a11y.FocusFrame.unframe(); - }); - } -}); + /** + * Template method to be implemented by all subclasses that is to + * initialize the filter and install required menu items. + * Defaults to Ext.emptyFn. + */ + init : Ext.emptyFn, + + /** + * Template method to be implemented by all subclasses that is to + * get and return the value of the filter. + * Defaults to Ext.emptyFn. + * @return {Object} The 'serialized' form of this filter + * @methodOf Ext.ux.grid.filter.Filter + */ + getValue : Ext.emptyFn, + + /** + * Template method to be implemented by all subclasses that is to + * set the value of the filter and fire the 'update' event. + * Defaults to Ext.emptyFn. + * @param {Object} data The value to set the filter + * @methodOf Ext.ux.grid.filter.Filter + */ + setValue : Ext.emptyFn, + + /** + * Template method to be implemented by all subclasses that is to + * return true if the filter has enough configuration information to be activated. + * Defaults to return true. + * @return {Boolean} + */ + isActivatable : function(){ + return true; + }, + + /** + * Template method to be implemented by all subclasses that is to + * get and return serialized filter data for transmission to the server. + * Defaults to Ext.emptyFn. + */ + getSerialArgs : Ext.emptyFn, + + /** + * Template method to be implemented by all subclasses that is to + * validates the provided Ext.data.Record against the filters configuration. + * Defaults to return true. + * @param {Ext.data.Record} record The record to validate + * @return {Boolean} true if the record is valid within the bounds + * of the filter, false otherwise. + */ + validateRecord : function(){ + return true; + }, + + /** + * Returns the serialized filter data for transmission to the server + * and fires the 'serialize' event. + * @return {Object/Array} An object or collection of objects containing + * key value pairs representing the current configuration of the filter. + * @methodOf Ext.ux.grid.filter.Filter + */ + serialize : function(){ + var args = this.getSerialArgs(); + this.fireEvent('serialize', args, this); + return args; + }, -Ext.override(Ext.form.Field, { - isFocusable: true, - noFocus: false, + /** @private */ + fireUpdate : function(){ + if (this.active) { + this.fireEvent('update', this); + } + this.setActive(this.isActivatable()); + }, - initFocus: function(){ - this.fi = this.fi || new Ext.a11y.Focusable(this, null, true); - - Ext.form.Field.superclass.initFocus.call(this); - - if (this.hidden) { - this.isFocusable = false; + /** + * Sets the status of the filter and fires the appropriate events. + * @param {Boolean} active The new filter state. + * @param {Boolean} suppressEvent True to prevent events from being fired. + * @methodOf Ext.ux.grid.filter.Filter + */ + setActive : function(active, suppressEvent){ + if(this.active != active){ + this.active = active; + if (suppressEvent !== true) { + this.fireEvent(active ? 'activate' : 'deactivate', this); + } } - - this.on('show', function(){ - this.isFocusable = true; - }, this); - this.on('hide', function(){ - this.isFocusable = false; - }, this); - } + } +});/** + * @class Ext.ux.grid.filter.BooleanFilter + * @extends Ext.ux.grid.filter.Filter + * Boolean filters use unique radio group IDs (so you can have more than one!) + *

Example Usage:

+ *
    
+var filters = new Ext.ux.grid.GridFilters({
+    ...
+    filters: [{
+        // required configs
+        type: 'boolean',
+        dataIndex: 'visible'
+
+        // optional configs
+        defaultValue: null, // leave unselected (false selected by default)
+        yesText: 'Yes',     // default
+        noText: 'No'        // default
+    }]
 });
+ * 
+ */ +Ext.ux.grid.filter.BooleanFilter = Ext.extend(Ext.ux.grid.filter.Filter, { + /** + * @cfg {Boolean} defaultValue + * Set this to null if you do not want either option to be checked by default. Defaults to false. + */ + defaultValue : false, + /** + * @cfg {String} yesText + * Defaults to 'Yes'. + */ + yesText : 'Yes', + /** + * @cfg {String} noText + * Defaults to 'No'. + */ + noText : 'No', -Ext.override(Ext.FormPanel, { - initFocus: function(){ - Ext.FormPanel.superclass.initFocus.call(this); - this.on('focus', this.onFieldFocus, this, { - stopEvent: true - }); + /** + * @private + * Template method that is to initialize the filter and install required menu items. + */ + init : function (config) { + var gId = Ext.id(); + this.options = [ + new Ext.menu.CheckItem({text: this.yesText, group: gId, checked: this.defaultValue === true}), + new Ext.menu.CheckItem({text: this.noText, group: gId, checked: this.defaultValue === false})]; + + this.menu.add(this.options[0], this.options[1]); + + for(var i=0; iExample Usage:

+ *
    
+var filters = new Ext.ux.grid.GridFilters({
+    ...
+    filters: [{
+        // required configs
+        type: 'date',
+        dataIndex: 'dateAdded',
+        
+        // optional configs
+        dateFormat: 'm/d/Y',  // default
+        beforeText: 'Before', // default
+        afterText: 'After',   // default
+        onText: 'On',         // default
+        pickerOpts: {
+            // any DateMenu configs
+        },
+
+        active: true // default is false
+    }]
+});
+ * 
+ */ +Ext.ux.grid.filter.DateFilter = Ext.extend(Ext.ux.grid.filter.Filter, { + /** + * @cfg {String} afterText + * Defaults to 'After'. + */ + afterText : 'After', + /** + * @cfg {String} beforeText + * Defaults to 'Before'. + */ + beforeText : 'Before', + /** + * @cfg {Object} compareMap + * Map for assigning the comparison values used in serialization. + */ + compareMap : { + before: 'lt', + after: 'gt', + on: 'eq' }, - - // private - createForm: function(){ - delete this.initialConfig.listeners; - var form = new Ext.form.BasicForm(null, this.initialConfig); - form.afterMethod('add', this.formItemAdd, this); - return form; + /** + * @cfg {String} dateFormat + * The date format to return when using getValue. + * Defaults to 'm/d/Y'. + */ + dateFormat : 'm/d/Y', + + /** + * @cfg {Date} maxDate + * Allowable date as passed to the Ext.DatePicker + * Defaults to undefined. + */ + /** + * @cfg {Date} minDate + * Allowable date as passed to the Ext.DatePicker + * Defaults to undefined. + */ + /** + * @cfg {Array} menuItems + * The items to be shown in this menu + * Defaults to:
+     * menuItems : ['before', 'after', '-', 'on'],
+     * 
+ */ + menuItems : ['before', 'after', '-', 'on'], + + /** + * @cfg {Object} menuItemCfgs + * Default configuration options for each menu item + */ + menuItemCfgs : { + selectOnFocus: true, + width: 125 }, + + /** + * @cfg {String} onText + * Defaults to 'On'. + */ + onText : 'On', - formItemAdd: function(item){ - item.on('render', function(field){ - field.fi.setRelayTo(this.el); - this.relayEvents(field.fi, ['focus']); - }, this, { - single: true + /** + * @cfg {Object} pickerOpts + * Configuration options for the date picker associated with each field. + */ + pickerOpts : {}, + + /** + * @private + * Template method that is to initialize the filter and install required menu items. + */ + init : function (config) { + var menuCfg, i, len, item, cfg, Cls; + + menuCfg = Ext.apply(this.pickerOpts, { + minDate: this.minDate, + maxDate: this.maxDate, + format: this.dateFormat, + listeners: { + scope: this, + select: this.onMenuSelect + } }); + + this.fields = {}; + for (i = 0, len = this.menuItems.length; i < len; i++) { + item = this.menuItems[i]; + if (item !== '-') { + cfg = { + itemId: 'range-' + item, + text: this[item + 'Text'], + menu: new Ext.menu.DateMenu( + Ext.apply(menuCfg, { + itemId: item + }) + ), + listeners: { + scope: this, + checkchange: this.onCheckChange + } + }; + Cls = Ext.menu.CheckItem; + item = this.fields[item] = new Cls(cfg); + } + //this.add(item); + this.menu.add(item); + } }, - - onFocus: function(){ - var items = this.getFocusItems(); - if (items && items.getCount() > 0) { - if (this.lastFocus && items.indexOf(this.lastFocus) !== -1) { - this.lastFocus.focus(); + + onCheckChange : function () { + this.setActive(this.isActivatable()); + this.fireEvent('update', this); + }, + + /** + * @private + * Handler method called when there is a keyup event on an input + * item of this menu. + */ + onInputKeyUp : function (field, e) { + var k = e.getKey(); + if (k == e.RETURN && field.isValid()) { + e.stopEvent(); + this.menu.hide(true); + return; + } + }, + + /** + * Handler for when the menu for a field fires the 'select' event + * @param {Object} date + * @param {Object} menuItem + * @param {Object} value + * @param {Object} picker + */ + onMenuSelect : function (menuItem, value, picker) { + var fields = this.fields, + field = this.fields[menuItem.itemId]; + + field.setChecked(true); + + if (field == fields.on) { + fields.before.setChecked(false, true); + fields.after.setChecked(false, true); + } else { + fields.on.setChecked(false, true); + if (field == fields.after && fields.before.menu.picker.value < value) { + fields.before.setChecked(false, true); + } else if (field == fields.before && fields.after.menu.picker.value > value) { + fields.after.setChecked(false, true); } - else { - items.first().focus(); + } + this.fireEvent('update', this); + }, + + /** + * @private + * Template method that is to get and return the value of the filter. + * @return {String} The value of this filter + */ + getValue : function () { + var key, result = {}; + for (key in this.fields) { + if (this.fields[key].checked) { + result[key] = this.fields[key].menu.picker.getValue(); } } + return result; }, - - onFieldFocus: function(e, t, tf){ - this.lastFocus = tf.component || null; + + /** + * @private + * Template method that is to set the value of the filter. + * @param {Object} value The value to set the filter + * @param {Boolean} preserve true to preserve the checked status + * of the other fields. Defaults to false, unchecking the + * other fields + */ + setValue : function (value, preserve) { + var key; + for (key in this.fields) { + if(value[key]){ + this.fields[key].menu.picker.setValue(value[key]); + this.fields[key].setChecked(true); + } else if (!preserve) { + this.fields[key].setChecked(false); + } + } + this.fireEvent('update', this); + }, + + /** + * @private + * Template method that is to return true if the filter + * has enough configuration information to be activated. + * @return {Boolean} + */ + isActivatable : function () { + var key; + for (key in this.fields) { + if (this.fields[key].checked) { + return true; + } + } + return false; + }, + + /** + * @private + * Template method that is to get and return serialized filter data for + * transmission to the server. + * @return {Object/Array} An object or collection of objects containing + * key value pairs representing the current configuration of the filter. + */ + getSerialArgs : function () { + var args = []; + for (var key in this.fields) { + if(this.fields[key].checked){ + args.push({ + type: 'date', + comparison: this.compareMap[key], + value: this.getFieldValue(key).format(this.dateFormat) + }); + } + } + return args; + }, + + /** + * Get and return the date menu picker value + * @param {String} item The field identifier ('before', 'after', 'on') + * @return {Date} Gets the current selected value of the date field + */ + getFieldValue : function(item){ + return this.fields[item].menu.picker.getValue(); }, - onTab: function(e, t, tf){ - if (tf.relayTo.component === this) { - var item = e.shiftKey ? this.getPreviousFocus(tf.component) : this.getNextFocus(tf.component); + /** + * Gets the menu picker associated with the passed field + * @param {String} item The field identifier ('before', 'after', 'on') + * @return {Object} The menu picker + */ + getPicker : function(item){ + return this.fields[item].menu.picker; + }, + + /** + * Template method that is to validate the provided Ext.data.Record + * against the filters configuration. + * @param {Ext.data.Record} record The record to validate + * @return {Boolean} true if the record is valid within the bounds + * of the filter, false otherwise. + */ + validateRecord : function (record) { + var key, + pickerValue, + val = record.get(this.dataIndex); - if (item) { - ev.stopEvent(); - item.focus(); - return; + if(!Ext.isDate(val)){ + return false; + } + val = val.clearTime(true).getTime(); + + for (key in this.fields) { + if (this.fields[key].checked) { + pickerValue = this.getFieldValue(key).clearTime(true).getTime(); + if (key == 'before' && pickerValue <= val) { + return false; + } + if (key == 'after' && pickerValue >= val) { + return false; + } + if (key == 'on' && pickerValue != val) { + return false; + } } } - Ext.FormPanel.superclass.onTab.apply(this, arguments); + return true; + } +});/** + * @class Ext.ux.grid.filter.ListFilter + * @extends Ext.ux.grid.filter.Filter + *

List filters are able to be preloaded/backed by an Ext.data.Store to load + * their options the first time they are shown. ListFilter utilizes the + * {@link Ext.ux.menu.ListMenu} component.

+ *

Although not shown here, this class accepts all configuration options + * for {@link Ext.ux.menu.ListMenu}.

+ * + *

Example Usage:

+ *
    
+var filters = new Ext.ux.grid.GridFilters({
+    ...
+    filters: [{
+        type: 'list',
+        dataIndex: 'size',
+        phpMode: true,
+        // options will be used as data to implicitly creates an ArrayStore
+        options: ['extra small', 'small', 'medium', 'large', 'extra large']
+    }]
+});
+ * 
+ * + */ +Ext.ux.grid.filter.ListFilter = Ext.extend(Ext.ux.grid.filter.Filter, { + + /** + * @cfg {Array} options + *

data to be used to implicitly create a data store + * to back this list when the data source is local. If the + * data for the list is remote, use the {@link #store} + * config instead.

+ *

Each item within the provided array may be in one of the + * following formats:

+ *
    + *
  • Array : + *
    
    +options: [
    +    [11, 'extra small'], 
    +    [18, 'small'],
    +    [22, 'medium'],
    +    [35, 'large'],
    +    [44, 'extra large']
    +]
    +     * 
    + *
  • + *
  • Object : + *
    
    +labelField: 'name', // override default of 'text'
    +options: [
    +    {id: 11, name:'extra small'}, 
    +    {id: 18, name:'small'}, 
    +    {id: 22, name:'medium'}, 
    +    {id: 35, name:'large'}, 
    +    {id: 44, name:'extra large'} 
    +]
    +     * 
    + *
  • + *
  • String : + *
    
    +     * options: ['extra small', 'small', 'medium', 'large', 'extra large']
    +     * 
    + *
  • + */ + /** + * @cfg {Boolean} phpMode + *

    Adjust the format of this filter. Defaults to false.

    + *

    When GridFilters @cfg encode = false (default):

    + *
    
    +// phpMode == false (default):
    +filter[0][data][type] list
    +filter[0][data][value] value1
    +filter[0][data][value] value2
    +filter[0][field] prod 
    +
    +// phpMode == true:
    +filter[0][data][type] list
    +filter[0][data][value] value1, value2
    +filter[0][field] prod 
    +     * 
    + * When GridFilters @cfg encode = true: + *
    
    +// phpMode == false (default):
    +filter : [{"type":"list","value":["small","medium"],"field":"size"}]
    +
    +// phpMode == true:
    +filter : [{"type":"list","value":"small,medium","field":"size"}]
    +     * 
    + */ + phpMode : false, + /** + * @cfg {Ext.data.Store} store + * The {@link Ext.data.Store} this list should use as its data source + * when the data source is remote. If the data for the list + * is local, use the {@link #options} config instead. + */ + + /** + * @private + * Template method that is to initialize the filter and install required menu items. + * @param {Object} config + */ + init : function (config) { + this.dt = new Ext.util.DelayedTask(this.fireUpdate, this); + + // if a menu already existed, do clean up first + if (this.menu){ + this.menu.destroy(); + } + this.menu = new Ext.ux.menu.ListMenu(config); + this.menu.on('checkchange', this.onCheckChange, this); }, - getNextFocus: function(current){ - var items = this.getFocusItems(), i = items.indexOf(current), length = items.getCount(); - - return (i < length - 1) ? items.get(i + 1) : false; + /** + * @private + * Template method that is to get and return the value of the filter. + * @return {String} The value of this filter + */ + getValue : function () { + return this.menu.getSelected(); + }, + /** + * @private + * Template method that is to set the value of the filter. + * @param {Object} value The value to set the filter + */ + setValue : function (value) { + this.menu.setSelected(value); + this.fireEvent('update', this); + }, + + /** + * @private + * Template method that is to return true if the filter + * has enough configuration information to be activated. + * @return {Boolean} + */ + isActivatable : function () { + return this.getValue().length > 0; }, - getPreviousFocus: function(current){ - var items = this.getFocusItems(), i = items.indexOf(current), length = items.getCount(); - - return (i > 0) ? items.get(i - 1) : false; + /** + * @private + * Template method that is to get and return serialized filter data for + * transmission to the server. + * @return {Object/Array} An object or collection of objects containing + * key value pairs representing the current configuration of the filter. + */ + getSerialArgs : function () { + var args = {type: 'list', value: this.phpMode ? this.getValue().join(',') : this.getValue()}; + return args; + }, + + /** @private */ + onCheckChange : function(){ + this.dt.delay(this.updateBuffer); + }, + + + /** + * Template method that is to validate the provided Ext.data.Record + * against the filters configuration. + * @param {Ext.data.Record} record The record to validate + * @return {Boolean} true if the record is valid within the bounds + * of the filter, false otherwise. + */ + validateRecord : function (record) { + return this.getValue().indexOf(record.get(this.dataIndex)) > -1; } +});/** + * @class Ext.ux.grid.filter.NumericFilter + * @extends Ext.ux.grid.filter.Filter + * Filters using an Ext.ux.menu.RangeMenu. + *

    Example Usage:

    + *
        
    +var filters = new Ext.ux.grid.GridFilters({
    +    ...
    +    filters: [{
    +        type: 'numeric',
    +        dataIndex: 'price'
    +    }]
     });
    + * 
    + */ +Ext.ux.grid.filter.NumericFilter = Ext.extend(Ext.ux.grid.filter.Filter, { -Ext.override(Ext.Viewport, { - initFocus: function(){ - Ext.Viewport.superclass.initFocus.apply(this); - this.mon(Ext.get(document), 'focus', this.focus, this); - this.mon(Ext.get(document), 'blur', this.blur, this); - this.fi.setNoFrame(true); + /** + * @cfg {Object} fieldCls + * The Class to use to construct each field item within this menu + * Defaults to:
    +     * fieldCls : Ext.form.NumberField
    +     * 
    + */ + fieldCls : Ext.form.NumberField, + /** + * @cfg {Object} fieldCfg + * The default configuration options for any field item unless superseded + * by the {@link #fields} configuration. + * Defaults to:
    +     * fieldCfg : {}
    +     * 
    + * Example usage: + *
    
    +fieldCfg : {
    +    width: 150,
    +},
    +     * 
    + */ + /** + * @cfg {Object} fields + * The field items may be configured individually + * Defaults to undefined. + * Example usage: + *
    
    +fields : {
    +    gt: { // override fieldCfg options
    +        width: 200,
    +        fieldCls: Ext.ux.form.CustomNumberField // to override default {@link #fieldCls}
    +    }
    +},
    +     * 
    + */ + /** + * @cfg {Object} iconCls + * The iconCls to be applied to each comparator field item. + * Defaults to:
    +iconCls : {
    +    gt : 'ux-rangemenu-gt',
    +    lt : 'ux-rangemenu-lt',
    +    eq : 'ux-rangemenu-eq'
    +}
    +     * 
    + */ + iconCls : { + gt : 'ux-rangemenu-gt', + lt : 'ux-rangemenu-lt', + eq : 'ux-rangemenu-eq' + }, + + /** + * @cfg {Object} menuItemCfgs + * Default configuration options for each menu item + * Defaults to:
    +menuItemCfgs : {
    +    emptyText: 'Enter Filter Text...',
    +    selectOnFocus: true,
    +    width: 125
    +}
    +     * 
    + */ + menuItemCfgs : { + emptyText: 'Enter Filter Text...', + selectOnFocus: true, + width: 125 + }, + + /** + * @cfg {Array} menuItems + * The items to be shown in this menu. Items are added to the menu + * according to their position within this array. Defaults to:
    +     * menuItems : ['lt','gt','-','eq']
    +     * 
    + */ + menuItems : ['lt', 'gt', '-', 'eq'], + + /** + * @private + * Template method that is to initialize the filter and install required menu items. + */ + init : function (config) { + // if a menu already existed, do clean up first + if (this.menu){ + this.menu.destroy(); + } + this.menu = new Ext.ux.menu.RangeMenu(Ext.apply(config, { + // pass along filter configs to the menu + fieldCfg : this.fieldCfg || {}, + fieldCls : this.fieldCls, + fields : this.fields || {}, + iconCls: this.iconCls, + menuItemCfgs: this.menuItemCfgs, + menuItems: this.menuItems, + updateBuffer: this.updateBuffer + })); + // relay the event fired by the menu + this.menu.on('update', this.fireUpdate, this); }, - onTab: function(e, t, tf, f){ - e.stopEvent(); - - if (tf === f) { - items = this.getFocusItems(); - if (items && items.getCount() > 0) { - items.first().focus(); + /** + * @private + * Template method that is to get and return the value of the filter. + * @return {String} The value of this filter + */ + getValue : function () { + return this.menu.getValue(); + }, + + /** + * @private + * Template method that is to set the value of the filter. + * @param {Object} value The value to set the filter + */ + setValue : function (value) { + this.menu.setValue(value); + }, + + /** + * @private + * Template method that is to return true if the filter + * has enough configuration information to be activated. + * @return {Boolean} + */ + isActivatable : function () { + var values = this.getValue(); + for (key in values) { + if (values[key] !== undefined) { + return true; } } - else { - var rf = tf.relayTo || tf; - var item = e.shiftKey ? this.getPreviousFocus(rf.component) : this.getNextFocus(rf.component); - item.focus(); - } - } -}); + return false; + }, -})();/** - * @class Ext.ux.GMapPanel - * @extends Ext.Panel - * @author Shea Frederick - */ -Ext.ux.GMapPanel = Ext.extend(Ext.Panel, { - initComponent : function(){ - - var defConfig = { - plain: true, - zoomLevel: 3, - yaw: 180, - pitch: 0, - zoom: 0, - gmapType: 'map', - border: false - }; - - Ext.applyIf(this,defConfig); - - Ext.ux.GMapPanel.superclass.initComponent.call(this); - + /** + * @private + * Template method that is to get and return serialized filter data for + * transmission to the server. + * @return {Object/Array} An object or collection of objects containing + * key value pairs representing the current configuration of the filter. + */ + getSerialArgs : function () { + var key, + args = [], + values = this.menu.getValue(); + for (key in values) { + args.push({ + type: 'numeric', + comparison: key, + value: values[key] + }); + } + return args; }, - afterRender : function(){ - - var wh = this.ownerCt.getSize(); - Ext.applyIf(this, wh); - - Ext.ux.GMapPanel.superclass.afterRender.call(this); - - if (this.gmapType === 'map'){ - this.gmap = new GMap2(this.body.dom); + + /** + * Template method that is to validate the provided Ext.data.Record + * against the filters configuration. + * @param {Ext.data.Record} record The record to validate + * @return {Boolean} true if the record is valid within the bounds + * of the filter, false otherwise. + */ + validateRecord : function (record) { + var val = record.get(this.dataIndex), + values = this.getValue(); + if (values.eq !== undefined && val != values.eq) { + return false; } - - if (this.gmapType === 'panorama'){ - this.gmap = new GStreetviewPanorama(this.body.dom); + if (values.lt !== undefined && val >= values.lt) { + return false; } - - if (typeof this.addControl == 'object' && this.gmapType === 'map') { - this.gmap.addControl(this.addControl); + if (values.gt !== undefined && val <= values.gt) { + return false; } + return true; + } +});/** + * @class Ext.ux.grid.filter.StringFilter + * @extends Ext.ux.grid.filter.Filter + * Filter by a configurable Ext.form.TextField + *

    Example Usage:

    + *
        
    +var filters = new Ext.ux.grid.GridFilters({
    +    ...
    +    filters: [{
    +        // required configs
    +        type: 'string',
    +        dataIndex: 'name',
             
    -        if (typeof this.setCenter === 'object') {
    -            if (typeof this.setCenter.geoCodeAddr === 'string'){
    -                this.geoCodeLookup(this.setCenter.geoCodeAddr);
    -            }else{
    -                if (this.gmapType === 'map'){
    -                    var point = new GLatLng(this.setCenter.lat,this.setCenter.lng);
    -                    this.gmap.setCenter(point, this.zoomLevel);    
    -                }
    -                if (typeof this.setCenter.marker === 'object' && typeof point === 'object'){
    -                    this.addMarker(point,this.setCenter.marker,this.setCenter.marker.clear);
    -                }
    -            }
    -            if (this.gmapType === 'panorama'){
    -                this.gmap.setLocationAndPOV(new GLatLng(this.setCenter.lat,this.setCenter.lng), {yaw: this.yaw, pitch: this.pitch, zoom: this.zoom});
    -            }
    -        }
    +        // optional configs
    +        value: 'foo',
    +        active: true, // default is false
    +        iconCls: 'ux-gridfilter-text-icon' // default
    +        // any Ext.form.TextField configs accepted
    +    }]
    +});
    + * 
    + */ +Ext.ux.grid.filter.StringFilter = Ext.extend(Ext.ux.grid.filter.Filter, { - GEvent.bind(this.gmap, 'load', this, function(){ - this.onMapReady(); + /** + * @cfg {String} iconCls + * The iconCls to be applied to the menu item. + * Defaults to 'ux-gridfilter-text-icon'. + */ + iconCls : 'ux-gridfilter-text-icon', + + emptyText: 'Enter Filter Text...', + selectOnFocus: true, + width: 125, + + /** + * @private + * Template method that is to initialize the filter and install required menu items. + */ + init : function (config) { + Ext.applyIf(config, { + enableKeyEvents: true, + iconCls: this.iconCls, + listeners: { + scope: this, + keyup: this.onInputKeyUp + } }); + this.inputItem = new Ext.form.TextField(config); + this.menu.add(this.inputItem); + this.updateTask = new Ext.util.DelayedTask(this.fireUpdate, this); + }, + + /** + * @private + * Template method that is to get and return the value of the filter. + * @return {String} The value of this filter + */ + getValue : function () { + return this.inputItem.getValue(); + }, + + /** + * @private + * Template method that is to set the value of the filter. + * @param {Object} value The value to set the filter + */ + setValue : function (value) { + this.inputItem.setValue(value); + this.fireEvent('update', this); + }, + + /** + * @private + * Template method that is to return true if the filter + * has enough configuration information to be activated. + * @return {Boolean} + */ + isActivatable : function () { + return this.inputItem.getValue().length > 0; }, - onMapReady : function(){ - this.addMarkers(this.markers); - this.addMapControls(); - this.addOptions(); + + /** + * @private + * Template method that is to get and return serialized filter data for + * transmission to the server. + * @return {Object/Array} An object or collection of objects containing + * key value pairs representing the current configuration of the filter. + */ + getSerialArgs : function () { + return {type: 'string', value: this.getValue()}; }, - onResize : function(w, h){ - if (typeof this.getMap() == 'object') { - this.gmap.checkResize(); + /** + * Template method that is to validate the provided Ext.data.Record + * against the filters configuration. + * @param {Ext.data.Record} record The record to validate + * @return {Boolean} true if the record is valid within the bounds + * of the filter, false otherwise. + */ + validateRecord : function (record) { + var val = record.get(this.dataIndex); + + if(typeof val != 'string') { + return (this.getValue().length === 0); } - - Ext.ux.GMapPanel.superclass.onResize.call(this, w, h); + return val.toLowerCase().indexOf(this.getValue().toLowerCase()) > -1; }, - setSize : function(width, height, animate){ - - if (typeof this.getMap() == 'object') { - this.gmap.checkResize(); + + /** + * @private + * Handler method called when there is a keyup event on this.inputItem + */ + onInputKeyUp : function (field, e) { + var k = e.getKey(); + if (k == e.RETURN && field.isValid()) { + e.stopEvent(); + this.menu.hide(true); + return; + } + // restart the timer + this.updateTask.delay(this.updateBuffer); + } +}); +Ext.namespace('Ext.ux.menu'); + +/** + * @class Ext.ux.menu.ListMenu + * @extends Ext.menu.Menu + * This is a supporting class for {@link Ext.ux.grid.filter.ListFilter}. + * Although not listed as configuration options for this class, this class + * also accepts all configuration options from {@link Ext.ux.grid.filter.ListFilter}. + */ +Ext.ux.menu.ListMenu = Ext.extend(Ext.menu.Menu, { + /** + * @cfg {String} labelField + * Defaults to 'text'. + */ + labelField : 'text', + /** + * @cfg {String} paramPrefix + * Defaults to 'Loading...'. + */ + loadingText : 'Loading...', + /** + * @cfg {Boolean} loadOnShow + * Defaults to true. + */ + loadOnShow : true, + /** + * @cfg {Boolean} single + * Specify true to group all items in this list into a single-select + * radio button group. Defaults to false. + */ + single : false, + + constructor : function (cfg) { + this.selected = []; + this.addEvents( + /** + * @event checkchange + * Fires when there is a change in checked items from this list + * @param {Object} item Ext.menu.CheckItem + * @param {Object} checked The checked value that was set + */ + 'checkchange' + ); + + Ext.ux.menu.ListMenu.superclass.constructor.call(this, cfg = cfg || {}); + + if(!cfg.store && cfg.options){ + var options = []; + for(var i=0, len=cfg.options.length; i -1, + hideOnClick: false}); + + item.itemId = records[i].id; + item.on('checkchange', this.checkChange, this); + + this.add(item); + } - var ll = this.getCenter(); - return {lat: ll.lat(), lng: ll.lng()}; + this.loaded = true; + if (visible) { + this.show(); + } + this.fireEvent('load', this, records); }, - addMarkers : function(markers) { - - if (Ext.isArray(markers)){ - for (var i = 0; i < markers.length; i++) { - var mkr_point = new GLatLng(markers[i].lat,markers[i].lng); - this.addMarker(mkr_point,markers[i].marker,false,markers[i].setCenter, markers[i].listeners); - } + + /** + * Get the selected items. + * @return {Array} selected + */ + getSelected : function () { + return this.selected; + }, + + /** @private */ + setSelected : function (value) { + value = this.selected = [].concat(value); + + if (this.loaded) { + this.items.each(function(item){ + item.setChecked(false, true); + for (var i = 0, len = value.length; i < len; i++) { + if (item.itemId == value[i]) { + item.setChecked(true, true); + } + } + }, this); } - }, - addMarker : function(point, marker, clear, center, listeners){ + + /** + * Handler for the 'checkchange' event from an check item in this menu + * @param {Object} item Ext.menu.CheckItem + * @param {Object} checked The checked value that was set + */ + checkChange : function (item, checked) { + var value = []; + this.items.each(function(item){ + if (item.checked) { + value.push(item.itemId); + } + },this); + this.selected = value; - Ext.applyIf(marker,G_DEFAULT_ICON); + this.fireEvent('checkchange', item, checked); + } +});Ext.ns('Ext.ux.menu'); + +/** + * @class Ext.ux.menu.RangeMenu + * @extends Ext.menu.Menu + * Custom implementation of Ext.menu.Menu that has preconfigured + * items for gt, lt, eq. + *

    Example Usage:

    + *
        
    +
    + * 
    + */ +Ext.ux.menu.RangeMenu = Ext.extend(Ext.menu.Menu, { - if (clear === true){ - this.getMap().clearOverlays(); - } - if (center === true) { - this.getMap().setCenter(point, this.zoomLevel); - } + constructor : function (config) { - var mark = new GMarker(point,marker); - if (typeof listeners === 'object'){ - for (evt in listeners) { - GEvent.bind(mark, evt, this, listeners[evt]); + Ext.ux.menu.RangeMenu.superclass.constructor.call(this, config); + + this.addEvents( + /** + * @event update + * Fires when a filter configuration has changed + * @param {Ext.ux.grid.filter.Filter} this The filter object. + */ + 'update' + ); + + this.updateTask = new Ext.util.DelayedTask(this.fireUpdate, this); + + var i, len, item, cfg, Cls; + + for (i = 0, len = this.menuItems.length; i < len; i++) { + item = this.menuItems[i]; + if (item !== '-') { + // defaults + cfg = { + itemId: 'range-' + item, + enableKeyEvents: true, + iconCls: this.iconCls[item] || 'no-icon', + listeners: { + scope: this, + keyup: this.onInputKeyUp + } + }; + Ext.apply( + cfg, + // custom configs + Ext.applyIf(this.fields[item] || {}, this.fieldCfg[item]), + // configurable defaults + this.menuItemCfgs + ); + Cls = cfg.fieldCls || this.fieldCls; + item = this.fields[item] = new Cls(cfg); } + this.add(item); } - this.getMap().addOverlay(mark); + }, + /** + * @private + * called by this.updateTask + */ + fireUpdate : function () { + this.fireEvent('update', this); }, - addMapControls : function(){ - - if (this.gmapType === 'map') { - if (Ext.isArray(this.mapControls)) { - for(i=0;i 0) { + result[key] = field.getValue(); } } - + return result; }, - addMapControl : function(mc){ - - var mcf = window[mc]; - if (typeof mcf === 'function') { - this.getMap().addControl(new mcf()); - } - + + /** + * Set the value of this menu and fires the 'update' event. + * @param {Object} data The data to assign to this menu + */ + setValue : function (data) { + var key; + for (key in this.fields) { + this.fields[key].setValue(data[key] !== undefined ? data[key] : ''); + } + this.fireEvent('update', this); }, - addOptions : function(){ + + /** + * @private + * Handler method called when there is a keyup event on an input + * item of this menu. + */ + onInputKeyUp : function (field, e) { + var k = e.getKey(); + if (k == e.RETURN && field.isValid()) { + e.stopEvent(); + this.hide(true); + return; + } - if (Ext.isArray(this.mapConfOpts)) { - var mc; - for(i=0;i
    Level '+accuracy+' Accuracy (8 = Exact Match, 1 = Vague Match)'); - }else{ - point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]); - if (typeof this.setCenter.marker === 'object' && typeof point === 'object'){ - this.addMarker(point,this.setCenter.marker,this.setCenter.marker.clear,true, this.setCenter.listeners); - } - } + if (this.fields.lt) { + this.fields.lt.setValue(null); } } + else { + this.fields.eq.setValue(null); + } + // restart the timer + this.updateTask.delay(this.updateBuffer); } - }); - -Ext.reg('gmappanel', Ext.ux.GMapPanel); Ext.ns('Ext.ux.grid'); +Ext.ns('Ext.ux.grid'); /** * @class Ext.ux.grid.GroupSummary @@ -1772,10 +3104,7 @@ summaryRenderer: function(v, params, data){ }, init : function(grid){ this.grid = grid; - this.cm = grid.getColumnModel(); - this.view = grid.getView(); - - var v = this.view; + var v = this.view = grid.getView(); v.doGroupEnd = this.doGroupEnd.createDelegate(this); v.afterMethod('onColumnWidthUpdated', this.doWidth, this); @@ -1822,9 +3151,8 @@ summaryRenderer: function(v, params, data){ renderSummary : function(o, cs){ cs = cs || this.view.getColumnData(); - var cfg = this.cm.config; - - var buf = [], c, p = {}, cf, last = cs.length-1; + var cfg = this.grid.getColumnModel().config, + buf = [], c, p = {}, cf, last = cs.length-1; for(var i = 0, len = cs.length; i < len; i++){ c = cs[i]; cf = cfg[i]; @@ -1852,7 +3180,7 @@ summaryRenderer: function(v, params, data){ * @param {Object} cs */ calculate : function(rs, cs){ - var data = {}, r, c, cfg = this.cm.config, cf; + var data = {}, r, c, cfg = this.grid.getColumnModel().config, cf; for(var j = 0, jlen = rs.length; j < jlen; j++){ r = rs[j]; for(var i = 0, len = cs.length; i < len; i++){ @@ -1920,21 +3248,21 @@ summaryRenderer: function(v, params, data){ }, refreshSummaryById : function(gid){ - var g = document.getElementById(gid); + var g = Ext.getDom(gid); if(!g){ return false; } var rs = []; - this.grid.store.each(function(r){ + this.grid.getStore().each(function(r){ if(r._groupId == gid){ rs[rs.length] = r; } }); - var cs = this.view.getColumnData(); - var data = this.calculate(rs, cs); - var markup = this.renderSummary({data: data}, cs); - - var existing = this.getSummaryNode(gid); + var cs = this.view.getColumnData(), + data = this.calculate(rs, cs), + markup = this.renderSummary({data: data}, cs), + existing = this.getSummaryNode(gid); + if(existing){ g.removeChild(existing); } @@ -1964,8 +3292,8 @@ grid.on('afteredit', function(){ * @param {String} msg Text to use as innerHTML for the summary row. */ showSummaryMsg : function(groupValue, msg){ - var gid = this.view.getGroupId(groupValue); - var node = this.getSummaryNode(gid); + var gid = this.view.getGroupId(groupValue), + node = this.getSummaryNode(gid); if(node){ node.innerHTML = '
    ' + msg + '
    '; } @@ -2057,9 +3385,9 @@ Ext.ux.grid.HybridSummary = Ext.extend(Ext.ux.grid.GroupSummary, { * @param {Object} cs */ calculate : function(rs, cs){ - var gcol = this.view.getGroupField(); - var gvalue = rs[0].data[gcol]; - var gdata = this.getSummaryData(gvalue); + var gcol = this.view.getGroupField(), + gvalue = rs[0].data[gcol], + gdata = this.getSummaryData(gvalue); return gdata || Ext.ux.grid.HybridSummary.superclass.calculate.call(this, rs, cs); }, @@ -2081,7 +3409,7 @@ grid.on('afteredit', function(){ * @param {Boolean} skipRefresh (Optional) Defaults to false */ updateSummaryData : function(groupValue, data, skipRefresh){ - var json = this.grid.store.reader.jsonData; + var json = this.grid.getStore().reader.jsonData; if(!json.summaryData){ json.summaryData = {}; } @@ -2097,7 +3425,7 @@ grid.on('afteredit', function(){ * @return {Object} summaryData */ getSummaryData : function(groupValue){ - var json = this.grid.store.reader.jsonData; + var json = this.grid.getStore().reader.jsonData; if(json && json.summaryData){ return json.summaryData[groupValue]; } @@ -2406,7 +3734,7 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { this.on('beforeadd', function(gtp, item, index){ this.initGroup(item, index); - }); + }); }, initEvents : function() { @@ -2415,7 +3743,6 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { onRender: function(ct, position){ Ext.TabPanel.superclass.onRender.call(this, ct, position); - if(this.plain){ var pos = this.tabPosition == 'top' ? 'header' : 'footer'; this[pos].addClass('x-tab-panel-'+pos+'-plain'); @@ -2429,11 +3756,11 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null); this.strip = new Ext.Element(this.stripWrap.dom.firstChild); - this.header.addClass('x-grouptabs-panel-header'); - this.bwrap.addClass('x-grouptabs-bwrap'); + this.header.addClass('x-grouptabs-panel-header'); + this.bwrap.addClass('x-grouptabs-bwrap'); this.body.addClass('x-tab-panel-body-'+this.tabPosition + ' x-grouptabs-panel-body'); - if (!this.itemTpl) { + if (!this.groupTpl) { var tt = new Ext.Template( '
  • ', '', @@ -2443,9 +3770,8 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { ); tt.disableFormats = true; tt.compile(); - Ext.ux.GroupTabPanel.prototype.itemTpl = tt; + Ext.ux.GroupTabPanel.prototype.groupTpl = tt; } - this.items.each(this.initGroup, this); }, @@ -2471,8 +3797,8 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { // private findTargets: function(e){ - var item = null; - var itemEl = e.getTarget('li', this.strip); + var item = null, + itemEl = e.getTarget('li', this.strip); if (itemEl) { item = this.findById(itemEl.id.split(this.idDelimiter)[1]); if (item.disabled) { @@ -2523,7 +3849,7 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { groupEl = this.getGroupEl(groupEl); } Ext.fly(groupEl).toggleClass('x-grouptabs-expanded'); - this.syncTabJoint(); + this.syncTabJoint(); }, syncTabJoint: function(groupEl){ @@ -2534,7 +3860,7 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { groupEl = groupEl || this.getGroupEl(this.activeGroup); if(groupEl) { this.tabJoint.setHeight(Ext.fly(groupEl).getHeight() - 2); - + var y = Ext.isGecko2 ? 0 : 1; if (this.tabPosition == 'left'){ this.tabJoint.alignTo(groupEl, 'tl-tr', [-2,y]); @@ -2565,18 +3891,17 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { }, initGroup: function(group, index){ - var before = this.strip.dom.childNodes[index]; - var p = this.getTemplateArgs(group); + var before = this.strip.dom.childNodes[index], + p = this.getTemplateArgs(group); if (index === 0) { p.cls += ' x-tab-first'; } p.cls += ' x-grouptabs-main'; p.text = group.getMainItem().title; - var el = before ? this.itemTpl.insertBefore(before, p) : this.itemTpl.append(this.strip, p); - - var tl = this.createCorner(el, 'top-' + this.tabPosition); - var bl = this.createCorner(el, 'bottom-' + this.tabPosition); + var el = before ? this.groupTpl.insertBefore(before, p) : this.groupTpl.append(this.strip, p), + tl = this.createCorner(el, 'top-' + this.tabPosition), + bl = this.createCorner(el, 'bottom-' + this.tabPosition); if (group.expanded) { this.expandGroup(el); @@ -2589,8 +3914,11 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { tl.setTop('-5px'); } - this.mon(group, 'changemainitem', this.onGroupChangeMainItem, this); - this.mon(group, 'beforetabchange', this.onGroupBeforeTabChange, this); + this.mon(group, { + scope: this, + changemainitem: this.onGroupChangeMainItem, + beforetabchange: this.onGroupBeforeTabChange + }); }, setActiveGroup : function(group) { @@ -2608,7 +3936,7 @@ Ext.ux.GroupTabPanel = Ext.extend(Ext.TabPanel, { if(oldEl){ Ext.fly(oldEl).removeClass('x-grouptabs-strip-active'); } - this.activeGroup.fireEvent('deactivate', this.activeTab); + this.activeGroup.fireEvent('deactivate', this.activeGroup); } var groupEl = this.getGroupEl(group); @@ -3128,9 +4456,9 @@ Ext.ux.form.MultiSelect = Ext.extend(Ext.form.Field, { height: this.height, width: this.width, style: "padding:0;", - tbar: this.tbar, - bodyStyle: 'overflow: auto;' + tbar: this.tbar }); + fs.body.addClass('ux-mselect'); this.view = new Ext.ListView({ multiSelect: true, @@ -3961,41 +5289,30 @@ Ext.ux.ProgressBarPager = Ext.extend(Object, { }, //public init : function (parent) { - - if(parent.displayInfo){ - this.parent = parent; - var ind = parent.items.indexOf(parent.displayItem); - parent.remove(parent.displayItem, true); - this.progressBar = new Ext.ProgressBar({ - text : this.defaultText, - width : this.progBarWidth, - animate : this.defaultAnimCfg - }); - - parent.displayItem = this.progressBar; - - parent.add(parent.displayItem); - parent.doLayout(); - Ext.apply(parent, this.parentOverrides); - - this.progressBar.on('render', function(pb) { - pb.el.applyStyles('cursor:pointer'); - - pb.el.on('click', this.handleProgressBarClick, this); - }, this); - - - // Remove the click handler from the - this.progressBar.on({ - scope : this, - beforeDestroy : function() { - this.progressBar.el.un('click', this.handleProgressBarClick, this); - } - }); - - } - - }, + + if(parent.displayInfo){ + this.parent = parent; + var ind = parent.items.indexOf(parent.displayItem); + parent.remove(parent.displayItem, true); + this.progressBar = new Ext.ProgressBar({ + text : this.defaultText, + width : this.progBarWidth, + animate : this.defaultAnimCfg + }); + + parent.displayItem = this.progressBar; + + parent.add(parent.displayItem); + parent.doLayout(); + Ext.apply(parent, this.parentOverrides); + + this.progressBar.on('render', function(pb) { + pb.mon(pb.getEl().applyStyles('cursor:pointer'), 'click', this.handleProgressBarClick, this); + }, this, {single: true}); + + } + + }, // private // This method handles the click for the progress bar handleProgressBarClick : function(e){ @@ -4064,6 +5381,11 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { monitorValid: true, focusDelay: 250, errorSummary: true, + + saveText: 'Save', + cancelText: 'Cancel', + commitChangesText: 'You need to commit or cancel your changes', + errorText: 'Errors', defaults: { normalWidth: true @@ -4080,6 +5402,13 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { * @param {Number} rowIndex The rowIndex of the row just edited */ 'beforeedit', + /** + * @event canceledit + * Fired when the editor is cancelled. + * @param {Ext.ux.grid.RowEditor} roweditor This object + * @param {Boolean} forced True if the cancel button is pressed, false is the editor was invalid. + */ + 'canceledit', /** * @event validateedit * Fired after a row is edited and passes validation. @@ -4126,7 +5455,8 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { columnresize: this.verifyLayout, columnmove: this.refreshFields, reconfigure: this.refreshFields, - destroy : this.destroy, + beforedestroy : this.beforedestroy, + destroy : this.destroy, bodyscroll: { buffer: 250, fn: this.positionButtons @@ -4136,6 +5466,12 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { grid.getView().on('refresh', this.stopEditing.createDelegate(this, [])); }, + beforedestroy: function() { + this.grid.getStore().un('remove', this.onStoreRemove, this); + this.stopEditing(false); + Ext.destroy(this.btns); + }, + refreshFields: function(){ this.initFields(); this.verifyLayout(); @@ -4154,17 +5490,18 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { startEditing: function(rowIndex, doFocus){ if(this.editing && this.isDirty()){ - this.showTooltip('You need to commit or cancel your changes'); + this.showTooltip(this.commitChangesText); return; } - this.editing = true; - if(typeof rowIndex == 'object'){ + if(Ext.isObject(rowIndex)){ rowIndex = this.grid.getStore().indexOf(rowIndex); } if(this.fireEvent('beforeedit', this, rowIndex) !== false){ - var g = this.grid, view = g.getView(); - var row = view.getRow(rowIndex); - var record = g.store.getAt(rowIndex); + this.editing = true; + var g = this.grid, view = g.getView(), + row = view.getRow(rowIndex), + record = g.store.getAt(rowIndex); + this.record = record; this.rowIndex = rowIndex; this.values = {}; @@ -4181,7 +5518,7 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { val = this.preEditValue(record, cm.getDataIndex(i)); f = fields[i]; f.setValue(val); - this.values[f.id] = val || ''; + this.values[f.id] = Ext.isEmpty(val) ? '' : val; } this.verifyLayout(true); if(!this.isVisible()){ @@ -4205,16 +5542,20 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { } if(saveChanges === false || !this.isValid()){ this.hide(); + this.fireEvent('canceledit', this, saveChanges === false); return; } - var changes = {}, r = this.record, hasChange = false; - var cm = this.grid.colModel, fields = this.items.items; + var changes = {}, + r = this.record, + hasChange = false, + cm = this.grid.colModel, + fields = this.items.items; for(var i = 0, len = cm.getColumnCount(); i < len; i++){ if(!cm.isHidden(i)){ var dindex = cm.getDataIndex(i); if(!Ext.isEmpty(dindex)){ - var oldValue = r.data[dindex]; - var value = this.postEditValue(fields[i].getValue(), oldValue, r, dindex); + var oldValue = r.data[dindex], + value = this.postEditValue(fields[i].getValue(), oldValue, r, dindex); if(String(oldValue) !== String(value)){ changes[dindex] = value; hasChange = true; @@ -4224,11 +5565,9 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { } if(hasChange && this.fireEvent('validateedit', this, changes, r, this.rowIndex) !== false){ r.beginEdit(); - for(var k in changes){ - if(changes.hasOwnProperty(k)){ - r.set(k, changes[k]); - } - } + Ext.iterate(changes, function(name, value){ + r.set(name, value); + }); r.endEdit(); this.fireEvent('afteredit', this, changes, r, this.rowIndex); } @@ -4238,14 +5577,11 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { verifyLayout: function(force){ if(this.el && (this.isVisible() || force === true)){ var row = this.grid.getView().getRow(this.rowIndex); - this.setSize(Ext.fly(row).getWidth(), Ext.isIE ? Ext.fly(row).getHeight() + (Ext.isBorderBox ? 9 : 0) : undefined); + this.setSize(Ext.fly(row).getWidth(), Ext.fly(row).getHeight() + 9); var cm = this.grid.colModel, fields = this.items.items; for(var i = 0, len = cm.getColumnCount(); i < len; i++){ if(!cm.isHidden(i)){ var adjust = 0; - if(i === 0){ - adjust += 0; // outer padding - } if(i === (len - 1)){ adjust += 3; // outer padding } else{ @@ -4270,8 +5606,8 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { var cm = this.grid.getColumnModel(), pm = Ext.layout.ContainerLayout.prototype.parseMargins; this.removeAll(false); for(var i = 0, len = cm.getColumnCount(); i < len; i++){ - var c = cm.getColumnAt(i); - var ed = c.getEditor(); + var c = cm.getColumnAt(i), + ed = c.getEditor(); if(!ed){ ed = c.displayEditor || new Ext.form.DisplayField(); } @@ -4347,12 +5683,12 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { ref: 'saveBtn', itemId: 'saveBtn', xtype: 'button', - text: this.saveText || 'Save', + text: this.saveText, width: this.minButtonWidth, handler: this.stopEditing.createDelegate(this, [true]) }, { xtype: 'button', - text: this.cancelText || 'Cancel', + text: this.cancelText, width: this.minButtonWidth, handler: this.stopEditing.createDelegate(this, [false]) }] @@ -4383,11 +5719,13 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { positionButtons: function(){ if(this.btns){ - var h = this.el.dom.clientHeight; - var view = this.grid.getView(); - var scroll = view.scroller.dom.scrollLeft; - var width = view.mainBody.getWidth(); - var bw = this.btns.getWidth(); + var g = this.grid, + h = this.el.dom.clientHeight, + view = g.getView(), + scroll = view.scroller.dom.scrollLeft, + bw = this.btns.getWidth(), + width = Math.min(g.getWidth(), g.getColumnModel().getTotalWidth()); + this.btns.el.shift({left: (width/2)-(bw/2)+scroll, top: h - 2, stopFx: true, duration:0.2}); } }, @@ -4405,13 +5743,14 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { doFocus: function(pt){ if(this.isVisible()){ - var index = 0; + var index = 0, + cm = this.grid.getColumnModel(), + c; if(pt){ index = this.getTargetColumnIndex(pt); } - var cm = this.grid.getColumnModel(); for(var i = index||0, len = cm.getColumnCount(); i < len; i++){ - var c = cm.getColumnAt(i); + c = cm.getColumnAt(i); if(!c.hidden && c.getEditor()){ c.getEditor().focus(); break; @@ -4421,10 +5760,12 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { }, getTargetColumnIndex: function(pt){ - var grid = this.grid, v = grid.view; - var x = pt.left; - var cms = grid.colModel.config; - var i = 0, match = false; + var grid = this.grid, + v = grid.view, + x = pt.left, + cms = grid.colModel.config, + i = 0, + match = false; for(var len = cms.length, c; c = cms[i]; i++){ if(!c.hidden){ if(Ext.fly(v.getHeaderCell(i)).getRegion().right >= x){ @@ -4485,21 +5826,30 @@ Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, { maxWidth: 600, cls: 'errorTip', width: 300, - title: 'Errors', + title: this.errorText, autoHide: false, anchor: 'left', anchorToTarget: true, mouseOffset: [40,0] }); } - t.initTarget(this.items.last().getEl()); - if(!t.rendered){ + var v = this.grid.getView(), + top = parseInt(this.el.dom.style.top, 10), + scroll = v.scroller.dom.scrollTop, + h = this.el.getHeight(); + + if(top + h >= scroll){ + t.initTarget(this.items.last().getEl()); + if(!t.rendered){ + t.show(); + t.hide(); + } + t.body.update(msg); + t.doAutoWidth(); t.show(); + }else if(t.rendered){ t.hide(); } - t.body.update(msg); - t.doAutoWidth(); - t.show(); }, getErrorText: function(){ @@ -4687,10 +6037,19 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { // @private onDestroy: function() { - this.keyNav.disable(); - delete this.keyNav; + if(this.keyNav){ + this.keyNav.disable(); + delete this.keyNav; + } + /* + * A majority of the time, the plugin will be destroyed along with the grid, + * which means the mainBody won't be available. On the off chance that the plugin + * isn't destroyed with the grid, take care of removing the listener. + */ var mainBody = this.grid.getView().mainBody; - mainBody.un('mousedown', this.onMouseDown, this); + if(mainBody){ + mainBody.un('mousedown', this.onMouseDown, this); + } }, // @private onRowDblClick: function(grid, rowIdx, e) { @@ -4961,181 +6320,182 @@ Ext.ux.form.SearchField = Ext.extend(Ext.form.TwinTriggerField, { * @xtype selectbox */ Ext.ux.form.SelectBox = Ext.extend(Ext.form.ComboBox, { - constructor: function(config){ - this.searchResetDelay = 1000; - config = config || {}; - config = Ext.apply(config || {}, { - editable: false, - forceSelection: true, - rowHeight: false, - lastSearchTerm: false, - triggerAction: 'all', - mode: 'local' - }); - - Ext.ux.form.SelectBox.superclass.constructor.apply(this, arguments); - - this.lastSelectedIndex = this.selectedIndex || 0; - }, + constructor: function(config){ + this.searchResetDelay = 1000; + config = config || {}; + config = Ext.apply(config || {}, { + editable: false, + forceSelection: true, + rowHeight: false, + lastSearchTerm: false, + triggerAction: 'all', + mode: 'local' + }); + + Ext.ux.form.SelectBox.superclass.constructor.apply(this, arguments); + + this.lastSelectedIndex = this.selectedIndex || 0; + }, + + initEvents : function(){ + Ext.ux.form.SelectBox.superclass.initEvents.apply(this, arguments); + // you need to use keypress to capture upper/lower case and shift+key, but it doesn't work in IE + this.el.on('keydown', this.keySearch, this, true); + this.cshTask = new Ext.util.DelayedTask(this.clearSearchHistory, this); + }, + + keySearch : function(e, target, options) { + var raw = e.getKey(); + var key = String.fromCharCode(raw); + var startIndex = 0; + + if( !this.store.getCount() ) { + return; + } + + switch(raw) { + case Ext.EventObject.HOME: + e.stopEvent(); + this.selectFirst(); + return; + + case Ext.EventObject.END: + e.stopEvent(); + this.selectLast(); + return; - initEvents : function(){ - Ext.ux.form.SelectBox.superclass.initEvents.apply(this, arguments); - // you need to use keypress to capture upper/lower case and shift+key, but it doesn't work in IE - this.el.on('keydown', this.keySearch, this, true); - this.cshTask = new Ext.util.DelayedTask(this.clearSearchHistory, this); - }, + case Ext.EventObject.PAGEDOWN: + this.selectNextPage(); + e.stopEvent(); + return; - keySearch : function(e, target, options) { - var raw = e.getKey(); - var key = String.fromCharCode(raw); - var startIndex = 0; + case Ext.EventObject.PAGEUP: + this.selectPrevPage(); + e.stopEvent(); + return; + } - if( !this.store.getCount() ) { - return; - } + // skip special keys other than the shift key + if( (e.hasModifier() && !e.shiftKey) || e.isNavKeyPress() || e.isSpecialKey() ) { + return; + } + if( this.lastSearchTerm == key ) { + startIndex = this.lastSelectedIndex; + } + this.search(this.displayField, key, startIndex); + this.cshTask.delay(this.searchResetDelay); + }, - switch(raw) { - case Ext.EventObject.HOME: - e.stopEvent(); - this.selectFirst(); - return; - - case Ext.EventObject.END: - e.stopEvent(); - this.selectLast(); - return; - - case Ext.EventObject.PAGEDOWN: - this.selectNextPage(); - e.stopEvent(); - return; - - case Ext.EventObject.PAGEUP: - this.selectPrevPage(); - e.stopEvent(); - return; - } + onRender : function(ct, position) { + this.store.on('load', this.calcRowsPerPage, this); + Ext.ux.form.SelectBox.superclass.onRender.apply(this, arguments); + if( this.mode == 'local' ) { + this.initList(); + this.calcRowsPerPage(); + } + }, - // skip special keys other than the shift key - if( (e.hasModifier() && !e.shiftKey) || e.isNavKeyPress() || e.isSpecialKey() ) { - return; - } - if( this.lastSearchTerm == key ) { - startIndex = this.lastSelectedIndex; - } - this.search(this.displayField, key, startIndex); - this.cshTask.delay(this.searchResetDelay); - }, + onSelect : function(record, index, skipCollapse){ + if(this.fireEvent('beforeselect', this, record, index) !== false){ + this.setValue(record.data[this.valueField || this.displayField]); + if( !skipCollapse ) { + this.collapse(); + } + this.lastSelectedIndex = index + 1; + this.fireEvent('select', this, record, index); + } + }, - onRender : function(ct, position) { - this.store.on('load', this.calcRowsPerPage, this); - Ext.ux.form.SelectBox.superclass.onRender.apply(this, arguments); - if( this.mode == 'local' ) { - this.calcRowsPerPage(); - } - }, + afterRender : function() { + Ext.ux.form.SelectBox.superclass.afterRender.apply(this, arguments); + if(Ext.isWebKit) { + this.el.swallowEvent('mousedown', true); + } + this.el.unselectable(); + this.innerList.unselectable(); + this.trigger.unselectable(); + this.innerList.on('mouseup', function(e, target, options) { + if( target.id && target.id == this.innerList.id ) { + return; + } + this.onViewClick(); + }, this); - onSelect : function(record, index, skipCollapse){ - if(this.fireEvent('beforeselect', this, record, index) !== false){ - this.setValue(record.data[this.valueField || this.displayField]); - if( !skipCollapse ) { - this.collapse(); - } - this.lastSelectedIndex = index + 1; - this.fireEvent('select', this, record, index); - } - }, + this.innerList.on('mouseover', function(e, target, options) { + if( target.id && target.id == this.innerList.id ) { + return; + } + this.lastSelectedIndex = this.view.getSelectedIndexes()[0] + 1; + this.cshTask.delay(this.searchResetDelay); + }, this); - render : function(ct) { - Ext.ux.form.SelectBox.superclass.render.apply(this, arguments); - if( Ext.isSafari ) { - this.el.swallowEvent('mousedown', true); - } - this.el.unselectable(); - this.innerList.unselectable(); - this.trigger.unselectable(); - this.innerList.on('mouseup', function(e, target, options) { - if( target.id && target.id == this.innerList.id ) { - return; - } - this.onViewClick(); - }, this); - - this.innerList.on('mouseover', function(e, target, options) { - if( target.id && target.id == this.innerList.id ) { - return; - } - this.lastSelectedIndex = this.view.getSelectedIndexes()[0] + 1; - this.cshTask.delay(this.searchResetDelay); - }, this); - - this.trigger.un('click', this.onTriggerClick, this); - this.trigger.on('mousedown', function(e, target, options) { - e.preventDefault(); - this.onTriggerClick(); - }, this); - - this.on('collapse', function(e, target, options) { - Ext.getDoc().un('mouseup', this.collapseIf, this); - }, this, true); - - this.on('expand', function(e, target, options) { - Ext.getDoc().on('mouseup', this.collapseIf, this); - }, this, true); - }, + this.trigger.un('click', this.onTriggerClick, this); + this.trigger.on('mousedown', function(e, target, options) { + e.preventDefault(); + this.onTriggerClick(); + }, this); - clearSearchHistory : function() { - this.lastSelectedIndex = 0; - this.lastSearchTerm = false; - }, + this.on('collapse', function(e, target, options) { + Ext.getDoc().un('mouseup', this.collapseIf, this); + }, this, true); - selectFirst : function() { - this.focusAndSelect(this.store.data.first()); - }, + this.on('expand', function(e, target, options) { + Ext.getDoc().on('mouseup', this.collapseIf, this); + }, this, true); + }, - selectLast : function() { - this.focusAndSelect(this.store.data.last()); - }, + clearSearchHistory : function() { + this.lastSelectedIndex = 0; + this.lastSearchTerm = false; + }, - selectPrevPage : function() { - if( !this.rowHeight ) { - return; - } - var index = Math.max(this.selectedIndex-this.rowsPerPage, 0); - this.focusAndSelect(this.store.getAt(index)); - }, + selectFirst : function() { + this.focusAndSelect(this.store.data.first()); + }, - selectNextPage : function() { - if( !this.rowHeight ) { - return; - } - var index = Math.min(this.selectedIndex+this.rowsPerPage, this.store.getCount() - 1); - this.focusAndSelect(this.store.getAt(index)); - }, + selectLast : function() { + this.focusAndSelect(this.store.data.last()); + }, - search : function(field, value, startIndex) { - field = field || this.displayField; - this.lastSearchTerm = value; - var index = this.store.find.apply(this.store, arguments); - if( index !== -1 ) { - this.focusAndSelect(index); - } - }, + selectPrevPage : function() { + if( !this.rowHeight ) { + return; + } + var index = Math.max(this.selectedIndex-this.rowsPerPage, 0); + this.focusAndSelect(this.store.getAt(index)); + }, - focusAndSelect : function(record) { - var index = typeof record === 'number' ? record : this.store.indexOf(record); - this.select(index, this.isExpanded()); - this.onSelect(this.store.getAt(record), index, this.isExpanded()); - }, + selectNextPage : function() { + if( !this.rowHeight ) { + return; + } + var index = Math.min(this.selectedIndex+this.rowsPerPage, this.store.getCount() - 1); + this.focusAndSelect(this.store.getAt(index)); + }, - calcRowsPerPage : function() { - if( this.store.getCount() ) { - this.rowHeight = Ext.fly(this.view.getNode(0)).getHeight(); - this.rowsPerPage = this.maxHeight / this.rowHeight; - } else { - this.rowHeight = false; - } - } + search : function(field, value, startIndex) { + field = field || this.displayField; + this.lastSearchTerm = value; + var index = this.store.find.apply(this.store, arguments); + if( index !== -1 ) { + this.focusAndSelect(index); + } + }, + + focusAndSelect : function(record) { + var index = Ext.isNumber(record) ? record : this.store.indexOf(record); + this.select(index, this.isExpanded()); + this.onSelect(this.store.getAt(index), index, this.isExpanded()); + }, + + calcRowsPerPage : function() { + if( this.store.getCount() ) { + this.rowHeight = Ext.fly(this.view.getNode(0)).getHeight(); + this.rowsPerPage = this.maxHeight / this.rowHeight; + } else { + this.rowHeight = false; + } + } }); @@ -5209,6 +6569,7 @@ Ext.ux.SlidingPager = Ext.extend(Object, { * @xtype spinnerfield */ Ext.ux.form.SpinnerField = Ext.extend(Ext.form.NumberField, { + actionMode: 'wrap', deferHeight: true, autoSize: Ext.emptyFn, onBlur: Ext.emptyFn, @@ -5228,17 +6589,6 @@ Ext.ux.form.SpinnerField = Ext.extend(Ext.form.NumberField, { Ext.ux.form.SpinnerField.superclass.constructor.call(this, Ext.apply(config, {plugins: plugins})); }, - onShow: function(){ - if (this.wrap) { - this.wrap.dom.style.display = ''; - this.wrap.dom.style.visibility = 'visible'; - } - }, - - onHide: function(){ - this.wrap.dom.style.display = 'none'; - }, - // private getResizeEl: function(){ return this.wrap; @@ -5363,7 +6713,7 @@ Ext.ux.Spinner = Ext.extend(Ext.util.Observable, { doResize: function(w, h){ if (typeof w == 'number') { - this.el.setWidth(this.field.adjustWidth('input', w - this.trigger.getWidth())); + this.el.setWidth(w - this.trigger.getWidth()); } this.wrap.setWidth(this.el.getWidth() + this.trigger.getWidth()); }, @@ -5835,7 +7185,430 @@ Ext.ux.Spotlight.prototype = { }; //backwards compat -Ext.Spotlight = Ext.ux.Spotlight;/** +Ext.Spotlight = Ext.ux.Spotlight;/** + * @class Ext.ux.StatusBar + *

    Basic status bar component that can be used as the bottom toolbar of any {@link Ext.Panel}. In addition to + * supporting the standard {@link Ext.Toolbar} interface for adding buttons, menus and other items, the StatusBar + * provides a greedy status element that can be aligned to either side and has convenient methods for setting the + * status text and icon. You can also indicate that something is processing using the {@link #showBusy} method.

    + *
    
    +new Ext.Panel({
    +    title: 'StatusBar',
    +    // etc.
    +    bbar: new Ext.ux.StatusBar({
    +        id: 'my-status',
    +
    +        // defaults to use when the status is cleared:
    +        defaultText: 'Default status text',
    +        defaultIconCls: 'default-icon',
    +
    +        // values to set initially:
    +        text: 'Ready',
    +        iconCls: 'ready-icon',
    +
    +        // any standard Toolbar items:
    +        items: [{
    +            text: 'A Button'
    +        }, '-', 'Plain Text']
    +    })
    +});
    +
    +// Update the status bar later in code:
    +var sb = Ext.getCmp('my-status');
    +sb.setStatus({
    +    text: 'OK',
    +    iconCls: 'ok-icon',
    +    clear: true // auto-clear after a set interval
    +});
    +
    +// Set the status bar to show that something is processing:
    +sb.showBusy();
    +
    +// processing....
    +
    +sb.clearStatus(); // once completeed
    +
    + * @extends Ext.Toolbar + * @constructor + * Creates a new StatusBar + * @param {Object/Array} config A config object + */ +Ext.ux.StatusBar = Ext.extend(Ext.Toolbar, { + /** + * @cfg {String} statusAlign + * The alignment of the status element within the overall StatusBar layout. When the StatusBar is rendered, + * it creates an internal div containing the status text and icon. Any additional Toolbar items added in the + * StatusBar's {@link #items} config, or added via {@link #add} or any of the supported add* methods, will be + * rendered, in added order, to the opposite side. The status element is greedy, so it will automatically + * expand to take up all sapce left over by any other items. Example usage: + *
    
    +// Create a left-aligned status bar containing a button,
    +// separator and text item that will be right-aligned (default):
    +new Ext.Panel({
    +    title: 'StatusBar',
    +    // etc.
    +    bbar: new Ext.ux.StatusBar({
    +        defaultText: 'Default status text',
    +        id: 'status-id',
    +        items: [{
    +            text: 'A Button'
    +        }, '-', 'Plain Text']
    +    })
    +});
    +
    +// By adding the statusAlign config, this will create the
    +// exact same toolbar, except the status and toolbar item
    +// layout will be reversed from the previous example:
    +new Ext.Panel({
    +    title: 'StatusBar',
    +    // etc.
    +    bbar: new Ext.ux.StatusBar({
    +        defaultText: 'Default status text',
    +        id: 'status-id',
    +        statusAlign: 'right',
    +        items: [{
    +            text: 'A Button'
    +        }, '-', 'Plain Text']
    +    })
    +});
    +
    + */ + /** + * @cfg {String} defaultText + * The default {@link #text} value. This will be used anytime the status bar is cleared with the + * useDefaults:true option (defaults to ''). + */ + /** + * @cfg {String} defaultIconCls + * The default {@link #iconCls} value (see the iconCls docs for additional details about customizing the icon). + * This will be used anytime the status bar is cleared with the useDefaults:true option (defaults to ''). + */ + /** + * @cfg {String} text + * A string that will be initially set as the status message. This string + * will be set as innerHTML (html tags are accepted) for the toolbar item. + * If not specified, the value set for {@link #defaultText} + * will be used. + */ + /** + * @cfg {String} iconCls + * A CSS class that will be initially set as the status bar icon and is + * expected to provide a background image (defaults to ''). + * Example usage:
    
    +// Example CSS rule:
    +.x-statusbar .x-status-custom {
    +    padding-left: 25px;
    +    background: transparent url(images/custom-icon.gif) no-repeat 3px 2px;
    +}
    +
    +// Setting a default icon:
    +var sb = new Ext.ux.StatusBar({
    +    defaultIconCls: 'x-status-custom'
    +});
    +
    +// Changing the icon:
    +sb.setStatus({
    +    text: 'New status',
    +    iconCls: 'x-status-custom'
    +});
    +
    + */ + + /** + * @cfg {String} cls + * The base class applied to the containing element for this component on render (defaults to 'x-statusbar') + */ + cls : 'x-statusbar', + /** + * @cfg {String} busyIconCls + * The default {@link #iconCls} applied when calling + * {@link #showBusy} (defaults to 'x-status-busy'). + * It can be overridden at any time by passing the iconCls + * argument into {@link #showBusy}. + */ + busyIconCls : 'x-status-busy', + /** + * @cfg {String} busyText + * The default {@link #text} applied when calling + * {@link #showBusy} (defaults to 'Loading...'). + * It can be overridden at any time by passing the text + * argument into {@link #showBusy}. + */ + busyText : 'Loading...', + /** + * @cfg {Number} autoClear + * The number of milliseconds to wait after setting the status via + * {@link #setStatus} before automatically clearing the status + * text and icon (defaults to 5000). Note that this only applies + * when passing the clear argument to {@link #setStatus} + * since that is the only way to defer clearing the status. This can + * be overridden by specifying a different wait value in + * {@link #setStatus}. Calls to {@link #clearStatus} + * always clear the status bar immediately and ignore this value. + */ + autoClear : 5000, + + /** + * @cfg {String} emptyText + * The text string to use if no text has been set. Defaults to + * ' '). If there are no other items in the toolbar using + * an empty string ('') for this value would end up in the toolbar + * height collapsing since the empty string will not maintain the toolbar + * height. Use '' if the toolbar should collapse in height + * vertically when no text is specified and there are no other items in + * the toolbar. + */ + emptyText : ' ', + + // private + activeThreadId : 0, + + // private + initComponent : function(){ + if(this.statusAlign=='right'){ + this.cls += ' x-status-right'; + } + Ext.ux.StatusBar.superclass.initComponent.call(this); + }, + + // private + afterRender : function(){ + Ext.ux.StatusBar.superclass.afterRender.call(this); + + var right = this.statusAlign == 'right'; + this.currIconCls = this.iconCls || this.defaultIconCls; + this.statusEl = new Ext.Toolbar.TextItem({ + cls: 'x-status-text ' + (this.currIconCls || ''), + text: this.text || this.defaultText || '' + }); + + if(right){ + this.add('->'); + this.add(this.statusEl); + }else{ + this.insert(0, this.statusEl); + this.insert(1, '->'); + } + +// this.statusEl = td.createChild({ +// cls: 'x-status-text ' + (this.iconCls || this.defaultIconCls || ''), +// html: this.text || this.defaultText || '' +// }); +// this.statusEl.unselectable(); + +// this.spacerEl = td.insertSibling({ +// tag: 'td', +// style: 'width:100%', +// cn: [{cls:'ytb-spacer'}] +// }, right ? 'before' : 'after'); + }, + + /** + * Sets the status {@link #text} and/or {@link #iconCls}. Also supports automatically clearing the + * status that was set after a specified interval. + * @param {Object/String} config A config object specifying what status to set, or a string assumed + * to be the status text (and all other options are defaulted as explained below). A config + * object containing any or all of the following properties can be passed:
      + *
    • text {String} : (optional) The status text to display. If not specified, any current + * status text will remain unchanged.
    • + *
    • iconCls {String} : (optional) The CSS class used to customize the status icon (see + * {@link #iconCls} for details). If not specified, any current iconCls will remain unchanged.
    • + *
    • clear {Boolean/Number/Object} : (optional) Allows you to set an internal callback that will + * automatically clear the status text and iconCls after a specified amount of time has passed. If clear is not + * specified, the new status will not be auto-cleared and will stay until updated again or cleared using + * {@link #clearStatus}. If true is passed, the status will be cleared using {@link #autoClear}, + * {@link #defaultText} and {@link #defaultIconCls} via a fade out animation. If a numeric value is passed, + * it will be used as the callback interval (in milliseconds), overriding the {@link #autoClear} value. + * All other options will be defaulted as with the boolean option. To customize any other options, + * you can pass an object in the format:
        + *
      • wait {Number} : (optional) The number of milliseconds to wait before clearing + * (defaults to {@link #autoClear}).
      • + *
      • anim {Number} : (optional) False to clear the status immediately once the callback + * executes (defaults to true which fades the status out).
      • + *
      • useDefaults {Number} : (optional) False to completely clear the status text and iconCls + * (defaults to true which uses {@link #defaultText} and {@link #defaultIconCls}).
      • + *
    + * Example usage:
    
    +// Simple call to update the text
    +statusBar.setStatus('New status');
    +
    +// Set the status and icon, auto-clearing with default options:
    +statusBar.setStatus({
    +    text: 'New status',
    +    iconCls: 'x-status-custom',
    +    clear: true
    +});
    +
    +// Auto-clear with custom options:
    +statusBar.setStatus({
    +    text: 'New status',
    +    iconCls: 'x-status-custom',
    +    clear: {
    +        wait: 8000,
    +        anim: false,
    +        useDefaults: false
    +    }
    +});
    +
    + * @return {Ext.ux.StatusBar} this + */ + setStatus : function(o){ + o = o || {}; + + if(typeof o == 'string'){ + o = {text:o}; + } + if(o.text !== undefined){ + this.setText(o.text); + } + if(o.iconCls !== undefined){ + this.setIcon(o.iconCls); + } + + if(o.clear){ + var c = o.clear, + wait = this.autoClear, + defaults = {useDefaults: true, anim: true}; + + if(typeof c == 'object'){ + c = Ext.applyIf(c, defaults); + if(c.wait){ + wait = c.wait; + } + }else if(typeof c == 'number'){ + wait = c; + c = defaults; + }else if(typeof c == 'boolean'){ + c = defaults; + } + + c.threadId = this.activeThreadId; + this.clearStatus.defer(wait, this, [c]); + } + return this; + }, + + /** + * Clears the status {@link #text} and {@link #iconCls}. Also supports clearing via an optional fade out animation. + * @param {Object} config (optional) A config object containing any or all of the following properties. If this + * object is not specified the status will be cleared using the defaults below:
      + *
    • anim {Boolean} : (optional) True to clear the status by fading out the status element (defaults + * to false which clears immediately).
    • + *
    • useDefaults {Boolean} : (optional) True to reset the text and icon using {@link #defaultText} and + * {@link #defaultIconCls} (defaults to false which sets the text to '' and removes any existing icon class).
    • + *
    + * @return {Ext.ux.StatusBar} this + */ + clearStatus : function(o){ + o = o || {}; + + if(o.threadId && o.threadId !== this.activeThreadId){ + // this means the current call was made internally, but a newer + // thread has set a message since this call was deferred. Since + // we don't want to overwrite a newer message just ignore. + return this; + } + + var text = o.useDefaults ? this.defaultText : this.emptyText, + iconCls = o.useDefaults ? (this.defaultIconCls ? this.defaultIconCls : '') : ''; + + if(o.anim){ + // animate the statusEl Ext.Element + this.statusEl.el.fadeOut({ + remove: false, + useDisplay: true, + scope: this, + callback: function(){ + this.setStatus({ + text: text, + iconCls: iconCls + }); + + this.statusEl.el.show(); + } + }); + }else{ + // hide/show the el to avoid jumpy text or icon + this.statusEl.hide(); + this.setStatus({ + text: text, + iconCls: iconCls + }); + this.statusEl.show(); + } + return this; + }, + + /** + * Convenience method for setting the status text directly. For more flexible options see {@link #setStatus}. + * @param {String} text (optional) The text to set (defaults to '') + * @return {Ext.ux.StatusBar} this + */ + setText : function(text){ + this.activeThreadId++; + this.text = text || ''; + if(this.rendered){ + this.statusEl.setText(this.text); + } + return this; + }, + + /** + * Returns the current status text. + * @return {String} The status text + */ + getText : function(){ + return this.text; + }, + + /** + * Convenience method for setting the status icon directly. For more flexible options see {@link #setStatus}. + * See {@link #iconCls} for complete details about customizing the icon. + * @param {String} iconCls (optional) The icon class to set (defaults to '', and any current icon class is removed) + * @return {Ext.ux.StatusBar} this + */ + setIcon : function(cls){ + this.activeThreadId++; + cls = cls || ''; + + if(this.rendered){ + if(this.currIconCls){ + this.statusEl.removeClass(this.currIconCls); + this.currIconCls = null; + } + if(cls.length > 0){ + this.statusEl.addClass(cls); + this.currIconCls = cls; + } + }else{ + this.currIconCls = cls; + } + return this; + }, + + /** + * Convenience method for setting the status text and icon to special values that are pre-configured to indicate + * a "busy" state, usually for loading or processing activities. + * @param {Object/String} config (optional) A config object in the same format supported by {@link #setStatus}, or a + * string to use as the status text (in which case all other options for setStatus will be defaulted). Use the + * text and/or iconCls properties on the config to override the default {@link #busyText} + * and {@link #busyIconCls} settings. If the config argument is not specified, {@link #busyText} and + * {@link #busyIconCls} will be used in conjunction with all of the default options for {@link #setStatus}. + * @return {Ext.ux.StatusBar} this + */ + showBusy : function(o){ + if(typeof o == 'string'){ + o = {text:o}; + } + o = Ext.applyIf(o || {}, { + text: this.busyText, + iconCls: this.busyIconCls + }); + return this.setStatus(o); + } +}); +Ext.reg('statusbar', Ext.ux.StatusBar); +/** * @class Ext.ux.TabCloseMenu * @extends Object * Plugin (ptype = 'tabclosemenu') for adding a close context menu to tabs. @@ -6249,3 +8022,193 @@ Ext.ux.tree.XmlTreeLoader = Ext.extend(Ext.tree.TreeLoader, { //backwards compat Ext.ux.XmlTreeLoader = Ext.ux.tree.XmlTreeLoader; +/** + * @class Ext.ux.ValidationStatus + * A {@link Ext.StatusBar} plugin that provides automatic error notification when the + * associated form contains validation errors. + * @extends Ext.Component + * @constructor + * Creates a new ValiationStatus plugin + * @param {Object} config A config object + */ +Ext.ux.ValidationStatus = Ext.extend(Ext.Component, { + /** + * @cfg {String} errorIconCls + * The {@link #iconCls} value to be applied to the status message when there is a + * validation error. Defaults to 'x-status-error'. + */ + errorIconCls : 'x-status-error', + /** + * @cfg {String} errorListCls + * The css class to be used for the error list when there are validation errors. + * Defaults to 'x-status-error-list'. + */ + errorListCls : 'x-status-error-list', + /** + * @cfg {String} validIconCls + * The {@link #iconCls} value to be applied to the status message when the form + * validates. Defaults to 'x-status-valid'. + */ + validIconCls : 'x-status-valid', + + /** + * @cfg {String} showText + * The {@link #text} value to be applied when there is a form validation error. + * Defaults to 'The form has errors (click for details...)'. + */ + showText : 'The form has errors (click for details...)', + /** + * @cfg {String} showText + * The {@link #text} value to display when the error list is displayed. + * Defaults to 'Click again to hide the error list'. + */ + hideText : 'Click again to hide the error list', + /** + * @cfg {String} submitText + * The {@link #text} value to be applied when the form is being submitted. + * Defaults to 'Saving...'. + */ + submitText : 'Saving...', + + // private + init : function(sb){ + sb.on('render', function(){ + this.statusBar = sb; + this.monitor = true; + this.errors = new Ext.util.MixedCollection(); + this.listAlign = (sb.statusAlign=='right' ? 'br-tr?' : 'bl-tl?'); + + if(this.form){ + this.form = Ext.getCmp(this.form).getForm(); + this.startMonitoring(); + this.form.on('beforeaction', function(f, action){ + if(action.type == 'submit'){ + // Ignore monitoring while submitting otherwise the field validation + // events cause the status message to reset too early + this.monitor = false; + } + }, this); + var startMonitor = function(){ + this.monitor = true; + }; + this.form.on('actioncomplete', startMonitor, this); + this.form.on('actionfailed', startMonitor, this); + } + }, this, {single:true}); + sb.on({ + scope: this, + afterlayout:{ + single: true, + fn: function(){ + // Grab the statusEl after the first layout. + sb.statusEl.getEl().on('click', this.onStatusClick, this, {buffer:200}); + } + }, + beforedestroy:{ + single: true, + fn: this.onDestroy + } + }); + }, + + // private + startMonitoring : function(){ + this.form.items.each(function(f){ + f.on('invalid', this.onFieldValidation, this); + f.on('valid', this.onFieldValidation, this); + }, this); + }, + + // private + stopMonitoring : function(){ + this.form.items.each(function(f){ + f.un('invalid', this.onFieldValidation, this); + f.un('valid', this.onFieldValidation, this); + }, this); + }, + + // private + onDestroy : function(){ + this.stopMonitoring(); + this.statusBar.statusEl.un('click', this.onStatusClick, this); + Ext.ux.ValidationStatus.superclass.onDestroy.call(this); + }, + + // private + onFieldValidation : function(f, msg){ + if(!this.monitor){ + return false; + } + if(msg){ + this.errors.add(f.id, {field:f, msg:msg}); + }else{ + this.errors.removeKey(f.id); + } + this.updateErrorList(); + if(this.errors.getCount() > 0){ + if(this.statusBar.getText() != this.showText){ + this.statusBar.setStatus({text:this.showText, iconCls:this.errorIconCls}); + } + }else{ + this.statusBar.clearStatus().setIcon(this.validIconCls); + } + }, + + // private + updateErrorList : function(){ + if(this.errors.getCount() > 0){ + var msg = '
      '; + this.errors.each(function(err){ + msg += ('
    • ' + err.msg + '
    • '); + }, this); + this.getMsgEl().update(msg+'
    '); + }else{ + this.getMsgEl().update(''); + } + }, + + // private + getMsgEl : function(){ + if(!this.msgEl){ + this.msgEl = Ext.DomHelper.append(Ext.getBody(), { + cls: this.errorListCls+' x-hide-offsets' + }, true); + + this.msgEl.on('click', function(e){ + var t = e.getTarget('li', 10, true); + if(t){ + Ext.getCmp(t.id.split('x-err-')[1]).focus(); + this.hideErrors(); + } + }, this, {stopEvent:true}); // prevent anchor click navigation + } + return this.msgEl; + }, + + // private + showErrors : function(){ + this.updateErrorList(); + this.getMsgEl().alignTo(this.statusBar.getEl(), this.listAlign).slideIn('b', {duration:0.3, easing:'easeOut'}); + this.statusBar.setText(this.hideText); + this.form.getEl().on('click', this.hideErrors, this, {single:true}); // hide if the user clicks directly into the form + }, + + // private + hideErrors : function(){ + var el = this.getMsgEl(); + if(el.isVisible()){ + el.slideOut('b', {duration:0.2, easing:'easeIn'}); + this.statusBar.setText(this.showText); + } + this.form.getEl().un('click', this.hideErrors, this); + }, + + // private + onStatusClick : function(){ + if(this.getMsgEl().isVisible()){ + this.hideErrors(); + }else if(this.errors.getCount() > 0){ + this.showErrors(); + } + } +}); \ No newline at end of file