/** * A widget column is configured with a {@link #widget} config object which specifies an * {@link Ext.Component#cfg-xtype xtype} to indicate which type of Widget or Component belongs * in the cells of this column. * * When a widget cell is rendered, a {@link Ext.Widget Widget} or {@link Ext.Component Component} of the specified type * is rendered into that cell. Its {@link Ext.Component#defaultBindProperty defaultBindProperty} is set using this * column's {@link #dataIndex} field from the associated record. * * In the example below we are monitoring the throughput of electricity substations. The capacity being * used as a proportion of the maximum rated capacity is displayed as a progress bar. As new data arrives and the * instantaneous usage value is updated, the `capacityUsed` field updates itself, and * that is used as the {@link #dataIndex} for the `WidgetColumn` which contains the * progress bar. The progress Bar's * {@link Ext.ProgressBarWidget#defaultBindProperty defaultBindProperty} (which is * "value") is set to the calculated `capacityUsed`. * * @example * var grid = new Ext.grid.Panel({ * title: 'Substation power monitor', * width: 600, * columns: [{ * text: 'Id', * dataIndex: 'id', * width: 120 * }, { * text: 'Rating', * dataIndex: 'maxCapacity', * width: 80 * }, { * text: 'Avg.', * dataIndex: 'avg', * width: 85, * formatter: 'number("0.00")' * }, { * text: 'Max', * dataIndex: 'max', * width: 80 * }, { * text: 'Instant', * dataIndex: 'instant', * width: 80 * }, { * text: '%Capacity', * width: 150, * * // This is our Widget column * xtype: 'widgetcolumn', * dataIndex: 'capacityUsed', * * // This is the widget definition for each cell. * // Its "value" setting is taken from the column's dataIndex * widget: { * xtype: 'progressbarwidget', * textTpl: [ * '{percent:number("0")}% capacity' * ] * } * }], * renderTo: document.body, * disableSelection: true, * store: { * fields: [{ * name: 'id', * type: 'string' * }, { * name: 'maxCapacity', * type: 'int' * }, { * name: 'avg', * type: 'int', * calculate: function(data) { * // Make this depend upon the instant field being set which sets the sampleCount and total. * // Use subscript format to access the other pseudo fields which are set by the instant field's converter * return data.instant && data['total'] / data['sampleCount']; * } * }, { * name: 'max', * type: 'int', * calculate: function(data) { * // This will be seen to depend on the "instant" field. * // Use subscript format to access this field's current value to avoid circular dependency error. * return (data['max'] || 0) < data.instant ? data.instant : data['max']; * } * }, { * name: 'instant', * type: 'int', * * // Upon every update of instantaneous power throughput, * // update the sample count and total so that the max field can calculate itself * convert: function(value, rec) { * rec.data.sampleCount = (rec.data.sampleCount || 0) + 1; * rec.data.total = (rec.data.total || 0) + value; * return value; * }, * depends: [] * }, { * name: 'capacityUsed', * calculate: function(data) { * return data.instant / data.maxCapacity; * } * }], * data: [{ * id: 'Substation A', * maxCapacity: 1000, * avg: 770, * max: 950, * instant: 685 * }, { * id: 'Substation B', * maxCapacity: 1000, * avg: 819, * max: 992, * instant: 749 * }, { * id: 'Substation C', * maxCapacity: 1000, * avg: 588, * max: 936, * instant: 833 * }, { * id: 'Substation D', * maxCapacity: 1000, * avg: 639, * max: 917, * instant: 825 * }] * } * }); * * // Fake data updating... * // Change one record per second to a random power value * Ext.interval(function() { * var recIdx = Ext.Number.randomInt(0, 3), * newPowerReading = Ext.Number.randomInt(500, 1000); * * grid.store.getAt(recIdx).set('instant', newPowerReading); * }, 1000); * * @since 5.0.0 */Ext.define('Ext.grid.column.Widget', { extend: 'Ext.grid.column.Column', alias: 'widget.widgetcolumn', config: { /** * @cfg defaultWidgetUI * A map of xtype to {@link Ext.Component#ui} names to use when using Components in this column. * * Currently {@link Ext.Button Button} and all subclasses of {@link Ext.form.field.Text TextField} default * to using `ui: "default"` when in a WidgetColumn except for in the "classic" theme, when they use ui "grid-cell". */ defaultWidgetUI: {} }, /** * @cfg * @inheritdoc */ sortable: false, /** * @cfg {Object} renderer * @hide */ /** * @cfg {Object} scope * @hide */ /** * @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 cells of this column. * * This column's {@link #dataIndex} is used to update the widget/component's {@link Ext.Component#defaultBindProperty defaultBindProperty}. * * The widget will be decorated with 2 methods: * `getWidgetRecord` - Returns the {@link Ext.data.Model record} the widget is associated with. * `getWidgetColumn` - Returns the {@link Ext.grid.column.Widget column} the widget * was associated with. */ /** * @cfg {Function/String} onWidgetAttach * A function that will be called when a widget is attached to a record. This may be useful for * doing any post-processing. * * Ext.create({ * xtype: 'grid', * title: 'Student progress report', * width: 250, * renderTo: Ext.getBody(), * disableSelection: true, * store: { * fields: ['name', 'isHonorStudent'], * data: [{ * name: 'Finn', * isHonorStudent: true * }, { * name: 'Jake', * isHonorStudent: false * }] * }, * columns: [{ * text: 'Name', * dataIndex: 'name', * flex: 1 * }, { * xtype: 'widgetcolumn', * text: 'Honor Roll', * dataIndex: 'isHonorStudent', * width: 150, * widget: { * xtype: 'button', * handler: function() { * // print certificate handler * } * }, * // called when the widget is initially instantiated * // on the widget column * onWidgetAttach: function(col, widget, rec) { * widget.setText('Print Certificate'); * widget.setDisabled(!rec.get('isHonorStudent')); * } * }] * }); * * @param {Ext.grid.column.Column} column The column. * @param {Ext.Component/Ext.Widget} widget The {@link #widget} rendered to each cell. * @param {Ext.data.Model} record The record used with the current widget (cell). * @declarativeHandler */ onWidgetAttach: null, preventUpdate: true, /** * @cfg {Boolean} [stopSelection=true] * Prevent grid selection upon click on the widget. */ stopSelection: true, initComponent: function() { var me = this, widget; me.callParent(arguments); widget = me.widget; //<debug> if (!widget || widget.isComponent) { Ext.Error.raise('column.Widget requires a widget configuration.'); } //</debug> me.widget = widget = Ext.apply({}, widget); // Apply the default UI for the xtype which is going to feature in this column. if (!widget.ui) { widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default'; } me.isFixedSize = Ext.isNumber(widget.width); }, processEvent : function(type, view, cell, recordIndex, cellIndex, e, record, row) { var selector = view.innerSelector, target; if (this.stopSelection && type === 'click') { // Grab the target that matches the cell inner selector. If we have a target, then, // that means we either clicked on the inner part or the widget inside us. If // target === e.target, then it was on the cell, so it's ok. Otherwise, inside so // prevent the selection from happening target = e.getTarget(selector); if (target && target !== e.target) { e.stopSelection = true; } } }, beforeRender: function() { var me = this, tdCls = me.tdCls, widget; me.listenerScopeFn = function (defaultScope) { if (defaultScope === 'this') { return this; } return me.resolveListenerScope(defaultScope); }; me.liveWidgets = {}; me.cachedStyles = {}; me.freeWidgetStack = [widget = me.getFreeWidget()]; tdCls = tdCls ? tdCls + ' ' : ''; me.tdCls = tdCls + widget.getTdCls(); me.setupViewListeners(me.getView()); me.callParent(); }, afterRender: function() { var view = this.getView(); this.callParent(); // View already ready, means we were added later so go and set up our widgets, but if the grid // is reconfiguring, then the column will be rendered & the view will be ready, so wait until // the reconfigure forces a refresh if (view && view.viewReady && !view.ownerGrid.reconfiguring) { this.onViewRefresh(view, view.getViewRange()); } }, // Cell must be left blank defaultRenderer: Ext.emptyFn, updater: function(cell, value, record) { this.updateWidget(record); }, onResize: function(newWidth) { var me = this, liveWidgets = me.liveWidgets, view = me.getView(), id, cell; if (!me.isFixedSize && me.rendered && view && view.viewReady) { cell = view.getEl().down(me.getCellInnerSelector()); if (cell) { // Subtract innerCell padding width newWidth -= parseInt(me.getCachedStyle(cell, 'padding-left'), 10) + parseInt(me.getCachedStyle(cell, 'padding-right'), 10); for (id in liveWidgets) { liveWidgets[id].setWidth(newWidth); } } } }, onAdded: function() { var me = this, view; me.callParent(arguments); view = me.getView(); // If we are being added to a rendered HeaderContainer if (view) { me.setupViewListeners(view); if (view && view.viewReady && me.rendered && view.getEl().down(me.getCellSelector())) { // If the view is ready, it means we're already rendered. // At this point the view may refresh "soon", however we don't have // a way of knowing that the view is pending a refresh, so we need // to ensure the widgets get hooked up correctly here me.onViewRefresh(view, view.getViewRange()); } } }, onRemoved: function(isDestroying) { var me = this, liveWidgets = me.liveWidgets, viewListeners = me.viewListeners, id; if (me.rendered) { me.viewListeners = viewListeners && Ext.destroy(viewListeners); // If we are being removed, we have to move all widget elements into the detached body if (!isDestroying) { for (id in liveWidgets) { liveWidgets[id].detachFromBody(); } } } me.callParent(arguments); }, onDestroy: function() { var me = this, oldWidgetMap = me.liveWidgets, freeWidgetStack = me.freeWidgetStack, id, widget, i, len; if (me.rendered) { for (id in oldWidgetMap) { widget = oldWidgetMap[id]; widget.$widgetRecord = widget.$widgetColumn = null; delete widget.getWidgetRecord; delete widget.getWidgetColumn; widget.destroy(); } for (i = 0, len = freeWidgetStack.length; i < len; ++i) { freeWidgetStack[i].destroy(); } } me.freeWidgetStack = me.liveWidgets = null; me.callParent(); }, getWidget: function(record) { var liveWidgets = this.liveWidgets, widget; if (record && liveWidgets) { widget = liveWidgets[record.internalId]; } return widget || null; }, privates: { getCachedStyle: function(el, style) { var cachedStyles = this.cachedStyles; return cachedStyles[style] || (cachedStyles[style] = Ext.fly(el).getStyle(style)); }, getFreeWidget: function() { var me = this, result = me.freeWidgetStack ? me.freeWidgetStack.pop() : null; if (!result) { result = Ext.widget(me.widget); result.resolveListenerScope = me.listenerScopeFn; result.getWidgetRecord = me.widgetRecordDecorator; result.getWidgetColumn = me.widgetColumnDecorator; result.dataIndex = me.dataIndex; result.measurer = me; result.ownerCmp = me; // The ownerCmp of the widget is the column, which means it will be considered // as a layout child, but it isn't really, we always need the layout on the // component to run if asked. result.isLayoutChild = me.returnFalse; } return result; }, onBeforeRefresh: function () { var liveWidgets = this.liveWidgets, id; // Because of a memory leak bug in IE 8, we need to handle the dom node here before // it is destroyed. // See EXTJS-14874. for (id in liveWidgets) { liveWidgets[id].detachFromBody(); } }, onItemAdd: function(records) { var me = this, view = me.getView(), hasAttach = !!me.onWidgetAttach, dataIndex = me.dataIndex, isFixedSize = me.isFixedSize, len = records.length, i, record, cell, widget, el, width; // Loop through all records added, ensuring that our corresponding cell in each item // has a Widget of the correct type in it, and is updated with the correct value from the record. if (me.isVisible(true)) { for (i = 0; i < len; i++) { record = records[i]; if (record.isNonData) { continue; } cell = view.getCell(record, me); // May be a placeholder with no data row if (cell) { cell = cell.dom.firstChild; if (!isFixedSize && !width && me.lastBox) { width = me.lastBox.width - parseInt(me.getCachedStyle(cell, 'padding-left'), 10) - parseInt(me.getCachedStyle(cell, 'padding-right'), 10); } widget = me.liveWidgets[record.internalId] = me.getFreeWidget(); widget.$widgetColumn = me; widget.$widgetRecord = record; // Render/move a widget into the new row Ext.fly(cell).empty(); // Call the appropriate setter with this column's data field if (widget.defaultBindProperty && dataIndex) { widget.setConfig(widget.defaultBindProperty, record.get(dataIndex)); } el = widget.el || widget.element; if (el) { cell.appendChild(el.dom); if (!isFixedSize) { widget.setWidth(width); } widget.reattachToBody(); } else { if (!isFixedSize) { widget.width = width; } widget.render(cell); } // We have to run the callback *after* reattaching the Widget // back to the document body. Otherwise widget's layout may fail // because there are no dimensions to measure when the callback is fired! if (hasAttach) { Ext.callback(me.onWidgetAttach, me.scope, [me, widget, record], 0, me); } } } } else { view.refreshNeeded = true; } }, onTreeViewItemRemove: function(record, index, view) { // This shim is here because TreeView's itemremove event signature // is different from Table View. In 6.0+ both Views will fire this event // with the same parameters; in 5.x we have to use a shim because // changing event signature is a breaking change and is not desirable. this.onItemRemove(record, index, view.all.item(index, true)); }, onItemRemove: function(records, index, items) { var me = this, liveWidgets = me.liveWidgets, widget, item, id, len, i; if (me.rendered) { // Single item or Array. items are DOM nodes; we use them to get // record ids because records array may not be passed here items = Ext.Array.from(items); len = items.length; for (i = 0; i < len; i++) { item = items[i]; // If there was a record ID (collapsed placeholder will no longer be // accessible)... return ousted widget to free stack, and move its element // to the detached body id = item.getAttribute('data-recordId'); if (id && (widget = liveWidgets[id])) { delete liveWidgets[id]; me.freeWidgetStack.unshift(widget); widget.$widgetRecord = widget.$widgetColumn = null; widget.detachFromBody(); } } } }, onItemUpdate: function(record, recordIndex, oldItemDom) { this.updateWidget(record); }, onViewRefresh: function(view, records) { Ext.suspendLayouts(); this.onItemAdd(records); Ext.resumeLayouts(true); }, returnFalse: function() { return false; }, setupViewListeners: function(view) { var me = this; me.viewListeners = view.on({ refresh: me.onViewRefresh, itemupdate: me.onItemUpdate, itemadd: me.onItemAdd, itemremove: view.isTreeView ? me.onTreeViewItemRemove : me.onItemRemove, scope: me, destroyable: true }); if (Ext.isIE8) { view.on('beforerefresh', me.onBeforeRefresh, me); } }, updateWidget: function(record) { var me = this, dataIndex = me.dataIndex, widget; if (this.rendered) { widget = this.liveWidgets[record.internalId]; // Call the appropriate setter with this column's data field if (widget && widget.defaultBindProperty && dataIndex) { widget.setConfig(widget.defaultBindProperty, record.get(dataIndex)); } } }, widgetRecordDecorator: function() { return this.$widgetRecord; }, widgetColumnDecorator: function() { return this.$widgetColumn; } }});