/**
* Plugin (ptype = 'rowwidget') that adds the ability to second row body in a grid
* which expands/contracts.
*
* The expand/contract behavior is configurable to react on clicking of the column,
* double click of the row, and/or hitting enter while a row is selected.
*
* The expansion row may contain a {@link #cfg-widget} which is primed with the record
* of the corresponding grid row. The widget's
* {@link Ext.Component#cfg-defaultBindProperty defaultBindProperty} property is set to the record.
*/
Ext.define('Ext.grid.plugin.RowWidget', {
extend: 'Ext.grid.plugin.RowExpander',
alias: 'plugin.rowwidget',
mixins: [
'Ext.mixin.Identifiable',
'Ext.mixin.StyleCacher'
],
lockableScope: 'top',
config: {
/**
* @cfg {Object} defaultWidgetUI
* A map of xtype to {@link Ext.Component#ui} names to use when using Components
* in the expansion row.
*/
defaultWidgetUI: {}
},
/**
* @cfg {Object} widget
* A config object containing an {@link Ext.Component#cfg-xtype xtype}.
*
* This is used to create the widgets or components which are rendered into the expansion row.
*
* The associated grid row's record is used to update the widget/component's
* {@link Ext.Component#defaultBindProperty defaultBindProperty}.
*
* Note that if this plugin is applied to a lockable grid, the widget applies to the normal
* (unlocked) side.
* See {@link #lockedWidget}
*
*/
widget: null,
/**
* @cfg {Object} [lockedWidget]
* A config object containing an {@link Ext.Component#cfg-xtype xtype}.
*
* This is used to create the widgets or components which are rendered into the expansion row
* *on the locked side of a lockable grid*.
*/
lockedWidget: null,
addCollapsedCls: {
fn: function(out, values, parent) {
var me = this.rowExpander;
if (!me.recordsExpanded[values.record.internalId]) {
values.itemClasses.push(me.rowCollapsedCls);
}
this.nextTpl.applyOut(values, out, parent);
},
// We need a high priority to get in ahead of the outerRowTpl
// so we can setup row data
priority: 20000
},
setCmp: function(grid) {
var me = this,
features,
widget;
// Generate a unique class name so we can identify our row element.
me.rowIdCls = Ext.id(null, Ext.baseCSSPrefix + 'rowwidget-');
// Keep track of which record internalIds are expanded.
me.recordsExpanded = {};
Ext.plugin.Abstract.prototype.setCmp.apply(me, arguments);
widget = me.widget;
//<debug>
if (!widget || widget.isComponent) {
Ext.raise('RowWidget requires a widget configuration.');
}
//</debug>
me.widget = widget = Ext.apply({}, widget);
// Apply the default UI for the xtype which is going to feature
// in the normal side's expansion row.
if (!widget.ui) {
widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
}
// If the grid is a lockable assembly, we have to track locked widgets.
if (grid.enableLocking && me.lockedWidget) {
me.lockedWidget = widget = Ext.apply({}, me.lockedWidget);
// Apply the default UI for the xtype which is going to feature
// in the locked side's expansion row.
if (!widget.ui) {
widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
}
}
features = me.getFeatureConfig(grid);
if (grid.features) {
grid.features = Ext.Array.push(features, grid.features);
}
else {
grid.features = features;
}
// NOTE: features have to be added before init (before Table.initComponent)
},
/**
* @protected
* @return {Array} And array of Features or Feature config objects.
* Returns the array of Feature configurations needed to make the RowWidget work.
* May be overridden in a subclass to modify the returned array.
*/
getFeatureConfig: function(grid) {
var me = this,
features = [],
featuresCfg = {
ftype: 'rowbody',
rowExpander: me,
doSync: false,
rowIdCls: me.rowIdCls,
bodyBefore: me.bodyBefore,
recordsExpanded: me.recordsExpanded,
rowBodyHiddenCls: me.rowBodyHiddenCls,
rowCollapsedCls: me.rowCollapsedCls,
setupRowData: me.setupRowData,
setup: me.setup,
// Do not relay click events into the client grid's row
onClick: Ext.emptyFn
};
features.push(Ext.apply({
lockableScope: 'normal'
}, featuresCfg));
// Locked side will need a copy to keep the two DOM structures symmetrical.
// A lockedWidget config is available to create content in locked side.
// The enableLocking flag is set early in Ext.panel.Table#initComponent
// if any columns are locked.
if (grid.enableLocking) {
features.push(Ext.apply({
lockableScope: 'locked'
}, featuresCfg));
}
return features;
},
setupRowData: function(record, rowIndex, rowValues) {
var me = this.rowExpander;
me.rowBodyFeature = this;
rowValues.rowBodyCls = me.recordsExpanded[record.internalId] ? '' : me.rowBodyHiddenCls;
},
bindView: function(view) {
var me = this;
me.viewListeners = view.on({
refresh: me.onViewRefresh,
itemadd: me.onItemAdd,
scope: me,
destroyable: true
});
Ext.override(view, me.viewOverrides);
},
destroy: function() {
var me = this,
id = me.getId();
me.viewListeners.destroy();
if (me.grid.lockable) {
me.grid.destroyManagedWidgets(id + '-' + me.lockedView.getId());
me.grid.destroyManagedWidgets(id + '-' + me.normalView.getId());
}
else {
me.grid.destroyManagedWidgets(id + '-' + me.view.getId());
}
me.callParent();
},
privates: {
viewOverrides: {
handleEvent: function(e) {
// An override applied to the client view so that it ignores events
// from within the expander row
// Ignore all events from within our rowwidget
if (e.getTarget('.' + this.rowExpander.rowIdCls, this.body)) {
return;
}
this.callParent([e]);
},
onFocusEnter: function(e) {
// An override applied to the client view so that it ignores
// focus moving into the expander row
if (e.event.getTarget('.' + this.rowExpander.rowIdCls, this.body)) {
return;
}
this.callParent([e]);
},
toggleChildrenTabbability: function(enableTabbing) {
// An override applied to the client view so that it does not interfere
// with tabbability of elements within the expander rows.
var focusEl = this.getTargetEl(),
rows = this.all,
restoreOptions = { skipSelf: true },
saveOptions = { skipSelf: true, includeSaved: false },
i;
for (i = rows.startIndex; i <= rows.endIndex; i++) {
// Extract the data row from each row.
// We do not interfere with tabbing in the the expander row.
focusEl = Ext.fly(this.getRow(rows.item(i)));
if (!focusEl) {
continue;
}
if (enableTabbing) {
focusEl.restoreTabbableState(restoreOptions);
}
else {
// Do NOT includeSaved
// Once an item has had tabbability saved, do not increment its save level
focusEl.saveTabbableState(saveOptions);
}
}
}
},
destroyLiveWidget: function(recId, widget) {
widget.destroy();
},
destroyFreeWidget: function(widget) {
widget.destroy();
},
onItemAdd: function(newRecords, startIndex, newItems, view) {
var me = this,
len = newItems.length,
i,
record,
ownerLockable = me.grid.lockable;
// May be multiple widgets being layed out here
Ext.suspendLayouts();
for (i = 0; i < len; i++) {
record = newRecords[i];
if (!record.isNonData && me.recordsExpanded[record.internalId]) {
// If any added items are expanded, we will need a syncRowHeights
// call on next layout
if (ownerLockable) {
me.grid.syncRowHeightOnNextLayout = true;
}
me.addWidget(view, record);
}
}
Ext.resumeLayouts(true);
},
onViewRefresh: function(view, records) {
var me = this,
rows = view.all,
itemIndex, recordIndex;
Ext.suspendLayouts();
// eslint-disable-next-line max-len
for (itemIndex = rows.startIndex, recordIndex = 0; itemIndex <= rows.endIndex; itemIndex++, recordIndex++) {
if (me.recordsExpanded[records[recordIndex].internalId]) {
me.addWidget(view, records[recordIndex]);
}
}
Ext.resumeLayouts(true);
},
returnFalse: function() {
return false;
},
/**
* Returns if possible the widget currently associated with the passed record
* within the passed view.
*
* Note that if the record is not currently in the rendered block, *or*,
* it has never been expanded then there will not be a widget associated
* with that `record/view` context.
* @param {type} view The view for which to return the widget
* @param {type} record The record for which to return the widget
* @return {me.lockedLiveWidgets/me.liveWidgets}
*/
getWidget: function(view, record) {
var me = this,
result,
widget;
if (record) {
widget = me.grid.lockable && view === me.lockedView ? me.lockedWidget : me.widget;
if (widget) {
result = me.grid.createManagedWidget(
view, me.getId() + '-' + view.getId(), widget, record
);
result.measurer = me;
result.ownerLayout = view.componentLayout;
}
}
return result;
},
addWidget: function(view, record) {
var me = this,
target,
width,
widget,
hasAttach = !!me.onWidgetAttach,
isFixedSize = me.isFixedSize,
el;
// If the record is non data (placeholder), or not expanded, return
if (record.isNonData || !me.recordsExpanded[record.internalId]) {
return;
}
target = Ext.fly(view.getNode(record).querySelector(me.rowBodyFeature.innerSelector));
width = target.getWidth(true) - target.getPadding('lr');
widget = me.getWidget(view, record);
// Might be no widget if we are handling a lockable grid
// and only one side has a widget definition.
if (widget) {
if (hasAttach) {
Ext.callback(me.onWidgetAttach, me.scope, [me, widget, record], 0, me);
}
el = widget.el || widget.element;
if (el) {
target.dom.appendChild(el.dom);
if (!isFixedSize && widget.width !== width) {
widget.setWidth(width);
}
else {
widget.updateLayout();
}
widget.reattachToBody();
}
else {
if (!isFixedSize) {
widget.width = width;
}
widget.render(target);
}
widget.updateLayout();
}
return widget;
},
toggleRow: function(rowIdx, record) {
var me = this,
// If we are handling a lockable assembly,
// handle the normal view first
view = me.normalView || me.view,
rowNode = view.getNode(rowIdx),
normalRow = Ext.fly(rowNode),
lockedRow,
nextBd = normalRow.down(me.rowBodyTrSelector, true),
wasCollapsed = normalRow.hasCls(me.rowCollapsedCls),
addOrRemoveCls = wasCollapsed ? 'removeCls' : 'addCls',
ownerLockable = me.grid.lockable && me.grid,
widget, vm;
normalRow[addOrRemoveCls](me.rowCollapsedCls);
Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
// All layouts must be coalesced.
// Particularly important for locking assemblies which need
// to sync row height on the next layout.
Ext.suspendLayouts();
// We're expanding
if (wasCollapsed) {
me.recordsExpanded[record.internalId] = true;
widget = me.addWidget(view, record);
vm = widget.lookupViewModel();
}
else {
delete me.recordsExpanded[record.internalId];
widget = me.getWidget(view, record);
}
// Sync the collapsed/hidden classes on the locked side
if (ownerLockable) {
// Only attempt to toggle lockable side if it is visible.
if (ownerLockable.lockedGrid.isVisible()) {
view = me.lockedView;
// Process the locked side.
lockedRow = Ext.fly(view.getNode(rowIdx));
// Just because the grid is locked, doesn't mean we'll necessarily
// have a locked row.
if (lockedRow) {
lockedRow[addOrRemoveCls](me.rowCollapsedCls);
// If there is a template for expander content in the locked side,
// toggle that side too
nextBd = lockedRow.down(me.rowBodyTrSelector, true);
Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
// Pass an array if we're in a lockable assembly.
if (wasCollapsed && me.lockedWidget) {
widget = [widget, me.addWidget(view, record)];
}
else {
widget = [widget, me.getWidget(view, record)];
}
}
// We're going to need a layout run to synchronize row heights
ownerLockable.syncRowHeightOnNextLayout = true;
}
}
me.view.fireEvent(wasCollapsed ? 'expandbody' : 'collapsebody', rowNode, record,
nextBd, widget);
view.updateLayout();
// Before layouts are resumed, if we have *expanded* the widget row,
// then ensure bound data is flushed into the widget so that it assumes its final size.
if (vm) {
vm.notify();
}
Ext.resumeLayouts(true);
if (me.scrollIntoViewOnExpand && wasCollapsed) {
me.grid.ensureVisible(rowIdx);
}
}
}
});