Ext.namespace('Zarafa.common.ui.grid'); /** * @class Zarafa.common.ui.grid.GridPanel * @extends Ext.grid.GridPanel * @xtype zarafa.gridpanel * * WebApp specific GridPanel which contain extra features and bugfixes * which could not be resolved by plugins or directly in extjs. */ Zarafa.common.ui.grid.GridPanel = Ext.extend(Ext.grid.GridPanel, { /** * @cfg {String} ddText * @hide */ /** * The entryid of the {@link Zarafa.core.data.MAPIFolder folder} which is currently being displayed * inside this panel. This is used by {@link #getStateName} to obtain the correct {@link #getState state}. * * @property * @type String */ currentEntryId : undefined, /** * The store entryid of the {@link Zarafa.core.data.MAPIFolder folder} which is currently being displayed * inside this panel. This is used by {@link #getStateName} to obtain the correct {@link #getState state}. * * @property * @type String */ currentStoreEntryId : undefined, /** * The tooltip that will be used to show the full date when hovered over 'short' formatted dates * @property * @type {Zarafa.common.ui.grid.DateTooltip} */ dateTooltip : null, /** * @constructor * @param {Object} config Configuration object */ constructor : function(config) { config = config || {}; Ext.applyIf(config, { deferRowRender : false }); if(Ext.isEmpty(config.view)) { config.viewConfig = Ext.applyIf(config.viewConfig || {}, { autoFill : true, markDirty: false }); Ext.applyIf(config, { view : new Zarafa.common.ui.grid.GridView(config.viewConfig) }); } Zarafa.common.ui.grid.GridPanel.superclass.constructor.call(this, config); }, /** * Called when the GridPanel has been rendered. * This activate the keymap on the element of this component after the normal operations of * afterRender have been completed. It will activate by getting the xtype hierarchy from * {@link #getXTypes} and format it into a string usable by the * {@link Zarafa.core.KeyMapMgr KeyMapMgr}. * @private */ afterRender: function() { Zarafa.common.ui.grid.GridPanel.superclass.afterRender.apply(this, arguments); var xtypes = this.getXTypes(); // The first part leading up to zarafa.gridpanel will be stripped xtypes = xtypes.replace('component/box/container/panel/grid/zarafa.gridpanel',''); // Then the "zarafa." will be stripped off from all the xtypes like "zarafa.somegrid". xtypes = xtypes.replace(/\/zarafa\./g,'.'); // Finally we strip the string "grid" from all the xtypes. Otherwise each level will have // that "grid" mentioned in them. Also we add "grid" to the start as that sets // it apart from other components in the key mapping. xtypes = 'grid' + xtypes.replace(/grid/g, ''); Zarafa.core.KeyMapMgr.activate(this, xtypes); this.dateTooltip = new Zarafa.common.ui.grid.DateTooltip({ target: this.getEl().dom }); }, /** * Called when the GridPanel has been destroyed. Will destroy the {#dateTooltip} * @private */ onDestroy: function() { Zarafa.common.ui.grid.GridPanel.superclass.onDestroy.apply(this, arguments); if ( this.dateTooltip ){ this.dateTooltip.destroy(); } }, /** * Initialize event handlers * @private */ initEvents : function() { // First bind columnModel and then the store. The bindStore() function // will add some initialization which requires the model to be initialized. this.bindColumnModel(this.getColumnModel(), true); this.bindStore(this.getStore(), true); Zarafa.common.ui.grid.GridPanel.superclass.initEvents.call(this); // use our custom load mask if(this.loadMask) { // destroy loadmask created by superclass this.loadMask.destroy(); this.loadMask = new Zarafa.common.ui.LoadMask(this.bwrap, Ext.apply( { store : this.store }, this.initialConfig.loadMask ) ); } this.store.on('write', this.onWriteRecord, this); this.on('viewready', this.onViewReady, this); if (this.model && this.model.statefulRecordSelection) { this.mon(this.model, 'recordselectionchange', this.onRecordSelectionChange, this); } this.mon(this.getSelectionModel(), 'selectionchange', this.onGridSelectionChange, this); }, /** * Event handler which is fired when the recordselection in the {@link #model} has been changed. * If no selection is currently active, this will automatically select the given records in the grid. * * @param {Zarafa.core.ContextModel} model this model. * @param {Zarafa.core.data.IPMRecord[]} records The selected records * @private */ onRecordSelectionChange : function(model, records) { if (!this.getSelectionModel().hasSelection() && !Ext.isEmpty(records)) { var index = model.getStore().indexOf(records[0]); this.getSelectionModel().selectRecords(records); this.getView().focusRow(index); } }, /** * Handler for 'write' event. If the action carried out with the write call was 'destroy' than we have to check * a situation where the total loaded record is less than total page size and grid-scroll is not there. * If the above mentioned situation is there than additional records needs to be fetched from server and * synchronize {@link Zarafa.core.data.ListModuleStore store} with as many numbers of new {@link Zarafa.core.data.IPMRecords[] records} as deleted * @param {Ext.data.Store} store The new {@link Ext.data.Store} object * @param {String} action The name if action that was carried out for this write call. * @param {Object} Result The data arrives from server side. * @param {Object} response as it was received from server. * @param {Array} recordSet Records that are deleted. */ onWriteRecord : function(store, action, result, response, recordSet) { if(action === 'destroy') { if (!store.syncStore) { var gridScroller = this.getView().scroller; if(Ext.isDefined(gridScroller) && !gridScroller.isScrollable()) { if (store.totalLoadedRecord < store.totalLength) { var options = { add : true, actionType : Zarafa.core.Actions['list'] }; // load store with as many new records as deleted before scrollbar has disappeared // For that sets start and limit base on remaining number of records. // For example if we have 11 remaining records left then start = 11 and limit = page_size - 11 Ext.applyIf(options, store.lastOptions); var limit = container.getSettingsModel().get('zarafa/v1/main/page_size'); options.params.restriction.limit = limit - store.getCount(); options.params.restriction.start = store.getCount(); store.syncStore = true; if (store.loadMask) { store.loadMask.hide(); store.loadMask = undefined; } store.load(options); } } } } }, /** * Reconfigures the grid to use a different Store and Column Model and fires the 'reconfigure' event. * The View will be bound to the new objects and refreshed. * Be aware that upon reconfiguring a GridPanel, certain existing settings may become invalidated. * For example the configured {@link #autoExpandColumn} may no longer exist in the new ColumnModel. * Also, an existing {@link Ext.PagingToolbar PagingToolbar} will still be bound to the old Store, * and will need rebinding. Any {@link #plugins} might also need reconfiguring with the new data * * @param {Ext.data.Store} store The new {@link Ext.data.Store} object * @param {Ext.grid.ColumnModel} model The new {@link Ext.grid.ColumnModel} object */ reconfigure : function(store, colModel) { // First bind columnModel and then the store. The bindStore() function // will add some initialization which requires the model to be initialized. this.bindColumnModel(colModel); this.bindStore(store); Zarafa.common.ui.grid.GridPanel.superclass.reconfigure.call(this, store, colModel); }, /** * Bind a new column model to this gridpanel. This will hook to all * important events with configuration changes which must be stored into the {@link #getState state}. * * @param {Ext.grid.ColumnModel} model The model which must be bound to the panel * @param {Boolean} initialize True if this is called from the constructor to initialize * the panel for the first time. * @private */ bindColumnModel : function(model, initialize) { var oldModel = this.getColumnModel(); if (initialize === true || oldModel !== model) { if (oldModel) { this.mun(oldModel, 'beforeconfigchange', this.onBeforeConfigChange, this); this.mun(oldModel, 'configchange', this.onConfigChange, this); } if (model) { this.mon(model, 'beforeconfigchange', this.onBeforeConfigChange, this); this.mon(model, 'configchange', this.onConfigChange, this); } } }, /** * Bind a new store to this gridpanel. This will hook to all * important events with configuration changes which must be stored into the {@link #getState state}.. * * @param {Ext.data.Store} store The store which must be bound to the panel * @param {Boolean} initialize True if this is called from the constructor to initialize * the panel for the first time. * @private */ bindStore : function(store, initialize) { var oldStore = this.getStore(); if (initialize === true || oldStore !== store) { if (oldStore) { this.mun(oldStore, 'beforeload', this.onStoreBeforeLoad, this); this.mun(oldStore, 'load', this.onStoreLoad, this); } if (store) { this.mon(store, 'beforeload', this.onStoreBeforeLoad, this); this.mon(store, 'load', this.onStoreLoad, this); // In case the store was already loaded, just call the // event handler directly. if (store.lastOptions) { this.onStoreBeforeLoad(store, store.lastOptions); } } } }, /** * Event handler for the {@link #viewready} event. The {@link Ext.grid.GridPanel Extjs GridPanel}, * or more accurately the {@link Zarafa.common.ui.LoadMask} do not handle the case nicely when the store * is already busy loading when the grid is being rendered. Because we do like such an optimization, * we have to check at this time if the store is loading, and display the loadmask. * @private */ onViewReady : function() { var show = this.store && (this.store.isExecuting(Zarafa.core.Actions['list']) === true || this.store.isExecuting(Zarafa.core.Actions['search']) === true); if (this.loadMask && show) { this.loadMask.show(); } }, /** * Called right before the {@link #store} sends a {@link Ext.data.Store#load} request to the server, * this will check which entryId is loaded from the server and will {@link #applyState apply the state} * for the new folder onto the panel. The 'options' argument will be updated according to the sorting * requirements from the {@link Ext.state.Manager#get state}. * @param {Ext.data.Store} store The store which fired the event * @param {Object} options The options used for loading the new data from the store * @private */ onStoreBeforeLoad : function(store, options) { /* * if action type is updatelist and grid has no dummy row with warped * loading mask, then show the loading mask on the grid row rather to * show load mask on whole grid. */ if(options && (options.actionType === Zarafa.core.Actions['updatelist'])) { if(!this.isLoading) { this.getView().showGridRowLoadMask(this.loadMask.msg); this.isLoading = true; } return; } // No need to reset the column model when we are searching // We must use the column model the user set for the folder // in which he is searching if ( !Ext.isEmpty(store) && store.hasSearchResults ===true ){ return; } var model = this.getColumnModel(); var folder = options.folder; var entryId; var storeEntryId; if (!Ext.isEmpty(folder)) { if (Array.isArray(folder)) { entryId = folder[0].get('entryid'); storeEntryId = folder[0].get('store_entryid'); } else { entryId = folder.get('entryid'); storeEntryId = folder.get('store_entryid'); } } if (this.currentEntryId === entryId) { return; } this.currentEntryId = entryId; this.currentStoreEntryId = storeEntryId; model.setConfig(model.columns, false); }, /** * Called when {@link Ext.grid.GridPanel#store store} on the {@link #field} is * loading new data. At this point we must check which folder entryid is being loaded. * * @param {Zarafa.core.data.MAPIStore} store The store which being loaded. * @param {Ext.data.Record[]} records The records which have been loaded from the store * @param {Object} options The loading options that were specified (see {@link Ext.data.Store#load load} for details) * @private */ onStoreLoad : function(store, records, options) { /* * if action type is updatelist and grid has dummy row which was warped * with loading mask, then remove that dummy row from grid. also don't * show the loading mask on whole grid. */ if(options && (options.actionType === Zarafa.core.Actions['updatelist'])) { if(this.isLoading){ this.getView().removeGridRowLoadMask(); this.isLoading = false; } return; } }, /** * Event handler for the {@link #beforeconfigchange} event which is fired at the start of * {@link Zarafa.common.ui.grid.ColumnModel#setConfig}. * * @param {Ext.gridColumnModel} columnModel The model which is being configured * @param {Object} config The configuration object * @private */ onBeforeConfigChange : Ext.emptyFn, /** * Called when {@link Ext.grid.ColumnModel#setConfig} has completed the configuration changes. * * @param {Ext.gridColumnModel} columnModel The model which is being configured * @private */ onConfigChange : Ext.emptyFn, /** * Event handler for the selectionchange event of the grid's selectionmodel. * Will set the class 'zarafa-multiselection' on selected rows when more than * one row is selected. This class can then be used to not show the * 'add categories' button when hovering. * * @param {Ext.grid.RowSelectionModel} selectionModel The selection model of this grid */ onGridSelectionChange : function(selectionModel) { var view = this.getView(); var store = this.getStore(); // First remove the multiple selection class from all rows for ( var i=0; i<store.getCount(); i++ ){ view.removeRowClass(i, 'zarafa-multiselection'); } var selection = selectionModel.getSelections(); if ( selection.length > 1 ){ // Multiple rows are selected. Make sure they all have the multiple selection class. Ext.each(selection, function(record){ view.addRowClass(store.indexOf(record), 'zarafa-multiselection'); }, this); } }, /** * Called to get grid's drag proxy text. Opposite to the superclass, * this will not return {@link Ext.grid.GridPanel#ddText}, * but will use 'ngettext' to return a properly formatted plural sentence. * @return {String} The text */ getDragDropText : function() { var count = this.selModel.getCount(); return String.format(ngettext('{0} selected row', '{0} selected rows', count), count); }, /** * Obtain the path in which the {@link #getState state} must be saved. * This option is only used when the {@link Zarafa.core.data.SettingsStateProvider SettingsStateProvider} is * used in the {@link Ext.state.Manager}. This returns {@link #statefulName} if provided, or else generates * a custom name. * @return {String} The unique name for this component by which the {@link #getState state} must be saved. */ getStateName : function() { var options = this.store.lastOptions; var folder; if (options && options.folder) { folder = options.folder; if (Array.isArray(folder)) { folder = folder[0]; } } return container.getHierarchyStore().getStateName(folder, 'list'); }, /** * When {@link #stateful} the State object which should be saved into the * {@link Ext.state.Manager}. * @return {Object} The state object * @protected */ getState : function() { var state = Zarafa.common.ui.grid.GridPanel.superclass.getState.call(this); // Sorting is handled by the ContextModel rather the by the grid, // hence we remove the sorting information from this location. var wrap = { sort : state.sort }; delete state.sort; wrap[this.getColumnModel().name] = state; return wrap; }, /** * Apply the given state to this object activating the properties which were previously * saved in {@link Ext.state.Manager}. * @param {Object} state The state object * @protected */ applyState : function(state) { if (state) { // Sorting is handled by the ContextModel rather then by the grid, // if the unwrap object contains 'sort' then this function would trigger // a reload of the store which is not what we need. var unwrap = { sort : state.sort }; delete state.sort; Ext.apply(unwrap, state[this.getColumnModel().name]); Zarafa.common.ui.grid.GridPanel.superclass.applyState.call(this, unwrap); } } }); Ext.reg('zarafa.gridpanel', Zarafa.common.ui.grid.GridPanel);