/**
* This class is the base class for both {@link Ext.tree.Panel TreePanel} and
* {@link Ext.grid.Panel GridPanel}.
*
* TablePanel aggregates:
*
* - a Selection Model
* - a View
* - a Store
* - Ext.grid.header.Container
*
* @mixins Ext.grid.locking.Lockable
*/
Ext.define('Ext.panel.Table', {
extend: 'Ext.panel.Panel',
xtype: 'tablepanel',
requires: [
'Ext.layout.container.Fit'
],
uses: [
'Ext.selection.RowModel',
'Ext.selection.CellModel',
'Ext.selection.CheckboxModel',
'Ext.grid.plugin.BufferedRenderer',
'Ext.grid.header.Container',
'Ext.grid.locking.Lockable',
'Ext.grid.NavigationModel',
'Ext.grid.RowContext',
'Ext.grid.CellContext'
],
extraBaseCls: Ext.baseCSSPrefix + 'grid',
extraBodyCls: Ext.baseCSSPrefix + 'grid-body',
actionableModeCls: Ext.baseCSSPrefix + 'grid-actionable',
noHeaderBordersCls: Ext.baseCSSPrefix + 'no-header-borders',
/**
* @property defaultBindProperty
* @inheritdoc
*/
defaultBindProperty: 'store',
/**
* @cfg layout
* @inheritdoc
*/
layout: 'fit',
manageLayoutScroll: false,
/**
* @property ariaRole
* @inheritdoc
*/
ariaRole: 'presentation',
config: {
/**
* @cfg {Ext.grid.CellContext/Ext.data.Model/Number} focused
* The focused cell, model or index. Typically used with {@link #bind binding}.
*
* If bound to a record (such as a selection), the first cell will be focused.
*/
focused: null,
/**
* @cfg {Boolean} headerBorders
* To show no borders around grid headers, configure this as `false`.
*/
headerBorders: true,
/**
* @cfg {Boolean} hideHeaders
* By default, visibility of headers is managed automatically based upon
* whether there is textual content to display.
* This configuration is only necessary if you want to disable automatic
* header visibility management.
*
* If no columns have a {@link Ext.grid.column.Column#title text} config
* (for example in the case of a {@link Ext.tree.Panel TreePanel} with no
* columns specified), and no columns have
* {@link Ext.grid.column.Column#columns child columns} then headers are hidden.
*
* If this status changes - if the column set ever goes from none having
* text, to one having text or vice versa), then the visibility of headers
* will be recalculated.
*
* Configure as `true` to hide column headers. Configure as `false` to show
* column headers even if none of them have text.
*
*/
hideHeaders: null
},
/**
* @cfg publishes
* @inheritdoc
*/
publishes: ['selection'],
/**
* @cfg twoWayBindable
* @inheritdoc
*/
twoWayBindable: ['selection'],
/**
* @cfg {Ext.data.Model} selection
* The selected model. Typically used with {@link #bind binding}.
*/
selection: null,
/**
* @cfg {Boolean} autoLoad
* Use `true` to load the store as soon as this component is fully constructed. It is
* best to initiate the store load this way to allow this component and potentially
* its plugins (such as `{@link Ext.grid.filters.Filters}`) to be ready to load.
*/
autoLoad: false,
/**
* @cfg {Boolean} variableRowHeight
* @deprecated 5.0.0 Use {@link Ext.grid.column.Column#variableRowHeight} instead.
* Configure as `true` if the row heights are not all the same height as the first row.
*/
variableRowHeight: false,
/**
* @cfg {Number} numFromEdge
* This configures the zone which causes new rows to be appended to the view. As soon
* as the edge of the rendered grid is this number of rows from the edge of the viewport,
* the view is moved.
*/
numFromEdge: 2,
/**
* @cfg {Number} trailingBufferZone
* TableViews are buffer rendered in 5.x and above which means that only the visible subset
* of data rows are rendered into the DOM. These are removed and added as scrolling demands.
*
* This configures the number of extra rows to render on the trailing side of scrolling
* **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
*/
trailingBufferZone: 10,
/**
* @cfg {Number} [leadingBufferZone]
* TableViews are buffer rendered in 5.x and above which means that only the visible subset
* of data rows are rendered into the DOM. These are removed and added as scrolling demands.
*
* This configures the number of extra rows to render on the leading side of scrolling
* **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
*/
leadingBufferZone: 20,
/**
* @property {Boolean} hasView
* True to indicate that a view has been injected into the panel.
*/
hasView: false,
/**
* @property items
* @hide
*/
/**
* @cfg {String} viewType
* An xtype of view to use. This is automatically set to 'tableview' by
* {@link Ext.grid.Panel Grid} and to 'treeview' by {@link Ext.tree.Panel Tree}.
* @protected
*/
viewType: null,
/**
* @cfg {Object} viewConfig
* A config object that will be applied to the grid's UI view. Any of the config options
* available for {@link Ext.view.Table} can be specified here. This option is ignored
* if {@link #view} is specified.
*/
/**
* @cfg {String/Object} rowViewModel
* The type or a config object specifying the type of the ViewModel to instantiate when creating
* ViewModels for records to which {@link Ext.grid.column.Widget widgets in widget columns},
* and widgets in a {@link Ext.grid.plugin.RowWidget RowWidget} row bind.
*/
/**
* @cfg {Ext.view.Table} view
* The {@link Ext.view.Table} used by the grid. Use {@link #viewConfig} to just supply
* some config options to view (instead of creating an entire View instance).
*/
/**
* @cfg {String} [selType]
* An xtype of selection model to use. This is used to create selection model if just
* a config object or nothing at all given in {@link #selModel} config.
*
* @deprecated 5.1.0 Use the {@link #selModel}'s `type` property. Or, if no other
* configs are required, use the string form of selModel.
*/
/**
* @cfg {Ext.selection.Model/Object/String} [selModel=rowmodel]
* A {@link Ext.selection.Model selection model} instance or config object, or the selection
* model class's alias string.
*
* In latter case its `type` property determines to which type of selection model this config
* is applied.
*/
/**
* @cfg {Boolean} [multiSelect=false]
* True to enable 'MULTI' selection mode on selection model.
* @deprecated 4.1.1 Use {@link Ext.selection.Model#mode} 'MULTI' instead.
*/
/**
* @cfg {Boolean} [simpleSelect=false]
* True to enable 'SIMPLE' selection mode on selection model.
* @deprecated 4.1.1 Use {@link Ext.selection.Model#mode} 'SIMPLE' instead.
*/
/**
* @cfg {Ext.data.Store/String/Object} store (required)
* The data source to which the grid / tree is bound. Acceptable values for this
* property are:
*
* - **any {@link Ext.data.Store Store} class / subclass**
* - **an {@link Ext.data.Store#storeId ID of a store}**
* - **a {@link Ext.data.Store Store} config object**. When passing a config you can
* specify the store type by alias. Passing a config object with a store type will
* dynamically create a new store of that type when the grid / tree is instantiated.
*
* For example:
*
* Ext.define('MyApp.store.Customers', {
* extend: 'Ext.data.Store',
* alias: 'store.customerstore',
* fields: ['name']
* });
*
* Ext.create({
* xtype: 'gridpanel',
* renderTo: document.body,
* store: {
* type: 'customerstore',
* data: [{
* name: 'Foo'
* }]
* },
* columns: [{
* text: 'Name',
* dataIndex: 'name'
* }]
* });
*/
/**
* @cfg {String/Boolean} scroll
* Scrollers configuration. Valid values are 'both', 'horizontal' or 'vertical'.
* True implies 'both'. False implies 'none'.
* @deprecated 5.1.0 Use {@link #scrollable} instead
*/
/**
* @cfg {Boolean} [reserveScrollbar=false]
* Set this to true to **always** leave a scrollbar sized space at the end of the grid content
* when fitting content into the width of the grid.
*
* If the grid's record count fluctuates enough to hide and show the scrollbar regularly,
* this setting avoids the multiple layouts associated with switching from scrollbar present
* to scrollbar not present.
*/
/**
* @cfg {Ext.grid.column.Column[]/Object} columns
* An array of {@link Ext.grid.column.Column column} definition objects which define all columns
* that appear in this grid. Each column definition provides the header text for the column,
* and a definition of where the data for that column comes from.
*
* This can also be a configuration object for a
* {@link Ext.grid.header.Container HeaderContainer} which may override certain default
* configurations if necessary. For example, the special layout may be overridden to use
* a simpler layout, or one can set default values shared by all columns:
*
* columns: {
* items: [
* {
* text: "Column A",
* dataIndex: "field_A"
* }, {
* text: "Column B",
* dataIndex: "field_B"
* },
* ...
* ],
* defaults: {
* flex: 1
* }
* }
*/
/**
* @cfg {Boolean} forceFit
* True to force the columns to fit into the available width. Headers are first sized according
* to configuration, whether that be a specific width, or flex. Then they are all proportionally
* changed in width so that the entire content width is used. For more accurate control,
* it is more optimal to specify a flex setting on the columns that are to be stretched
* and explicit widths on columns that are not.
*/
/**
* @cfg {Ext.grid.feature.Feature[]/Object[]/Ext.enums.Feature[]} features
* An array of grid Features to be added to this grid. Can also be just a single feature
* instead of array.
*
* Features config behaves much like {@link #plugins}.
* A feature can be added by either directly referencing the instance:
*
* features: [
* Ext.create('Ext.grid.feature.GroupingSummary', {groupHeaderTpl: 'Subject: {name}'})
* ],
*
* By using config object with ftype:
*
* features: [{ftype: 'groupingsummary', groupHeaderTpl: 'Subject: {name}'}],
*
* Or with just a ftype:
*
* features: ['grouping', 'groupingsummary'],
*
* See {@link Ext.enums.Feature} for list of all ftypes.
*/
/**
* @cfg {Boolean} deferRowRender
* Configure as `true` to enable deferred row rendering.
*
* This allows the View to execute a refresh quickly, with the update of the row structure
* deferred so that layouts with GridPanels appear, and lay out more quickly.
*/
deferRowRender: false,
/**
* @cfg {Boolean} sortableColumns
* False to disable column sorting via clicking the header and via the Sorting menu items.
*/
sortableColumns: true,
/**
* @cfg {Boolean} multiColumnSort
* Configure as `true` to have columns remember their sorted state after other columns have been
* clicked upon to sort.
*
* As subsequent columns are clicked upon, they become the new primary sort key.
*
* The maximum number of sorters allowed in a Store is configurable via its underlying data
* collection. See {@link Ext.util.Collection#multiSortLimit}
*/
multiColumnSort: false,
/**
* @cfg {Boolean} enableLocking
* Configure as `true` to enable locking support for this grid. Alternatively, locking will also
* be automatically enabled if any of the columns in the {@link #columns columns} configuration
* contain a {@link Ext.grid.column.Column#locked locked} config option.
*
* A locking grid is processed in a special way. The configuration options are cloned and *two*
* grids are created to be the locked (left) side and the normal (right) side. This Panel
* becomes merely a {@link Ext.container.Container container} which arranges both in an
* {@link Ext.layout.container.HBox HBox} layout.
*
* {@link #plugins Plugins} may be targeted at either locked, or unlocked grid, or, both,
* in which case the plugin is cloned and used on both sides.
*
* Plugins may also be targeted at the containing locking Panel.
*
* This is configured by specifying a `lockableScope` property in your plugin which may have
* the following values:
*
* * `"both"` (the default) - The plugin is added to both grids
* * `"top"` - The plugin is added to the containing Panel
* * `"locked"` - The plugin is added to the locked (left) grid
* * `"normal"` - The plugin is added to the normal (right) grid
*
* If `both` is specified, then each copy of the plugin gains a property `lockingPartner`
* which references its sibling on the other side so that they can synchronize operations
* if necessary.
*
* {@link #features Features} may also be configured with `lockableScope` and may target
* the locked grid, the normal grid or both grids. Features also get a `lockingPartner`
* reference injected.
*/
enableLocking: false,
/**
* @private
* Used to determine where to go down to find views
* this is here to support locking.
*/
scrollerOwner: true,
/**
* @cfg {Boolean} enableColumnMove
* False to disable column dragging within this grid.
*/
enableColumnMove: true,
/**
* @cfg {Boolean} sealedColumns
* True to constrain column dragging so that a column cannot be dragged in or out of it's
* current group. Only relevant while {@link #enableColumnMove} is enabled.
*/
sealedColumns: false,
/**
* @cfg {Boolean} enableColumnResize
* False to disable column resizing within this grid.
*/
enableColumnResize: true,
/**
* @cfg {Boolean} [enableColumnHide=true]
* False to disable column hiding within this grid.
*/
/**
* @cfg {Boolean} columnLines
* Adds column line styling
*/
columnLines: false,
/**
* @cfg {Boolean} rowLines
* Adds row line styling
*/
rowLines: true,
/**
* @cfg {Boolean} [disableSelection=false]
* True to disable selection model.
*/
/**
* @cfg {String} emptyText Default text (HTML tags are accepted) to display in the
* Panel body when the Store is empty. When specified, and the Store is empty, the
* text will be rendered inside a DIV with the CSS class "x-grid-empty". The emptyText
* will not display until the first load of the associated store by default. If you
* want the text to be displayed prior to the first store load use the
* {@link Ext.view.Table#deferEmptyText deferEmptyText} config in the {@link #viewConfig}
* config.
*/
/**
* @cfg {Boolean} [allowDeselect=false]
* True to allow deselecting a record. This config is forwarded to
* {@link Ext.selection.Model#allowDeselect}.
*/
/**
* @cfg {Boolean} bufferedRenderer
* Buffered rendering is enabled by default.
*
* Configure as `false` to disable buffered rendering.
* See {@link Ext.grid.plugin.BufferedRenderer}.
*
* @since 5.0.0
*/
bufferedRenderer: true,
/**
* @cfg {Boolean} preciseHeight
* Set to `true` to ensure that measurements (such as locking grid's row-height synchronization)
* accurately measure rows with sub-pixel sizes. This can be an issue for some types
* of row content on browsers that support sub-pixel sizing. Note that setting this to `true`
* may cause a decrease in performance for large amounts of rendered content and therefore
* should only be used when needed.
* @since 6.5.1
*/
preciseHeight: false,
/**
* @cfg stateEvents
* @inheritdoc Ext.state.Stateful#cfg-stateEvents
* @localdoc By default the following stateEvents are added:
*
* - {@link #event-resize} - _(added by Ext.Component)_
* - {@link #event-collapse} - _(added by Ext.panel.Panel)_
* - {@link #event-expand} - _(added by Ext.panel.Panel)_
* - {@link #event-columnresize}
* - {@link #event-columnmove}
* - {@link #event-columnhide}
* - {@link #event-columnshow}
* - {@link #event-sortchange}
* - {@link #event-filterchange}
* - {@link #event-groupchange}
*/
/**
* @property {Boolean} optimizedColumnMove
* If you are writing a grid plugin or a {Ext.grid.feature.Feature Feature} which creates
* a column-based structure which needs a view refresh when columns are moved, then set
* this property in the grid.
*
* An example is the built in {@link Ext.grid.feature.AbstractSummary Summary} Feature.
* This creates summary rows, and the summary columns must be in the same order
* as the data columns. This plugin sets the `optimizedColumnMove` to `false.
*/
/**
* @property {Ext.panel.Table} ownerGrid
* A reference to the top-level owning grid component.
*
* This is a reference to this GridPanel if this GridPanel is not part of a locked grid
* arrangement.
* @readonly
* @private
* @since 5.0.0
*/
ownerGrid: null,
colLinesCls: Ext.baseCSSPrefix + 'grid-with-col-lines',
rowLinesCls: Ext.baseCSSPrefix + 'grid-with-row-lines',
noRowLinesCls: Ext.baseCSSPrefix + 'grid-no-row-lines',
hiddenHeaderCtCls: Ext.baseCSSPrefix + 'grid-header-ct-hidden',
hiddenHeaderCls: Ext.baseCSSPrefix + 'grid-header-hidden',
resizeMarkerCls: Ext.baseCSSPrefix + 'grid-resize-marker',
emptyCls: Ext.baseCSSPrefix + 'grid-empty',
// The TablePanel claims to be focusable, but it does not place a tabIndex
// on any of its elements.
// Its focus implementation delegates to its view. TableViews are focusable.
/**
* @property focusable
* @inheritdoc
*/
focusable: true,
/**
* @event viewready
* Fires when the grid view is available (use this for selecting a default row).
* @param {Ext.panel.Table} this
*/
constructor: function(config) {
var me = this,
topGrid = config && config.ownerGrid,
store;
me.ownerGrid = topGrid || me;
/**
* @property {Array} actionables An array of objects which register themselves
* with a grid panel using {@link #registerActionable} which are consulted upon entry
* into actionable mode.
*
* These must implement the following methods:
*
* - activateCell Called when actionable mode is requested upon a cell.
* A {@link Ext.grid.CellContext CellContext} object is passed. If that cell
* is actionable by the terms of the callee, the callee should return `true` if it
* ascertains that the cell is actionable, and that it now contains focusable elements
* which may be tabbed to.
* - activateRow Called when the user enters actionable mode in a row. The row DOM
* is passed. Actionables should take any action they need to prime the row for cell
* activation which happens as users TAB from cell to cell.
* @readonly
*/
// One shared array when there's a lockable at the top
me.actionables = topGrid ? topGrid.actionables : [];
me.callParent([config]);
store = me.store;
// Any further changes become stateful.
store.trackStateChanges = true;
if (me.autoLoad) {
// Note: if there is a store bound by a VM, we (might) do the load in #setStore.
if (!store.isEmptyStore) {
store.load();
}
}
},
/**
*
* @param {Object} actionable An object which has an interest in the implementation
* of actionable mode in this grid.
*
* An actionable object may be a Plugin which upon activation injects tabbable elements
* or Components into a grid row.
*/
registerActionable: function(actionable) {
// If a lockableScope: 'both' plugin/feature registers on each side,
// only include it in the actionables once.
Ext.Array.include(this.actionables, actionable);
},
initComponent: function() {
//<debug>
if (this.verticalScroller) {
Ext.raise("The verticalScroller config is not supported.");
}
if (!this.viewType) {
Ext.raise("You must specify a viewType config.");
}
if (this.headers) {
Ext.raise("The headers config is not supported. Please specify columns instead.");
}
//</debug>
// eslint-disable-next-line vars-on-top
var me = this,
columns = me.columns || me.colModel || [],
selection = me.selection,
store, view, i, len, bufferedRenderer, headerCtCfg, headerCt;
if (selection) {
me.selection = null;
me.setSelection(selection);
}
// Look up the configured Store. If none configured, use the fieldless, empty Store
// defined in Ext.data.Store. If store configuration is present with no storeId
// we will be creating a new Store instance unique to this Panel, and we should
// destroy it as well.
store = me.store;
if (store && Ext.isObject(store) && !store.isStore && !store.storeId) {
store = Ext.apply({
autoDestroy: true
}, store);
}
store = me.store = Ext.data.StoreManager.lookup(store || 'ext-empty-store');
me.enableLocking = me.enableLocking || me.hasLockedColumns(columns);
// Construct the plugins now rather than in the constructor of AbstractComponent
// because the component may have a subclass that has overridden initComponent
// and defined plugins in it. For plugins like RowExpander that rely upon a grid feature,
// this is a problem because the view needs to know about all its features before it's
// constructed. Constructing the plugins now ensures that plugins defined in the instance
// config or in initComponent are all constructed before the view.
// See EXTJSIV-11927.
//
// Note that any components that do not inherit from this class will still have
// their plugins constructed in AbstractComponent#initComponent.
if (me.plugins) {
me.plugins = me.constructPlugins();
}
// Add the row/column line classes to the body element so that the settings are not
// inherited by docked grids (https://sencha.jira.com/browse/EXTJSIV-9263).
if (me.columnLines) {
me.addBodyCls(me.colLinesCls);
}
me.addBodyCls(me.rowLines ? me.rowLinesCls : me.noRowLinesCls);
me.addBodyCls(me.extraBodyCls);
// If any of the Column objects contain a locked property, and are not processed,
// this is a lockable TablePanel, a special view will be injected by the
// Ext.grid.locking.Lockable mixin, so no processing of.
if (me.enableLocking) {
// Only first invocation mixes Lockable into the TablePanel class
if (!me.mixins.lockable) {
me.self.mixin('lockable', Ext.grid.locking.Lockable);
}
me.injectLockable();
}
// Not lockable - create the HeaderContainer
else {
// It's a fully instantiated HeaderContainer
if (columns.isRootHeader) {
me.headerCt = headerCt = columns;
headerCt.grid = me;
headerCt.forceFit = !!me.forceFit;
columns = [];
// If it's an instance then the column managers were already created and bound
// to the headerCt.
me.columnManager = headerCt.columnManager;
me.visibleColumnManager = headerCt.visibleColumnManager;
}
// It's an array of Column definitions, or a config object of a HeaderContainer
else {
headerCtCfg = {
grid: me,
$initParent: me,
forceFit: me.forceFit,
sortable: me.sortableColumns,
enableColumnMove: me.enableColumnMove,
enableColumnResize: me.enableColumnResize,
columnLines: me.columnLines,
sealed: me.sealedColumns
};
if (Ext.isObject(columns)) {
Ext.apply(headerCtCfg, columns);
columns = columns.items;
delete headerCtCfg.items;
}
me.headerCt = headerCt = new Ext.grid.header.Container(headerCtCfg);
}
if (Ext.isDefined(me.enableColumnHide)) {
headerCt.enableColumnHide = me.enableColumnHide;
}
}
me.scrollTask = new Ext.util.DelayedTask(me.syncHorizontalScroll, me);
me.cls = (me.cls || '') + (' ' + me.extraBaseCls);
// autoScroll is not a valid configuration
delete me.autoScroll;
bufferedRenderer = me.plugins && Ext.Array.findBy(me.plugins, function(p) {
return p.isBufferedRenderer;
});
// If we find one in the plugins, just use that.
if (bufferedRenderer) {
me.bufferedRenderer = bufferedRenderer;
}
// If this TablePanel is lockable (Either configured lockable, or any of the defined
// columns has a 'locked' property) then a special lockable view containing 2 side-by-side
// grids will have been injected so we do not need to set up any UI.
if (!me.hasView) {
// If the store is paging blocks of the dataset in, then it can only be sorted remotely.
// And if the store is not remoteSort, then we cannot sort it at all.
if (store.isBufferedStore && !store.getRemoteSort()) {
for (i = 0, len = columns.length; i < len; i++) {
columns[i].sortable = false;
}
}
me.relayHeaderCtEvents(headerCt);
me.features = me.features || [];
if (!Ext.isArray(me.features)) {
me.features = [me.features];
}
me.viewConfig = me.viewConfig || {};
// AbstractDataView will look up a Store configured as an object
// getView converts viewConfig into a View instance
view = me.getView();
me.items = [view];
me.hasView = true;
// Attach this Panel to the Store
me.bindStore(store, true);
me.mon(view, {
viewready: me.onViewReady,
refresh: me.onRestoreHorzScroll,
scope: me
});
}
// Whatever kind of View we have, be it a TableView, or a LockingView, we are interested
// in the selection model
me.selModel = me.view.getSelectionModel();
// We update the bound selection whenever the selectionchange event fires.
// Even a CellModel, or a SpreadsheetModel in cell selection mode can yield
// the *records* that are selected, and it is the first record which is published
// to the selection property.
me.selModel.on({
scope: me,
lastselectedchanged: me.updateBindSelection,
selectionchange: me.updateBindSelection
});
// Relay events from the View whether it be a LockingView, or a regular GridView
me.relayEvents(me.view, [
/**
* @event beforeitemlongpress
* @inheritdoc Ext.view.View#beforeitemlongpress
*/
'beforeitemlongpress',
/**
* @event beforeitemmousedown
* @inheritdoc Ext.view.View#beforeitemmousedown
*/
'beforeitemmousedown',
/**
* @event beforeitemmouseup
* @inheritdoc Ext.view.View#beforeitemmouseup
*/
'beforeitemmouseup',
/**
* @event beforeitemmouseenter
* @inheritdoc Ext.view.View#beforeitemmouseenter
*/
'beforeitemmouseenter',
/**
* @event beforeitemmouseleave
* @inheritdoc Ext.view.View#beforeitemmouseleave
*/
'beforeitemmouseleave',
/**
* @event beforeitemclick
* @inheritdoc Ext.view.View#beforeitemclick
*/
'beforeitemclick',
/**
* @event beforeitemdblclick
* @inheritdoc Ext.view.View#beforeitemdblclick
*/
'beforeitemdblclick',
/**
* @event beforeitemcontextmenu
* @inheritdoc Ext.view.View#beforeitemcontextmenu
*/
'beforeitemcontextmenu',
/**
* @event itemlongpress
* @inheritdoc Ext.view.View#itemlongpress
*/
'itemlongpress',
/**
* @event itemmousedown
* @inheritdoc Ext.view.View#itemmousedown
*/
'itemmousedown',
/**
* @event itemmouseup
* @inheritdoc Ext.view.View#itemmouseup
*/
'itemmouseup',
/**
* @event itemmouseenter
* @inheritdoc Ext.view.View#itemmouseenter
*/
'itemmouseenter',
/**
* @event itemmouseleave
* @inheritdoc Ext.view.View#itemmouseleave
*/
'itemmouseleave',
/**
* @event itemclick
* @inheritdoc Ext.view.View#itemclick
*/
'itemclick',
/**
* @event itemdblclick
* @inheritdoc Ext.view.View#itemdblclick
*/
'itemdblclick',
/**
* @event itemcontextmenu
* @inheritdoc Ext.view.View#itemcontextmenu
*/
'itemcontextmenu',
/**
* @event beforecellclick
* @inheritdoc Ext.view.Table#beforecellclick
*/
'beforecellclick',
/**
* @event cellclick
* @inheritdoc Ext.view.Table#cellclick
*/
'cellclick',
/**
* @event beforecelldblclick
* @inheritdoc Ext.view.Table#beforecelldblclick
*/
'beforecelldblclick',
/**
* @event celldblclick
* @inheritdoc Ext.view.Table#celldblclick
*/
'celldblclick',
/**
* @event beforecellcontextmenu
* @inheritdoc Ext.view.Table#beforecellcontextmenu
*/
'beforecellcontextmenu',
/**
* @event cellcontextmenu
* @inheritdoc Ext.view.Table#cellcontextmenu
*/
'cellcontextmenu',
/**
* @event beforecellmousedown
* @inheritdoc Ext.view.Table#beforecellmousedown
*/
'beforecellmousedown',
/**
* @event cellmousedown
* @inheritdoc Ext.view.Table#cellmousedown
*/
'cellmousedown',
/**
* @event beforecellmouseup
* @inheritdoc Ext.view.Table#beforecellmouseup
*/
'beforecellmouseup',
/**
* @event cellmouseup
* @inheritdoc Ext.view.Table#cellmouseup
*/
'cellmouseup',
/**
* @event beforecellkeydown
* @inheritdoc Ext.view.Table#beforecellkeydown
*/
'beforecellkeydown',
/**
* @event cellkeydown
* @inheritdoc Ext.view.Table#cellkeydown
*/
'cellkeydown',
/**
* @event rowclick
* @inheritdoc Ext.view.Table#rowclick
*/
'rowclick',
/**
* @event rowdblclick
* @inheritdoc Ext.view.Table#rowdblclick
*/
'rowdblclick',
/**
* @event rowcontextmenu
* @inheritdoc Ext.view.Table#rowcontextmenu
*/
'rowcontextmenu',
/**
* @event rowmousedown
* @inheritdoc Ext.view.Table#rowmousedown
*/
'rowmousedown',
/**
* @event rowmouseup
* @inheritdoc Ext.view.Table#rowmouseup
*/
'rowmouseup',
/**
* @event rowkeydown
* @inheritdoc Ext.view.Table#rowkeydown
*/
'rowkeydown',
/**
* @event beforeitemkeydown
* @inheritdoc Ext.view.View#event!beforeitemkeydown
*/
'beforeitemkeydown',
/**
* @event itemkeydown
* @inheritdoc Ext.view.View#event!itemkeydown
*/
'itemkeydown',
/**
* @event beforeitemkeyup
* @inheritdoc Ext.view.View#event!beforeitemkeyup
*/
'beforeitemkeyup',
/**
* @event itemkeyup
* @inheritdoc Ext.view.View#event!itemkeyup
*/
'itemkeyup',
/**
* @event beforeitemkeypress
* @inheritdoc Ext.view.View#event!beforeitemkeypress
*/
'beforeitemkeypress',
/**
* @event itemkeypress
* @inheritdoc Ext.view.View#event!itemkeypress
*/
'itemkeypress',
/**
* @event beforecontainermousedown
* @inheritdoc Ext.view.View#beforecontainermousedown
*/
'beforecontainermousedown',
/**
* @event beforecontainermouseup
* @inheritdoc Ext.view.View#beforecontainermouseup
*/
'beforecontainermouseup',
/**
* @event beforecontainermouseover
* @inheritdoc Ext.view.View#beforecontainermouseover
*/
'beforecontainermouseover',
/**
* @event beforecontainermouseout
* @inheritdoc Ext.view.View#beforecontainermouseout
*/
'beforecontainermouseout',
/**
* @event beforecontainerclick
* @inheritdoc Ext.view.View#beforecontainerclick
*/
'beforecontainerclick',
/**
* @event beforecontainerdblclick
* @inheritdoc Ext.view.View#beforecontainerdblclick
*/
'beforecontainerdblclick',
/**
* @event beforecontainercontextmenu
* @inheritdoc Ext.view.View#beforecontainercontextmenu
*/
'beforecontainercontextmenu',
/**
* @event beforecontainerkeydown
* @inheritdoc Ext.view.View#beforecontainerkeydown
*/
'beforecontainerkeydown',
/**
* @event beforecontainerkeyup
* @inheritdoc Ext.view.View#beforecontainerkeyup
*/
'beforecontainerkeyup',
/**
* @event beforecontainerkeypress
* @inheritdoc Ext.view.View#beforecontainerkeypress
*/
'beforecontainerkeypress',
/**
* @event containermouseup
* @inheritdoc Ext.view.View#containermouseup
*/
'containermouseup',
/**
* @event containermousedown
* @inheritdoc Ext.view.View#containermousedown
*/
'containermousedown',
/**
* @event containermouseover
* @inheritdoc Ext.view.View#containermouseover
*/
'containermouseover',
/**
* @event containermouseout
* @inheritdoc Ext.view.View#containermouseout
*/
'containermouseout',
/**
* @event containerclick
* @inheritdoc Ext.view.View#containerclick
*/
'containerclick',
/**
* @event containerdblclick
* @inheritdoc Ext.view.View#containerdblclick
*/
'containerdblclick',
/**
* @event containercontextmenu
* @inheritdoc Ext.view.View#containercontextmenu
*/
'containercontextmenu',
/**
* @event containerkeydown
* @inheritdoc Ext.view.View#containerkeydown
*/
'containerkeydown',
/**
* @event containerkeyup
* @inheritdoc Ext.view.View#containerkeyup
*/
'containerkeyup',
/**
* @event containerkeypress
* @inheritdoc Ext.view.View#containerkeypress
*/
'containerkeypress',
/**
* @event beforeselect
* @inheritdoc Ext.selection.RowModel#beforeselect
*/
'beforeselect',
/**
* @event select
* @inheritdoc Ext.selection.RowModel#select
*/
'select',
/**
* @event beforedeselect
* @inheritdoc Ext.selection.RowModel#beforedeselect
*/
'beforedeselect',
/**
* @event deselect
* @inheritdoc Ext.selection.RowModel#deselect
*/
'deselect',
/**
* @event beforerowexit
* @inheritdoc Ext.view.Table#beforerowexit
*/
'beforerowexit'
]);
// Only relay the event if it's not SpreadsheetModel.
// SpreadsheetModel fires it directly through the Panel.
if (!me.selModel.isSpreadsheetModel) {
me.relayEvents(me.view, [
/**
* @event selectionchange
* @inheritdoc Ext.selection.Model#selectionchange
*/
'selectionchange'
]);
}
// If we have our own headerCt (not gone through injectLockable), then add it
// to our docked items and then add the columns. In this way, the columns
// will immediately be able to interrogate their environment through getView
// and getRootHeaderCt
if (headerCt) {
headerCt.view = me.view;
(me.dockedItems = Ext.Array.from(me.dockedItems, true)).unshift(headerCt);
headerCt.add(columns);
}
// Maintain backward compatibiliy by providing the initial leaf column set as a property.
me.columns = me.headerCt.getGridColumns();
me.callParent();
me.syncHeaderVisibility();
if (me.enableLocking) {
me.afterInjectLockable();
}
me.addStateEvents([
'columnresize',
'columnmove',
'columnhide',
'columnshow',
'sortchange',
'filteractivate',
'filterdeactivate',
'filterchange',
'groupchange'
]);
// rowBody feature events
/**
* @event beforerowbodymousedown
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodymousedown
*/
/**
* @event beforerowbodymouseup
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodymouseup
*/
/**
* @event beforerowbodyclick
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodyclick
*/
/**
* @event beforerowbodydblclick
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodydblclick
*/
/**
* @event beforerowbodycontextmenu
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodycontextmenu
*/
/**
* @event beforerowbodylongpress
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodylongpress
*/
/**
* @event beforerowbodykeydown
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodykeydown
*/
/**
* @event beforerowbodykeyup
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodykeyup
*/
/**
* @event beforerowbodykeypress
* @preventable
* @inheritdoc Ext.view.Table#event-beforerowbodykeypress
*/
/**
* @event rowbodymousedown
* @inheritdoc Ext.view.Table#event-rowbodymousedown
*/
/**
* @event rowbodymouseup
* @inheritdoc Ext.view.Table#event-rowbodymouseup
*/
/**
* @event rowbodyclick
* @inheritdoc Ext.view.Table#event-rowbodyclick
*/
/**
* @event rowbodydblclick
* @inheritdoc Ext.view.Table#event-rowbodydblclick
*/
/**
* @event rowbodycontextmenu
* @inheritdoc Ext.view.Table#event-rowbodycontextmenu
*/
/**
* @event rowbodylongpress
* @inheritdoc Ext.view.Table#event-rowbodylongpress
*/
/**
* @event rowbodykeydown
* @inheritdoc Ext.view.Table#event-rowbodykeydown
*/
/**
* @event rowbodykeyup
* @inheritdoc Ext.view.Table#event-rowbodykeyup
*/
/**
* @event rowbodykeypress
* @inheritdoc Ext.view.Table#event-rowbodykeypress
*/
},
updateHideHeaders: function(hideHeaders) {
// Must only update the visibility after all configuration is finished.
// initComponent calls syncHeaderVisibility
if (!this.isConfiguring) {
this.syncHeaderVisibility();
}
},
beforeRender: function() {
var me = this,
bufferedRenderer = me.bufferedRenderer,
ariaAttr;
// If this is the topmost container of a lockable assembly, add the special class body
if (me.lockable) {
me.getProtoBody().addCls(me.lockingBodyCls);
}
// Don't create a buffered renderer for a locked grid.
else {
// If we're auto heighting, we can't buffered render, so don't create it
if (bufferedRenderer && me.getSizeModel().height.auto) {
//<debug>
if (bufferedRenderer.isBufferedRenderer) {
Ext.raise('Cannot use buffered rendering with auto height');
}
//</debug>
me.bufferedRenderer = bufferedRenderer = false;
}
if (bufferedRenderer && !bufferedRenderer.isBufferedRenderer) {
// Create a BufferedRenderer as a plugin if we have not already configured with one.
bufferedRenderer = {
xclass: 'Ext.grid.plugin.BufferedRenderer'
};
// eslint-disable-next-line max-len
Ext.copy(bufferedRenderer, me, 'variableRowHeight,numFromEdge,trailingBufferZone,leadingBufferZone,scrollToLoadBuffer', true);
me.bufferedRenderer = me.addPlugin(bufferedRenderer);
}
ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
ariaAttr['aria-readonly'] = !me.isEditable;
ariaAttr['aria-multiselectable'] = me.selModel.selectionMode !== 'SINGLE';
}
me.callParent(arguments);
},
beforeLayout: function() {
var lockable = this.mixins.lockable;
if (lockable) {
lockable.beforeLayout.call(this);
}
this.callParent();
},
onHide: function(animateTarget, cb, scope) {
this.getView().onOwnerGridHide();
this.callParent([animateTarget, cb, scope]);
},
onShow: function() {
this.callParent();
this.getView().onOwnerGridShow();
},
/**
* Gets the {@link Ext.grid.header.Container headercontainer} for this grid / tree.
* @return {Ext.grid.header.Container} headercontainer
*
* **Note:** While a locked grid / tree will return an instance of
* {@link Ext.grid.locking.HeaderContainer} you will code to the
* {@link Ext.grid.header.Container} API.
*/
getHeaderContainer: function() {
return this.getView().getHeaderCt();
},
/**
* @method getColumns
* @inheritdoc Ext.grid.header.Container#getGridColumns
*/
getColumns: function() {
return this.getColumnManager().getColumns();
},
/**
* @method getVisibleColumns
* @inheritdoc Ext.grid.header.Container#getVisibleGridColumns
*/
getVisibleColumns: function() {
return this.getVisibleColumnManager().getColumns();
},
getScrollable: function() {
// Lockable grids own a separate Y scroller which scrolls both grids in a single
// scrolling element.
// Regular grids return their view's scroller.
return this.scrollable || this.view.getScrollable();
},
focus: function() {
// TablePanel is not focusable, but allow a call to delegate into the view
var view = this.getView();
if (!view.isVisible(true)) {
return false;
}
view.focus();
},
/**
* Disables interaction with, and masks this grid's column headers.
*/
disableColumnHeaders: function() {
this.headerCt.disable();
},
/**
* Enables interaction with, and unmasks this grid's column headers after a call
* to {#disableColumnHeaders}.
*/
enableColumnHeaders: function() {
this.headerCt.enable();
},
/**
* @private
* Determine if there are any columns with a locked configuration option.
*/
hasLockedColumns: function(columns) {
var i, len, column;
// Fully instantiated HeaderContainer
if (columns.isRootHeader) {
columns = columns.items.items;
}
// Config object with items
else if (Ext.isObject(columns)) {
columns = columns.items;
}
for (i = 0, len = columns.length; i < len; i++) {
column = columns[i];
if (!column.processed && column.locked) {
return true;
}
}
},
relayHeaderCtEvents: function(headerCt) {
this.relayEvents(headerCt, [
/**
* @event columnresize
* @inheritdoc Ext.grid.header.Container#columnresize
*/
'columnresize',
/**
* @event columnmove
* @inheritdoc Ext.grid.header.Container#columnmove
*/
'columnmove',
/**
* @event columnhide
* @inheritdoc Ext.grid.header.Container#columnhide
*/
'columnhide',
/**
* @event columnshow
* @inheritdoc Ext.grid.header.Container#columnshow
*/
'columnshow',
/**
* @event columnschanged
* @inheritdoc Ext.grid.header.Container#columnschanged
*/
'columnschanged',
/**
* @event sortchange
* @inheritdoc Ext.grid.header.Container#sortchange
*/
'sortchange',
/**
* @event headerclick
* @inheritdoc Ext.grid.header.Container#headerclick
*/
'headerclick',
/**
* @event headercontextmenu
* @inheritdoc Ext.grid.header.Container#headercontextmenu
*/
'headercontextmenu',
/**
* @event headertriggerclick
* @inheritdoc Ext.grid.header.Container#headertriggerclick
*/
'headertriggerclick'
]);
},
getState: function() {
var me = this,
state = me.callParent(),
storeState = me.store.getState();
state = me.addPropertyToState(state, 'columns', me.headerCt.getColumnsState());
if (storeState) {
state.storeState = storeState;
}
return state;
},
applyState: function(state) {
var me = this,
sorter = state.sort,
storeState = state.storeState,
store = me.store,
columns = state.columns = me.buildColumnHash(state.columns);
// Ensure superclass has applied *its* state.
// Component saves dimensions (and anchor/flex) plus collapsed state.
me.callParent([state]);
if (columns) {
// Column state restoration needs to examine store state
me.headerCt.applyColumnsState(columns, storeState);
}
if (store.isEmptyStore) {
return;
}
// Old stored sort state. Deprecated and will die out.
if (sorter) {
if (store.getRemoteSort()) {
// Pass false to prevent a sort from occurring.
store.sort({
property: sorter.property,
direction: sorter.direction,
root: sorter.root
}, null, false);
}
else {
store.sort(sorter.property, sorter.direction);
}
}
// New storeState which encapsulates groupers, sorters and filters.
else if (storeState) {
store.applyState(storeState);
}
},
buildColumnHash: function(columns) {
var len, columnState, i, result;
// Create a usable state lookup hash from which each column
// may look up its state based upon its stateId
// {
// col_name: {
// index: 0,
// width: 100,
// locked: true
// },
// col_details: {
// index: 1,
// width: 200,
// columns: {
// col_details_1: {
// index: 0,
// width: 100
// },
// col_details_2: {
// index: 1,
// width: 100
// }
// }
// },
// }
if (columns) {
result = {};
for (i = 0, len = columns.length; i < len; i++) {
columnState = columns[i];
columnState.index = i;
if (columnState.columns) {
columnState.columns = this.buildColumnHash(columnState.columns);
}
result[columnState.id] = columnState;
}
return result;
}
},
/**
* Returns the store associated with this Panel.
* @return {Ext.data.Store} The store
*/
getStore: function() {
return this.store;
},
onViewRefresh: function(view, records) {
this.onItemsAdded(view, records, 0);
},
onItemAdd: function(records, index, nodes, view) {
this.onItemsAdded(view, records, index);
},
onItemsAdded: function(view, records, index) {
var me = this,
recCount = records.length,
freeRowContexts = me.freeRowContexts,
liveRowContexts = me.liveRowContexts || (me.liveRowContexts = {}),
i, internalId, rowContext, record;
// Ensure we have RowContexts ready for all the widget owners
// (Widget columns or RowWidget plugin) which will be needing instantiated
// Widgets with attached ViewModels.
for (i = 0; i < recCount; i++) {
internalId = (record = records[i]).internalId;
// We may have already been informed about the addition of this item
// by the opposite locking partner
if (!(rowContext = liveRowContexts[internalId])) {
// Attempt to read from free RowContexts which may have been freed
// by a previous item remove event. Shift of the front
// to improve the chances of using the same RowContext for a record;
// They were pushed on in the item remove handler.
rowContext = freeRowContexts && freeRowContexts.shift();
// Need a new one
if (!rowContext) {
rowContext = new Ext.grid.RowContext({
ownerGrid: me
});
}
if (rowContext.attach(view)) {
// if this is the first view to attach, initialize the context and
// put it in the live set:
me.liveRowContexts[internalId] = rowContext;
rowContext.setRecord(record, index + i);
}
}
else {
rowContext.attach(view);
}
}
},
onItemRemove: function(records, index, nodes, view) {
var me = this,
freeRowContexts = me.freeRowContexts || (me.freeRowContexts = []),
liveRowContexts = me.liveRowContexts,
len = nodes.length,
i, id, context;
for (i = 0; i < len; i++) {
id = nodes[i].getAttribute('data-recordId');
context = liveRowContexts[id];
if (context && context.detach(view)) {
// if this is the last view to detach, return the context to the free
// list.
freeRowContexts.push(context);
delete liveRowContexts[id];
}
}
},
createManagedWidget: function(view, ownerId, widgetConfig, record) {
return this.liveRowContexts[record.internalId].getWidget(view, ownerId, widgetConfig);
},
destroyManagedWidgets: function(ownerId) {
var me = this,
contexts = me.liveRowContexts,
freeRowContexts = me.freeRowContexts,
len = freeRowContexts && freeRowContexts.length,
i, recInternalId, rowWidgets;
// Destroy widgets from both live contexts, and free ones
for (recInternalId in contexts) {
rowWidgets = contexts[recInternalId].widgets;
if (rowWidgets) {
Ext.destroy(rowWidgets[ownerId]);
delete rowWidgets[ownerId];
}
}
for (i = 0; i < len; i++) {
rowWidgets = freeRowContexts[i].widgets;
if (rowWidgets) {
Ext.destroy(rowWidgets[ownerId]);
delete rowWidgets[ownerId];
}
}
},
getManagedWidgets: function(ownerId) {
var me = this,
contexts = me.liveRowContexts,
recInternalId,
result = [];
for (recInternalId in contexts) {
result.push(contexts[recInternalId].widgets[ownerId]);
}
return result;
},
/**
* Gets the view for this panel.
* @return {Ext.view.Table}
*/
getView: function() {
var me = this,
scroll, scrollable, viewConfig;
if (!me.view) {
viewConfig = me.viewConfig;
scroll = viewConfig.scroll || me.scroll;
scrollable = me.scrollable;
if (scrollable == null && viewConfig.scrollable == null && scroll !== null) {
// transform deprecated scroll config into scrollable config
if (scroll === true || scroll === 'both') {
scrollable = true;
}
else if (scroll === false || scroll === 'none') {
scrollable = false;
}
else if (scroll === 'vertical') {
scrollable = {
x: false,
y: true
};
}
else if (scroll === 'horizontal') {
scrollable = {
x: true,
y: false
};
}
}
viewConfig = Ext.apply({
// TableView injects the view reference into this grid so that we have a reference
// as early as possible and Features need a reference to the grid.
// For these reasons, we configure a reference to this grid into the View
grid: me,
ownerGrid: me.ownerGrid,
deferInitialRefresh: me.deferRowRender,
variableRowHeight: me.variableRowHeight,
preserveScrollOnRefresh: true,
trackOver: me.trackMouseOver !== false,
throttledUpdate: me.throttledUpdate === true,
xtype: me.viewType,
store: me.store,
headerCt: me.headerCt,
columnLines: me.columnLines,
rowLines: me.rowLines,
navigationModel: 'grid',
features: me.features,
panel: me,
emptyText: me.emptyText || ''
}, me.viewConfig);
// Impose our calculated scrollable config only if scrollability is not configured.
// eslint-disable-next-line max-len
if (!('scrollable' in viewConfig || 'scroll' in viewConfig || 'autoScroll' in viewConfig) && scrollable != null) {
viewConfig.scrollable = scrollable;
}
viewConfig.$initParent = me;
Ext.create(viewConfig);
delete viewConfig.$initParent;
// Normalize the application of the markup wrapping the emptyText config.
// `emptyText` can now be defined on the grid as well as on its viewConfig,
// and this led to the emptyText not having the wrapping markup when it was defined
// in the viewConfig. It should be backwards compatible.
// Note that in the unlikely event that emptyText is defined on both the grid config
// and the viewConfig that the viewConfig wins.
if (me.view.emptyText) {
me.view.emptyText = '<div class="' + me.emptyCls + '">' +
me.view.emptyText + '</div>';
}
// TableView's custom component layout, Ext.view.TableLayout requires a reference
// to the headerCt because it depends on the headerCt doing its work.
me.view.getComponentLayout().headerCt = me.headerCt;
me.mon(me.view, {
uievent: me.processEvent,
scope: me
});
// Plugins and features may need to access the view as soon as it is created.
if (me.hasListeners.viewcreated) {
me.fireEvent('viewcreated', me, me.view);
}
}
return me.view;
},
getEmptyText: function() {
return this.view.emptyText;
},
setEmptyText: function(emptyText) {
this.emptyText = emptyText;
this.view.setEmptyText(
'<div class="' + this.emptyCls + '">' + emptyText + '</div>'
);
return this;
},
getColumnManager: function() {
return this.columnManager;
},
getVisibleColumnManager: function() {
return this.visibleColumnManager;
},
getTopLevelColumnManager: function() {
return this.ownerGrid.getColumnManager();
},
getTopLevelVisibleColumnManager: function() {
return this.ownerGrid.getVisibleColumnManager();
},
/**
* @method setAutoScroll
*/
setAutoScroll: Ext.emptyFn,
applyScrollable: function(scrollable) {
var view = this.view;
view = view && (view.normalView || view);
if (view) {
view.setScrollable(scrollable);
}
// The view might not yet exists so we just stash the raw config away so it
// can be processed by getView()
return scrollable;
},
/**
* @private
* Processes UI events from the view. Propagates them to whatever internal Components
* need to process them.
* @param {String} type Event type, eg 'click'
* @param {Ext.view.Table} view TableView Component
* @param {HTMLElement} cell Cell HTMLElement the event took place within
* @param {Number} recordIndex Index of the associated Store Model (-1 if none)
* @param {Number} cellIndex Cell index within the row
* @param {Ext.event.Event} e Original event
* @param {Ext.data.Model} record
* @param {Object} row
*/
processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
var header = e.position.column;
if (header) {
return header.processEvent.apply(header, arguments);
}
},
/**
* Scrolls the specified record into view.
* @param {Number/String/Ext.data.Model} record The record, record id, or the zero-based
* position in the dataset to scroll to.
* @param {Object} [options] An object containing options to modify the operation.
* @param {Number/Ext.grid.column.Column} [options.column] The column to scroll into view.
* @param {Boolean} [options.animate] Pass `true` to animate the row into view.
* @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
* when it is in view.
* @param {Boolean} [options.select] Pass as `true` to select the specified row.
* @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
* @param {Function} [options.callback] A function to execute when the record is in view.
* This may be necessary if the first parameter is a record index and the view is backed by a
* {@link Ext.data.BufferedStore buffered store} which does not contain that record.
* @param {Boolean} options.callback.success `true` if acquiring the record's view node
* was successful.
* @param {Ext.data.Model} options.callback.record If successful, the target record.
* @param {HTMLElement} options.callback.node If successful, the record's view node.
* @param {Object} [options.scope] The scope (`this` reference) in which the callback function
* is executed.
*/
ensureVisible: function(record, options) {
this.doEnsureVisible(record, options);
},
scrollByDeltaY: function(yDelta, animate) {
// xDelta should be null here not 0! We're not scrolling horizontally,
// and the Scroller is sensitive to these things.
this.getView().scrollBy(null, yDelta, animate);
},
scrollByDeltaX: function(xDelta, animate) {
// Ditto yDelta.
this.getView().scrollBy(xDelta, null, animate);
},
afterCollapse: function() {
this.saveScrollPos();
this.callParent(arguments);
},
afterExpand: function() {
this.callParent(arguments);
this.restoreScrollPos();
},
saveScrollPos: Ext.emptyFn,
restoreScrollPos: Ext.emptyFn,
onHeaderResize: Ext.emptyFn,
// Update the view when a header moves
onHeaderMove: function(headerCt, header, colsToMove, fromIdx, toIdx) {
var me = this;
// If there are Features or Plugins which create DOM which must match column order,
// they set the optimizedColumnMove flag to false.
// In this case we must refresh the view on column move.
if (me.optimizedColumnMove === false) {
me.view.refreshView();
}
// Simplest case for default DOM structure is just to swap the columns round in the view.
else {
me.view.moveColumn(fromIdx, toIdx, colsToMove);
}
me.delayScroll();
},
// Section onHeaderHide is invoked after view.
onHeaderHide: function(headerCt, header) {
var view = this.view;
// The headerCt may be hiding multiple children if a leaf level column
// causes a parent (and possibly other parents) to be hidden. Only run the refresh
// once we're done
if (!headerCt.childHideCount && view.refreshCounter) {
view.refreshView();
}
},
onHeaderShow: function(headerCt, header) {
var view = this.view;
if (view.refreshCounter) {
view.refreshView();
}
},
// To be triggered on add/remove/move for a leaf header
onHeadersChanged: function(headerCt, header) {
var me = this;
if (me.rendered && !me.reconfiguring) {
me.view.refreshView();
me.delayScroll();
}
},
delayScroll: function() {
var target = this.view;
if (target) {
// Do not cause a layout by reading scrollX now.
// It must be read from the target when the task finally executes.
this.scrollTask.delay(10, null, null, [target]);
}
},
/**
* @private
* Fires the TablePanel's viewready event when the view declares that its internal DOM is ready
*/
onViewReady: function() {
this.fireEvent('viewready', this);
},
/**
* @private
* Tracks when things happen to the view and preserves the horizontal scroll position.
*/
onRestoreHorzScroll: function() {
var me = this,
x = me.scrollXPos;
if (x) {
// We need to restore the body scroll position here
me.syncHorizontalScroll(me, true);
}
},
getScrollerOwner: function() {
var rootCmp = this;
if (!this.scrollerOwner) {
rootCmp = this.up('[scrollerOwner]');
}
return rootCmp;
},
/**
* Gets left hand side marker for header resizing.
* @private
*/
getLhsMarker: function() {
var me = this;
return me.lhsMarker || (me.lhsMarker = Ext.DomHelper.append(me.el, {
role: 'presentation',
cls: me.resizeMarkerCls
}, true));
},
/**
* Gets right hand side marker for header resizing.
* @private
*/
getRhsMarker: function() {
var me = this;
return me.rhsMarker || (me.rhsMarker = Ext.DomHelper.append(me.el, {
role: 'presentation',
cls: me.resizeMarkerCls
}, true));
},
/**
* @method getSelection
* Returns the grid's selection. See `{@link Ext.selection.Model#getSelection}`.
* @inheritdoc Ext.selection.Model#getSelection
*/
getSelection: function() {
return this.getSelectionModel().getSelection();
},
/**
* Sets the value of the selection.
* @param {Ext.data.Model} selection
*/
setSelection: function(selection) {
// This is purposefully written not as a config. Because getSelection
// is an existing API that doesn't mirror the value for setSelection, we
// don't want the publish system to call the getter, but rather just the
// raw property.
var current = this.selection;
if (selection !== current) {
this.selection = selection;
this.updateSelection(selection, current);
}
},
updateSelection: function(selection) {
var me = this,
sm;
if (!me.ignoreNextSelection) {
me.ignoreNextSelection = true;
sm = me.getSelectionModel();
if (selection) {
sm.select(selection);
}
else {
sm.deselectAll();
}
me.ignoreNextSelection = false;
}
me.publishState('selection', selection);
},
updateBindSelection: function(selModel, selection) {
var me = this,
hasSelection = selection.length > 0,
selected = null;
me.hasHadSelection = me.hasHadSelection || hasSelection;
if (!me.ignoreNextSelection) {
me.ignoreNextSelection = true;
if (hasSelection) {
selected = selModel.getLastSelected();
}
if (me.hasHadSelection) {
me.setSelection(selected);
}
me.ignoreNextSelection = false;
}
},
updateFocused: function(record) {
this.getNavigationModel().setPosition(record);
},
updateHeaderBorders: function(headerBorders) {
this[headerBorders ? 'removeCls' : 'addCls'](this.noHeaderBordersCls);
},
getNavigationModel: function() {
return this.getView().getNavigationModel();
},
/**
* Returns the selection model being used by this grid's {@link Ext.view.Table view}.
* @return {Ext.selection.Model} The selection model being used by this grid's
* {@link Ext.view.Table view}.
*/
getSelectionModel: function() {
return this.getView().getSelectionModel();
},
getScrollTarget: function() {
var items = this.getScrollerOwner().query('tableview');
// Last view has the scroller
return items[items.length - 1];
},
syncHorizontalScroll: function(target, setBody) {
var me = this,
x = me.view.getScrollX(),
scrollTarget;
setBody = setBody === true;
// Only set the horizontal scroll if we've changed position,
// so that we don't set this on vertical scrolls
if (me.rendered && (setBody || x !== me.scrollXPos)) {
// Only set the body position if we're reacting to a refresh, otherwise
// we just need to set the header.
if (setBody) {
scrollTarget = me.getScrollTarget();
scrollTarget.setScrollX(x);
}
me.headerCt.setScrollX(x);
me.scrollXPos = x;
}
},
// template method meant to be overriden
onStoreLoad: Ext.emptyFn,
getEditorParent: function() {
return this.body;
},
bindStore: function(store, initial) {
var me = this,
view = me.getView(),
oldStore = me.getStore();
// Normally, this method will always be called with a valid store (because there is
// a symmetric .unbindStore method), but there are cases where this method will be called
// and passed a null value, i.e., a panel is used as a pickerfield. See EXTJS-13089.
if (store) {
// Bind to store immediately because subsequent processing
// looks for grid's store property
me.store = store;
if (view.store !== store) {
// If coming from a reconfigure, we need to set the actual store property
// on the view. Setting the store will then also set the dataSource.
//
// Note that if it's a grid feature then this is sorted out in view.bindStore(),
// and its own implementation of .bindStore() will be called.
view.bindStore(store, false);
}
me.mon(store, {
load: me.onStoreLoad,
scope: me
});
me.storeRelayers = me.relayEvents(store, [
/**
* @event filterchange
* @inheritdoc Ext.data.Store#filterchange
*/
'filterchange',
/**
* @event groupchange
* @inheritdoc Ext.data.Store#groupchange
*/
'groupchange'
]);
// If this is being called from reconfigure then the storechange will be called
// by the reconfigure machinery at the end of all processing. Otherwise, fire here.
if (!me.reconfiguring && me.hasListeners.storechange && store !== oldStore) {
me.fireEvent('storechange', me, store, oldStore);
}
}
else {
me.unbindStore();
}
},
unbindStore: function() {
var me = this,
store = me.store,
view;
if (store) {
store.trackStateChanges = false;
me.store = null;
me.mun(store, {
load: me.onStoreLoad,
scope: me
});
Ext.destroy(me.storeRelayers);
view = me.view;
if (view.store) {
view.bindStore(null);
}
else if (!store.destroyed && store.autoDestroy) {
store.destroy();
}
// If this is being called from reconfigure then the storechange will be called
// by the reconfigure machinery at the end of all processing. Otherwise, fire here.
if (!me.reconfiguring && me.hasListeners.storechange) {
me.fireEvent('storechange', me, null, store);
}
}
},
setColumns: function(columns) {
// If being reconfigured from zero columns to zero columns, skip operation.
// This can happen if columns are being set from a binding and the initial value
// of the bound data in the ViewModel is []
if (columns.length || this.getColumnManager().getColumns().length) {
this.reconfigure(undefined, columns);
}
},
/**
* A convenience method that fires {@link #event-reconfigure} with the store param.
* To set the store AND change columns, use the {@link #method-reconfigure reconfigure method}.
*
* @param {Ext.data.Store} [store] The new store.
*/
setStore: function(store) {
var me = this;
me.reconfigure(store, undefined, true);
// If we are visible, load the store
if (me.isVisible(true)) {
if (store && me.autoLoad && !store.isEmptyStore &&
!(store.loading || store.isLoaded())) {
store.load();
}
}
// Otherwise, ensure that we will load as soon as we become visible
else if (!me.globalShowListener) {
me.globalShowListener = Ext.GlobalEvents.on({
show: me.onGlobalShow,
scope: me,
destroyable: true
});
}
},
onGlobalShow: function(comp) {
var me = this,
store = me.store;
// If the global show caused this to be shown, then load
// unless there's already a locked kicked off.
if (comp === me || (comp.isAncestor(me) && me.isVisible(true))) {
if (store && me.autoLoad && !store.isEmptyStore &&
!(store.loading || store.isLoaded())) {
store.load();
}
Ext.destroy(me.globalShowListener);
}
},
/**
* Reconfigures the grid or tree with a new store and/or columns. Stores and columns
* may also be passed as params.
*
* grid.reconfigure(store, columns);
*
* Additionally, you can pass just a store or columns.
*
* tree.reconfigure(store);
* // or
* grid.reconfigure(columns);
* // or
* tree.reconfigure(null, columns);
*
* If you're using locked columns, the {@link #enableLocking} config should be set
* to `true` before the reconfigure method is executed.
*
* @param {Ext.data.Store/Object} [store] The new store instance or store config. You can
* pass `null` if no new store.
* @param {Object[]} [columns] An array of column configs
* @param {Boolean} allowUnbind (private)
* @param {Boolean} applyState (private) Allow components (such as pivot grid) to determine
* if they want to update when the store is reconfigured
*/
reconfigure: function(store, columns, allowUnbind, applyState) {
var me = this,
oldStore = me.store,
headerCt = me.headerCt,
lockable = me.lockable,
oldColumns = headerCt ? headerCt.items.getRange() : me.columns,
view = me.getView(),
scroller, block, refreshCounter, storeChanged, columnsChanged, state, stateId,
restoreFocus;
// Allow optional store argument to be fully omitted, and the columns argument to be solo
if (arguments.length === 1 && Ext.isArray(store)) {
columns = store;
store = null;
}
// Make copy in case the beforereconfigure listener mutates it.
if (columns) {
columns = Ext.Array.slice(columns);
}
me.reconfiguring = true;
if (store) {
store = Ext.StoreManager.lookup(store);
storeChanged = store && store !== oldStore;
}
// Allow for nulling the store (convert to the empty store)
else if (allowUnbind) {
store = Ext.StoreManager.lookup('ext-empty-store');
storeChanged = store !== oldStore;
}
me.fireEvent('beforereconfigure', me, store, columns, oldStore, oldColumns);
Ext.suspendLayouts();
if (me.rendered && me.layoutCounter && (scroller = me.getScrollable())) {
scroller.scrollTo(0, 0);
}
if (lockable) {
me.reconfigureLockable(store, columns, allowUnbind);
}
else {
// Prevent the view from refreshing until we have resumed layouts
// and any columns are rendered
block = view.blockRefresh;
view.blockRefresh = true;
restoreFocus = view.saveFocusState();
// Note that we need to process the store first in case one or more passed columns
// (if there are any) have active gridfilters with values which would filter
// the currently-bound store.
if (storeChanged) {
me.unbindStore();
me.bindStore(store);
}
if (columns) {
// new columns, delete scroll pos
delete me.scrollXPos;
headerCt.removeAll();
headerCt.add(columns);
columnsChanged = true;
}
headerCt.onOwnerGridReconfigure(storeChanged, columnsChanged);
refreshCounter = view.refreshCounter;
}
if (me.stateful && applyState !== false) {
stateId = me.getStateId();
state = stateId && Ext.state.Manager.get(stateId);
if (state) {
me.applyState(state);
}
}
Ext.resumeLayouts(true);
me.reconfiguring = false;
if (lockable) {
me.afterReconfigureLockable();
}
else {
view.blockRefresh = block;
// If the layout resumption didn't trigger the view to refresh, do it here
if (view.refreshCounter === refreshCounter) {
view.refreshView();
restoreFocus();
}
}
me.fireEvent('reconfigure', me, store, columns, oldStore, oldColumns);
delete me.reconfiguring;
if (storeChanged) {
me.fireEvent('storechange', me, store, oldStore);
if (!oldStore.destroyed && oldStore.autoDestroy) {
oldStore.destroy();
}
}
},
doDestroy: function() {
var me = this,
task = me.scrollTask,
view = me.view;
if (view) {
view.destroying = true;
}
if (me.lockable) {
me.destroyLockable();
}
if (task) {
task.cancel();
}
// Need to destroy plugins here because they may have listeners on the View
Ext.destroy(
me.rowContextParent, me.plugins, me.focusEnterLeaveListeners,
me.freeRowContents, Ext.Object.getValues(me.liveRowContexts),
me.lhsMarker, me.rhsMarker
);
me.callParent();
// Have to unbind the store this late because plugins and other things
// may still need it until the very end.
me.unbindStore();
},
getElementHeight: function(el) {
var rect = this.preciseHeight &&
el.getBoundingClientRect();
return rect ? (rect.height || (rect.bottom - rect.top)) : el.offsetHeight;
},
getElementSize: function(el) {
var rect = this.preciseHeight &&
el.getBoundingClientRect();
return {
width: rect ? (rect.width || (rect.right - rect.left)) : el.offsetWidth,
height: rect ? (rect.height || (rect.bottom - rect.top)) : el.offsetHeight
};
},
privates: {
// The focusable flag is set, but there is no focusable element.
// Focus is delegated to the view by the focus implementation.
initFocusableElement: function() {},
doEnsureVisible: function(record, options) {
// Handle the case where this is a lockable assembly
if (this.lockable) {
return this.ensureLockedVisible(record, options);
}
// Allow them to pass the record id.
if (typeof record !== 'number' && !record.isEntity) {
record = this.store.getById(record);
}
// eslint-disable-next-line vars-on-top
var me = this,
view = me.getView(),
domNode = view.getNode(record),
isLocking = me.ownerGrid.lockable,
callback, scope, animate,
highlight, select, doFocus, verticalScroller, column, cell, targetContext,
internalCallback, scrollPromise;
if (options) {
callback = options.callback;
scope = options.scope;
animate = options.animate;
highlight = options.highlight;
select = options.select;
doFocus = options.focus;
column = options.column;
}
// Always supercede any prior deferred request
if (me.deferredEnsureVisible) {
me.deferredEnsureVisible.destroy();
}
// We have not yet run the layout.
// Add this to the end of the first sizing process.
// By using the resize event, we will come in AFTER any Component's onResize
// and onBoxReady handling.
if (!view.componentLayoutCounter) {
me.deferredEnsureVisible = view.on({
resize: me.doEnsureVisible,
args: Ext.Array.slice(arguments),
scope: me,
single: true,
destroyable: true
});
return;
}
if (typeof column === 'number') {
column = me.ownerGrid.getVisibleColumnManager().getColumns()[column];
}
// We found the DOM node associated with the record
if (domNode) {
if (!record.isEntity) {
record = view.getRecord(domNode);
}
verticalScroller = isLocking ? me.ownerGrid.getScrollable() : view.getScrollable();
// Scrolling *may* be asynchronous if animation is used, so post-process
// the target node in a callback.
if (callback || select || doFocus) {
internalCallback = function() {
if (view && view.destroyed) {
return;
}
targetContext =
new Ext.grid.CellContext(view).setPosition(record, column || 0);
if (select) {
view.getSelectionModel().selectByPosition(targetContext);
}
if (doFocus) {
view.getNavigationModel().setPosition(targetContext);
}
Ext.callback(callback, scope || me, [true, record, domNode]);
};
}
if (verticalScroller) {
if (column) {
cell = Ext.fly(domNode).selectNode(column.getCellSelector());
}
// We're going to need two scrollers if we are locking, and we need
// to scroll horizontally. The whole arrangement of side by side views
// scrolls up and down. Each view itself scrolls horizontally.
if (isLocking && column) {
verticalScroller.ensureVisible(domNode, {
x: false
});
scrollPromise = view.getScrollable().ensureVisible(cell || domNode, {
animation: animate,
highlight: highlight
});
}
// No locking, it's simple - we just use the view's scroller
else {
scrollPromise = verticalScroller.ensureVisible(cell || domNode, {
animation: animate,
highlight: highlight,
x: !!column
});
}
if (scrollPromise && internalCallback) {
scrollPromise.then(internalCallback);
}
}
}
// If we didn't find it, it's probably because of buffered rendering
else if (view.bufferedRenderer) {
view.bufferedRenderer.scrollTo(record, {
animate: animate,
highlight: highlight,
select: select,
focus: doFocus,
column: column,
callback: function(recordIdx, record, domNode) {
Ext.callback(callback, scope || me, [true, record, domNode]);
}
});
}
else {
Ext.callback(callback, scope || me, [false, null]);
}
},
getFocusEl: function() {
return this.getView().getFocusEl();
},
/**
* Provide a single parent viewmodel for the grid so that any VM for
* row contents share the same scheduler.
* @return {Ext.app.ViewModel}
*
* @private
*/
getRowContextViewModelParent: function() {
var vm = this.lookupViewModel() || this.rowContextParent;
if (!vm) {
// If we get to this point, it means that there's no parent VM above us
// so we have nothing to hook up to
this.rowContextParent = vm = new Ext.app.ViewModel();
}
return vm;
},
handleWidgetViewChange: function(view, ownerId) {
var contexts = this.liveRowContexts,
freeRowContexts = this.freeRowContexts,
len = freeRowContexts && freeRowContexts.length,
i, recInternalId;
for (recInternalId in contexts) {
contexts[recInternalId].handleWidgetViewChange(view, ownerId);
}
for (i = 0; i < len; i++) {
freeRowContexts[i].handleWidgetViewChange(view, ownerId);
}
},
initInheritedState: function(inheritedState, inheritedStateInner) {
inheritedState.inLockedGrid = !!this.isLocked;
this.callParent([inheritedState, inheritedStateInner]);
},
/**
* Toggles ARIA actionable mode on/off
* @param {Boolean} enabled
* @param {Ext.grid.CellContext} position The cell to activate.
* @param {HTMLElement/Ext.dom.Element} [position.target] The element within
* the referenced cell to focus.
* @return {Boolean} `true` if actionable mode was entered
* @private
*/
setActionableMode: function(enabled, position) {
// Always set the topmost grid in a lockable assembly
var me = this.ownerGrid;
// Can be called to exit actionable mode upon a focusLeave caused by destruction
if (!me.destroying && me.view.setActionableMode(enabled, position) !== false) {
me.fireEvent('actionablemodechange', enabled);
me[enabled ? 'addCls' : 'removeCls'](me.actionableModeCls);
return true;
}
},
/**
* Override for TablePanel.
* A TablePanel can never scroll. Its View scrolls.
* @private
*/
getOverflowStyle: function() {
// eslint-disable-next-line dot-notation
this.scrollFlags = this._scrollFlags['false']['false'];
return {
overflowX: 'hidden',
overflowY: 'hidden'
};
},
getOverflowEl: function() {
return null;
},
shouldAutoHideHeaders: function() {
var me = this,
columns = me.headerCt.items.items,
len = columns.length,
autoHideHeaders = !!len,
column, i;
// Loop until we find a column with content.
for (i = 0; autoHideHeaders && i < len; i++) {
column = columns[i];
// If any column was configured with visible text, we must show headers.
if (!column.isEmptyText(column.text, true) || column.columns ||
(column.isGroupHeader && column.items.items.length)) {
autoHideHeaders = false;
}
}
return autoHideHeaders;
},
syncHeaderVisibility: function() {
var me = this,
headerCt = me.headerCt,
hideHeaders = me.hideHeaders,
viewScroller, currentHideHeaderState;
if (me.lockable) {
me.syncLockableHeaderVisibility();
return;
}
if (hideHeaders == null) {
hideHeaders = me.shouldAutoHideHeaders();
}
currentHideHeaderState = headerCt.height === 0;
// set the focusable to false if the header is hidden
headerCt.focusableContainer = !hideHeaders;
if (!headerCt.rendered || hideHeaders !== currentHideHeaderState) {
headerCt.setHeight(hideHeaders ? 0 : null);
headerCt.hiddenHeaders = hideHeaders;
headerCt.toggleCls(me.hiddenHeaderCtCls, hideHeaders);
me.toggleCls(me.hiddenHeaderCls, hideHeaders);
if (!hideHeaders) {
headerCt.setScrollable({
x: false,
y: false
});
viewScroller = me.view.getScrollable();
if (viewScroller) {
headerCt.getScrollable().addPartner(viewScroller, 'x');
}
}
}
}
}
});