--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>The source code</title>
+ <link href="../prettify/prettify.css" type="text/css" rel="stylesheet" />
+ <script type="text/javascript" src="../prettify/prettify.js"></script>
+ <style type="text/css">
+ .highlight { display: block; background-color: #ddd; }
+ </style>
+ <script type="text/javascript">
+ function highlight() {
+ document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
+ }
+ </script>
+</head>
+<body onload="prettyPrint(); highlight();">
+ <pre class="prettyprint lang-js">// Currently has the following issues:
+// - Does not handle postEditValue
+// - Fields without editors need to sync with their values in Store
+// - starting to edit another record while already editing and dirty should probably prevent it
+// - aggregating validation messages
+// - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
+// - layout issues when changing sizes/width while hidden (layout bug)
+
+<span id='Ext-grid-RowEditor'>/**
+</span> * @class Ext.grid.RowEditor
+ * @extends Ext.form.Panel
+ *
+ * Internal utility class used to provide row editing functionality. For developers, they should use
+ * the RowEditing plugin to use this functionality with a grid.
+ *
+ * @ignore
+ */
+Ext.define('Ext.grid.RowEditor', {
+ extend: 'Ext.form.Panel',
+ requires: [
+ 'Ext.tip.ToolTip',
+ 'Ext.util.HashMap',
+ 'Ext.util.KeyNav'
+ ],
+
+ saveBtnText : 'Update',
+ cancelBtnText: 'Cancel',
+ errorsText: 'Errors',
+ dirtyText: 'You need to commit or cancel your changes',
+
+ lastScrollLeft: 0,
+ lastScrollTop: 0,
+
+ border: false,
+
+ // Change the hideMode to offsets so that we get accurate measurements when
+ // the roweditor is hidden for laying out things like a TriggerField.
+ hideMode: 'offsets',
+
+ initComponent: function() {
+ var me = this,
+ form;
+
+ me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
+
+ me.layout = {
+ type: 'hbox',
+ align: 'middle'
+ };
+
+ // Maintain field-to-column mapping
+ // It's easy to get a field from a column, but not vice versa
+ me.columns = Ext.create('Ext.util.HashMap');
+ me.columns.getKey = function(columnHeader) {
+ var f;
+ if (columnHeader.getEditor) {
+ f = columnHeader.getEditor();
+ if (f) {
+ return f.id;
+ }
+ }
+ return columnHeader.id;
+ };
+ me.mon(me.columns, {
+ add: me.onFieldAdd,
+ remove: me.onFieldRemove,
+ replace: me.onFieldReplace,
+ scope: me
+ });
+
+ me.callParent(arguments);
+
+ if (me.fields) {
+ me.setField(me.fields);
+ delete me.fields;
+ }
+
+ form = me.getForm();
+ form.trackResetOnLoad = true;
+ },
+
+ onFieldChange: function() {
+ var me = this,
+ form = me.getForm(),
+ valid = form.isValid();
+ if (me.errorSummary && me.isVisible()) {
+ me[valid ? 'hideToolTip' : 'showToolTip']();
+ }
+ if (me.floatingButtons) {
+ me.floatingButtons.child('#update').setDisabled(!valid);
+ }
+ me.isValid = valid;
+ },
+
+ afterRender: function() {
+ var me = this,
+ plugin = me.editingPlugin;
+
+ me.callParent(arguments);
+ me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
+
+ // Prevent from bubbling click events to the grid view
+ me.mon(me.el, {
+ click: Ext.emptyFn,
+ stopPropagation: true
+ });
+
+ me.el.swallowEvent([
+ 'keypress',
+ 'keydown'
+ ]);
+
+ me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
+ enter: plugin.completeEdit,
+ esc: plugin.onEscKey,
+ scope: plugin
+ });
+
+ me.mon(plugin.view, {
+ beforerefresh: me.onBeforeViewRefresh,
+ refresh: me.onViewRefresh,
+ scope: me
+ });
+ },
+
+ onBeforeViewRefresh: function(view) {
+ var me = this,
+ viewDom = view.el.dom;
+
+ if (me.el.dom.parentNode === viewDom) {
+ viewDom.removeChild(me.el.dom);
+ }
+ },
+
+ onViewRefresh: function(view) {
+ var me = this,
+ viewDom = view.el.dom,
+ context = me.context,
+ idx;
+
+ viewDom.appendChild(me.el.dom);
+
+ // Recover our row node after a view refresh
+ if (context && (idx = context.store.indexOf(context.record)) >= 0) {
+ context.row = view.getNode(idx);
+ me.reposition();
+ if (me.tooltip && me.tooltip.isVisible()) {
+ me.tooltip.setTarget(context.row);
+ }
+ } else {
+ me.editingPlugin.cancelEdit();
+ }
+ },
+
+ onCtScroll: function(e, target) {
+ var me = this,
+ scrollTop = target.scrollTop,
+ scrollLeft = target.scrollLeft;
+
+ if (scrollTop !== me.lastScrollTop) {
+ me.lastScrollTop = scrollTop;
+ if ((me.tooltip && me.tooltip.isVisible()) || me.hiddenTip) {
+ me.repositionTip();
+ }
+ }
+ if (scrollLeft !== me.lastScrollLeft) {
+ me.lastScrollLeft = scrollLeft;
+ me.reposition();
+ }
+ },
+
+ onColumnAdd: function(column) {
+ this.setField(column);
+ },
+
+ onColumnRemove: function(column) {
+ this.columns.remove(column);
+ },
+
+ onColumnResize: function(column, width) {
+ column.getEditor().setWidth(width - 2);
+ if (this.isVisible()) {
+ this.reposition();
+ }
+ },
+
+ onColumnHide: function(column) {
+ column.getEditor().hide();
+ if (this.isVisible()) {
+ this.reposition();
+ }
+ },
+
+ onColumnShow: function(column) {
+ var field = column.getEditor();
+ field.setWidth(column.getWidth() - 2).show();
+ if (this.isVisible()) {
+ this.reposition();
+ }
+ },
+
+ onColumnMove: function(column, fromIdx, toIdx) {
+ var field = column.getEditor();
+ if (this.items.indexOf(field) != toIdx) {
+ this.move(fromIdx, toIdx);
+ }
+ },
+
+ onFieldAdd: function(map, fieldId, column) {
+ var me = this,
+ colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
+ field = column.getEditor({ xtype: 'displayfield' });
+
+ me.insert(colIdx, field);
+ },
+
+ onFieldRemove: function(map, fieldId, column) {
+ var me = this,
+ field = column.getEditor(),
+ fieldEl = field.el;
+ me.remove(field, false);
+ if (fieldEl) {
+ fieldEl.remove();
+ }
+ },
+
+ onFieldReplace: function(map, fieldId, column, oldColumn) {
+ var me = this;
+ me.onFieldRemove(map, fieldId, oldColumn);
+ },
+
+ clearFields: function() {
+ var me = this,
+ map = me.columns;
+ map.each(function(fieldId) {
+ map.removeAtKey(fieldId);
+ });
+ },
+
+ getFloatingButtons: function() {
+ var me = this,
+ cssPrefix = Ext.baseCSSPrefix,
+ btnsCss = cssPrefix + 'grid-row-editor-buttons',
+ plugin = me.editingPlugin,
+ btns;
+
+ if (!me.floatingButtons) {
+ btns = me.floatingButtons = Ext.create('Ext.Container', {
+ renderTpl: [
+ '<div class="{baseCls}-ml"></div>',
+ '<div class="{baseCls}-mr"></div>',
+ '<div class="{baseCls}-bl"></div>',
+ '<div class="{baseCls}-br"></div>',
+ '<div class="{baseCls}-bc"></div>'
+ ],
+
+ renderTo: me.el,
+ baseCls: btnsCss,
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ defaults: {
+ margins: '0 1 0 1'
+ },
+ items: [{
+ itemId: 'update',
+ flex: 1,
+ xtype: 'button',
+ handler: plugin.completeEdit,
+ scope: plugin,
+ text: me.saveBtnText,
+ disabled: !me.isValid
+ }, {
+ flex: 1,
+ xtype: 'button',
+ handler: plugin.cancelEdit,
+ scope: plugin,
+ text: me.cancelBtnText
+ }]
+ });
+
+ // Prevent from bubbling click events to the grid view
+ me.mon(btns.el, {
+ // BrowserBug: Opera 11.01
+ // causes the view to scroll when a button is focused from mousedown
+ mousedown: Ext.emptyFn,
+ click: Ext.emptyFn,
+ stopEvent: true
+ });
+ }
+ return me.floatingButtons;
+ },
+
+ reposition: function(animateConfig) {
+ var me = this,
+ context = me.context,
+ row = context && Ext.get(context.row),
+ btns = me.getFloatingButtons(),
+ btnEl = btns.el,
+ grid = me.editingPlugin.grid,
+ viewEl = grid.view.el,
+ scroller = grid.verticalScroller,
+
+ // always get data from ColumnModel as its what drives
+ // the GridView's sizing
+ mainBodyWidth = grid.headerCt.getFullWidth(),
+ scrollerWidth = grid.getWidth(),
+
+ // use the minimum as the columns may not fill up the entire grid
+ // width
+ width = Math.min(mainBodyWidth, scrollerWidth),
+ scrollLeft = grid.view.el.dom.scrollLeft,
+ btnWidth = btns.getWidth(),
+ left = (width - btnWidth) / 2 + scrollLeft,
+ y, rowH, newHeight,
+
+ invalidateScroller = function() {
+ if (scroller) {
+ scroller.invalidate();
+ btnEl.scrollIntoView(viewEl, false);
+ }
+ if (animateConfig && animateConfig.callback) {
+ animateConfig.callback.call(animateConfig.scope || me);
+ }
+ };
+
+ // need to set both top/left
+ if (row && Ext.isElement(row.dom)) {
+ // Bring our row into view if necessary, so a row editor that's already
+ // visible and animated to the row will appear smooth
+ row.scrollIntoView(viewEl, false);
+
+ // Get the y position of the row relative to its top-most static parent.
+ // offsetTop will be relative to the table, and is incorrect
+ // when mixed with certain grid features (e.g., grouping).
+ y = row.getXY()[1] - 5;
+ rowH = row.getHeight();
+ newHeight = rowH + 10;
+
+ // IE doesn't set the height quite right.
+ // This isn't a border-box issue, it even happens
+ // in IE8 and IE7 quirks.
+ // TODO: Test in IE9!
+ if (Ext.isIE) {
+ newHeight += 2;
+ }
+
+ // Set editor height to match the row height
+ if (me.getHeight() != newHeight) {
+ me.setHeight(newHeight);
+ me.el.setLeft(0);
+ }
+
+ if (animateConfig) {
+ var animObj = {
+ to: {
+ y: y
+ },
+ duration: animateConfig.duration || 125,
+ listeners: {
+ afteranimate: function() {
+ invalidateScroller();
+ y = row.getXY()[1] - 5;
+ me.el.setY(y);
+ }
+ }
+ };
+ me.animate(animObj);
+ } else {
+ me.el.setY(y);
+ invalidateScroller();
+ }
+ }
+ if (me.getWidth() != mainBodyWidth) {
+ me.setWidth(mainBodyWidth);
+ }
+ btnEl.setLeft(left);
+ },
+
+ getEditor: function(fieldInfo) {
+ var me = this;
+
+ if (Ext.isNumber(fieldInfo)) {
+ // Query only form fields. This just future-proofs us in case we add
+ // other components to RowEditor later on. Don't want to mess with
+ // indices.
+ return me.query('>[isFormField]')[fieldInfo];
+ } else if (fieldInfo instanceof Ext.grid.column.Column) {
+ return fieldInfo.getEditor();
+ }
+ },
+
+ removeField: function(field) {
+ var me = this;
+
+ // Incase we pass a column instead, which is fine
+ field = me.getEditor(field);
+ me.mun(field, 'validitychange', me.onValidityChange, me);
+
+ // Remove field/column from our mapping, which will fire the event to
+ // remove the field from our container
+ me.columns.removeKey(field.id);
+ },
+
+ setField: function(column) {
+ var me = this,
+ field;
+
+ if (Ext.isArray(column)) {
+ Ext.Array.forEach(column, me.setField, me);
+ return;
+ }
+
+ // Get a default display field if necessary
+ field = column.getEditor(null, {
+ xtype: 'displayfield',
+ // Default display fields will not return values. This is done because
+ // the display field will pick up column renderers from the grid.
+ getModelData: function() {
+ return null;
+ }
+ });
+ field.margins = '0 0 0 2';
+ field.setWidth(column.getDesiredWidth() - 2);
+ me.mon(field, 'change', me.onFieldChange, me);
+
+ // Maintain mapping of fields-to-columns
+ // This will fire events that maintain our container items
+ me.columns.add(field.id, column);
+
+ if (me.isVisible() && me.context) {
+ me.renderColumnData(field, me.context.record);
+ }
+ },
+
+ loadRecord: function(record) {
+ var me = this,
+ form = me.getForm();
+ form.loadRecord(record);
+ if (form.isValid()) {
+ me.hideToolTip();
+ } else {
+ me.showToolTip();
+ }
+
+ // render display fields so they honor the column renderer/template
+ Ext.Array.forEach(me.query('>displayfield'), function(field) {
+ me.renderColumnData(field, record);
+ }, me);
+ },
+
+ renderColumnData: function(field, record) {
+ var me = this,
+ grid = me.editingPlugin.grid,
+ headerCt = grid.headerCt,
+ view = grid.view,
+ store = view.store,
+ column = me.columns.get(field.id),
+ value = record.get(column.dataIndex);
+
+ // honor our column's renderer (TemplateHeader sets renderer for us!)
+ if (column.renderer) {
+ var metaData = { tdCls: '', style: '' },
+ rowIdx = store.indexOf(record),
+ colIdx = headerCt.getHeaderIndex(column);
+
+ value = column.renderer.call(
+ column.scope || headerCt.ownerCt,
+ value,
+ metaData,
+ record,
+ rowIdx,
+ colIdx,
+ store,
+ view
+ );
+ }
+
+ field.setRawValue(value);
+ field.resetOriginalValue();
+ },
+
+ beforeEdit: function() {
+ var me = this;
+
+ if (me.isVisible() && !me.autoCancel && me.isDirty()) {
+ me.showToolTip();
+ return false;
+ }
+ },
+
+<span id='Ext-grid-RowEditor-method-startEdit'> /**
+</span> * Start editing the specified grid at the specified position.
+ * @param {Model} record The Store data record which backs the row to be edited.
+ * @param {Model} columnHeader The Column object defining the column to be edited.
+ */
+ startEdit: function(record, columnHeader) {
+ var me = this,
+ grid = me.editingPlugin.grid,
+ view = grid.getView(),
+ store = grid.store,
+ context = me.context = Ext.apply(me.editingPlugin.context, {
+ view: grid.getView(),
+ store: store
+ });
+
+ // make sure our row is selected before editing
+ context.grid.getSelectionModel().select(record);
+
+ // Reload the record data
+ me.loadRecord(record);
+
+ if (!me.isVisible()) {
+ me.show();
+ me.focusContextCell();
+ } else {
+ me.reposition({
+ callback: this.focusContextCell
+ });
+ }
+ },
+
+ // Focus the cell on start edit based upon the current context
+ focusContextCell: function() {
+ var field = this.getEditor(this.context.colIdx);
+ if (field && field.focus) {
+ field.focus();
+ }
+ },
+
+ cancelEdit: function() {
+ var me = this,
+ form = me.getForm();
+
+ me.hide();
+ form.clearInvalid();
+ form.reset();
+ },
+
+ completeEdit: function() {
+ var me = this,
+ form = me.getForm();
+
+ if (!form.isValid()) {
+ return;
+ }
+
+ form.updateRecord(me.context.record);
+ me.hide();
+ return true;
+ },
+
+ onShow: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.reposition();
+ },
+
+ onHide: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.hideToolTip();
+ me.invalidateScroller();
+ if (me.context) {
+ me.context.view.focus();
+ me.context = null;
+ }
+ },
+
+ isDirty: function() {
+ var me = this,
+ form = me.getForm();
+ return form.isDirty();
+ },
+
+ getToolTip: function() {
+ var me = this,
+ tip;
+
+ if (!me.tooltip) {
+ tip = me.tooltip = Ext.createWidget('tooltip', {
+ cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
+ title: me.errorsText,
+ autoHide: false,
+ closable: true,
+ closeAction: 'disable',
+ anchor: 'left'
+ });
+ }
+ return me.tooltip;
+ },
+
+ hideToolTip: function() {
+ var me = this,
+ tip = me.getToolTip();
+ if (tip.rendered) {
+ tip.disable();
+ }
+ me.hiddenTip = false;
+ },
+
+ showToolTip: function() {
+ var me = this,
+ tip = me.getToolTip(),
+ context = me.context,
+ row = Ext.get(context.row),
+ viewEl = context.grid.view.el;
+
+ tip.setTarget(row);
+ tip.showAt([-10000, -10000]);
+ tip.body.update(me.getErrors());
+ tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
+ me.repositionTip();
+ tip.doLayout();
+ tip.enable();
+ },
+
+ repositionTip: function() {
+ var me = this,
+ tip = me.getToolTip(),
+ context = me.context,
+ row = Ext.get(context.row),
+ viewEl = context.grid.view.el,
+ viewHeight = viewEl.getHeight(),
+ viewTop = me.lastScrollTop,
+ viewBottom = viewTop + viewHeight,
+ rowHeight = row.getHeight(),
+ rowTop = row.dom.offsetTop,
+ rowBottom = rowTop + rowHeight;
+
+ if (rowBottom > viewTop && rowTop < viewBottom) {
+ tip.show();
+ me.hiddenTip = false;
+ } else {
+ tip.hide();
+ me.hiddenTip = true;
+ }
+ },
+
+ getErrors: function() {
+ var me = this,
+ dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
+ errors = [];
+
+ Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
+ errors = errors.concat(
+ Ext.Array.map(field.getErrors(), function(e) {
+ return '<li>' + e + '</li>';
+ })
+ );
+ }, me);
+
+ return dirtyText + '<ul>' + errors.join('') + '</ul>';
+ },
+
+ invalidateScroller: function() {
+ var me = this,
+ context = me.context,
+ scroller = context.grid.verticalScroller;
+
+ if (scroller) {
+ scroller.invalidate();
+ }
+ }
+});</pre>
+</body>
+</html>