/** * @private * This class encapsulates a row of managed Widgets/Components when a WidgetColumn, or * RowWidget plugin is used in a grid. * * The instances are recycled, and this class holds instances which are derendered so that they can * be moved back into newly rendered rows. * * Developers should not use this class. * */Ext.define('Ext.grid.RowContext', { constructor: function(config) { Ext.apply(this, config); this.widgets = {}; /** * @property {Number} usage * Tracks the views that are using this row context. Each view is represented by * a bit position. The normal view of a locked grid (or the only view in a * non-locking grid) uses bit 0 (i.e., `1`). The locked view uses bit 1 (i.e., `2`). * * This value will be in the range of 0-3 (0 when unused by a view, 3 when used by * both normal and locking views). * @since 7.1 * @private */ this.usage = 0; }, setRecord: function(record, recordIndex) { var viewModel = this.viewModel; this.record = record; this.recordIndex = recordIndex; if (viewModel) { viewModel.set('record', record); viewModel.set('recordIndex', recordIndex); } }, attach: function(view) { var was = this.usage; this.usage |= view.usageBitMask; return !was; }, detach: function(view) { var me = this, widgets = me.widgets, usageBitMask = view.usageBitMask, viewModel = me.viewModel, focusEl, free, widget, widgetId; if (!(me.usage & usageBitMask)) { return false; } me.usage &= ~usageBitMask; free = !me.usage; if (free) { me.record = null; if (viewModel) { viewModel.set('record'); viewModel.set('recordIndex'); } } // All the widgets this RowContext manages must be blurred // and moved into the detached body to save them from garbage collection. for (widgetId in widgets) { widget = widgets[widgetId]; // Only remove widgets owned by this view... if (!view.isAncestor(widget)) { // NOTE: We cannot do something like "view.el.contains(widget.el)" in // trees because collapsing nodes de-render content too early. continue; } // Focusables in a grid must not be tabbable by default when they get put back in. focusEl = widget.getFocusEl(); if (focusEl) { // Widgets are reused so we must reset their tabbable state // regardless of their visibility. // For example, when removing rows in IE8 we're attaching // the nodes to a document-fragment which itself is invisible, // so isTabbable() returns false. Next time when we're reusing // this widget it will be attached to the document with its // tabbable state unreset, which might lead to undesired results. if (focusEl.isTabbable(true)) { focusEl.saveTabbableState({ includeHidden: true }); } // Some browsers do not deliver a focus change upon DOM removal. // Force the issue here. focusEl.blur(); } if (widget.rendered) { widget.detachFromBody(); } } return free; }, getWidget: function(view, ownerId, widgetCfg) { var me = this, widgets = me.widgets || (me.widgets = {}), ownerGrid = me.ownerGrid, rowViewModel = ownerGrid.rowViewModel, rowVM = me.viewModel, result; // Only spin up an attached ViewModel when we instantiate our first managed Widget // which uses binding. if ((widgetCfg.bind || rowViewModel) && !rowVM) { if (typeof rowViewModel === 'string') { rowViewModel = { type: rowViewModel }; } me.viewModel = rowVM = Ext.Factory.viewModel(Ext.merge({ parent: ownerGrid.getRowContextViewModelParent(), data: { record: me.record, recordIndex: me.recordIndex } }, rowViewModel)); } if (!(result = widgets[ownerId])) { result = widgets[ownerId] = Ext.widget(Ext.apply({ ownerCmp: view, _rowContext: me, // This will spin up a VM on the grid if we don't have one that // will be shared across all instances. If we don't have a rowVM // or a viewmodel on the created object, we don't need it, but // we can't really tell until we create an instance. $vmParent: rowVM || ownerGrid.getRowContextViewModelParent(), initInheritedState: me.initInheritedStateHook, lookupViewModel: me.lookupViewModelHook }, widgetCfg)); result.$fromLocked = !!view.isLockedView; // Components initialize binding on render. // Widgets in finishRender which will not be called in this case. // That is only called when rendered by a layout. if (result.isWidget) { result.initBindable(); } else { // Components store an Element reference to their container element. // For ordinary components that is not a problem since usually // that element is also referenced by Container instance, and gets // cleaned up when Container is destroyed. However for row widgets // the container element is a cell; cells do not get Element instances // and View is not going to clean them up. // So we have to clean up explicitly when the widget is destroyed // to avoid orphan Element instances in Ext.cache. result.collectContainerElement = true; } } return result; }, getWidgets: function() { var widgets = this.widgets, id, result = []; for (id in widgets) { result.push(widgets[id]); } return result; }, handleWidgetViewChange: function(view, ownerId) { var widget = this.widgets[ownerId]; if (widget) { // In this particular case poking the ownerCmp doesn't really have any significance here // since users will typically be interacting at the grid level. However, this is more // for the sake of correctness. We don't need to do anything other // than poke the reference. widget.ownerCmp = view; widget.$fromLocked = !!view.isLockedView; } }, destroy: function() { var me = this, widgets = me.widgets, widgetId, widget; for (widgetId in widgets) { widget = widgets[widgetId]; widget._rowContext = null; widget.destroy(); } Ext.destroy(me.viewModel); me.callParent(); }, privates: { initInheritedStateHook: function(inheritedState, inheritedStateInner) { var vmParent = this.$vmParent; this.self.prototype.initInheritedState.call(this, inheritedState, inheritedStateInner); if (!inheritedState.hasOwnProperty('viewModel') && vmParent) { inheritedState.viewModel = vmParent; } }, lookupViewModelHook: function(skipThis) { var ret = skipThis ? null : this.getViewModel(); if (!ret) { ret = this.$vmParent || null; } return ret; } }});