1 // Currently has the following issues:
2 // - Does not handle postEditValue
3 // - Fields without editors need to sync with their values in Store
4 // - starting to edit another record while already editing and dirty should probably prevent it
5 // - aggregating validation messages
6 // - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
7 // - layout issues when changing sizes/width while hidden (layout bug)
10 * @class Ext.grid.RowEditor
11 * @extends Ext.form.Panel
13 * Internal utility class used to provide row editing functionality. For developers, they should use
14 * the RowEditing plugin to use this functionality with a grid.
18 Ext.define('Ext.grid.RowEditor', {
19 extend: 'Ext.form.Panel',
26 saveBtnText : 'Update',
27 cancelBtnText: 'Cancel',
29 dirtyText: 'You need to commit or cancel your changes',
36 initComponent: function() {
40 me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
47 // Maintain field-to-column mapping
48 // It's easy to get a field from a column, but not vice versa
49 me.columns = Ext.create('Ext.util.HashMap');
50 me.columns.getKey = function(columnHeader) {
52 if (columnHeader.getEditor) {
53 f = columnHeader.getEditor();
58 return columnHeader.id;
62 remove: me.onFieldRemove,
63 replace: me.onFieldReplace,
67 me.callParent(arguments);
70 me.setField(me.fields);
75 form.trackResetOnLoad = true;
78 onFieldChange: function() {
81 valid = form.isValid();
82 if (me.errorSummary && me.isVisible()) {
83 me[valid ? 'hideToolTip' : 'showToolTip']();
85 if (me.floatingButtons) {
86 me.floatingButtons.child('#update').setDisabled(!valid);
91 afterRender: function() {
93 plugin = me.editingPlugin;
95 me.callParent(arguments);
96 me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
98 // Prevent from bubbling click events to the grid view
101 stopPropagation: true
109 me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
110 enter: plugin.completeEdit,
111 esc: plugin.onEscKey,
115 me.mon(plugin.view, {
116 beforerefresh: me.onBeforeViewRefresh,
117 refresh: me.onViewRefresh,
122 onBeforeViewRefresh: function(view) {
124 viewDom = view.el.dom;
126 if (me.el.dom.parentNode === viewDom) {
127 viewDom.removeChild(me.el.dom);
131 onViewRefresh: function(view) {
133 viewDom = view.el.dom,
134 context = me.context,
137 viewDom.appendChild(me.el.dom);
139 // Recover our row node after a view refresh
140 if (context && (idx = context.store.indexOf(context.record)) >= 0) {
141 context.row = view.getNode(idx);
143 if (me.tooltip && me.tooltip.isVisible()) {
144 me.tooltip.setTarget(context.row);
147 me.editingPlugin.cancelEdit();
151 onCtScroll: function(e, target) {
153 scrollTop = target.scrollTop,
154 scrollLeft = target.scrollLeft;
156 if (scrollTop !== me.lastScrollTop) {
157 me.lastScrollTop = scrollTop;
158 if ((me.tooltip && me.tooltip.isVisible()) || me.hiddenTip) {
162 if (scrollLeft !== me.lastScrollLeft) {
163 me.lastScrollLeft = scrollLeft;
168 onColumnAdd: function(column) {
169 this.setField(column);
172 onColumnRemove: function(column) {
173 this.columns.remove(column);
176 onColumnResize: function(column, width) {
177 column.getEditor().setWidth(width - 2);
178 if (this.isVisible()) {
183 onColumnHide: function(column) {
184 column.getEditor().hide();
185 if (this.isVisible()) {
190 onColumnShow: function(column) {
191 var field = column.getEditor();
192 field.setWidth(column.getWidth() - 2).show();
193 if (this.isVisible()) {
198 onColumnMove: function(column, fromIdx, toIdx) {
199 var field = column.getEditor();
200 if (this.items.indexOf(field) != toIdx) {
201 this.move(fromIdx, toIdx);
205 onFieldAdd: function(hm, fieldId, column) {
207 colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
208 field = column.getEditor({ xtype: 'displayfield' });
210 me.insert(colIdx, field);
213 onFieldRemove: function(hm, fieldId, column) {
215 field = column.getEditor(),
216 fieldDom = field.el.dom;
217 me.remove(field, false);
218 fieldDom.parentNode.removeChild(fieldDom);
221 onFieldReplace: function(hm, fieldId, column, oldColumn) {
223 me.onFieldRemove(hm, fieldId, oldColumn);
226 clearFields: function() {
229 hm.each(function(fieldId) {
230 hm.removeAtKey(fieldId);
234 getFloatingButtons: function() {
236 cssPrefix = Ext.baseCSSPrefix,
237 btnsCss = cssPrefix + 'grid-row-editor-buttons',
238 plugin = me.editingPlugin,
241 if (!me.floatingButtons) {
242 btns = me.floatingButtons = Ext.create('Ext.Container', {
244 '<div class="{baseCls}-ml"></div>',
245 '<div class="{baseCls}-mr"></div>',
246 '<div class="{baseCls}-bl"></div>',
247 '<div class="{baseCls}-br"></div>',
248 '<div class="{baseCls}-bc"></div>'
264 handler: plugin.completeEdit,
266 text: me.saveBtnText,
267 disabled: !me.isValid
271 handler: plugin.cancelEdit,
273 text: me.cancelBtnText
277 // Prevent from bubbling click events to the grid view
279 // BrowserBug: Opera 11.01
280 // causes the view to scroll when a button is focused from mousedown
281 mousedown: Ext.emptyFn,
286 return me.floatingButtons;
289 reposition: function(animateConfig) {
291 context = me.context,
292 row = context && Ext.get(context.row),
293 btns = me.getFloatingButtons(),
295 grid = me.editingPlugin.grid,
296 viewEl = grid.view.el,
297 scroller = grid.verticalScroller,
299 // always get data from ColumnModel as its what drives
300 // the GridView's sizing
301 mainBodyWidth = grid.headerCt.getFullWidth(),
302 scrollerWidth = grid.getWidth(),
304 // use the minimum as the columns may not fill up the entire grid
306 width = Math.min(mainBodyWidth, scrollerWidth),
307 scrollLeft = grid.view.el.dom.scrollLeft,
308 btnWidth = btns.getWidth(),
309 left = (width - btnWidth) / 2 + scrollLeft,
312 invalidateScroller = function() {
314 scroller.invalidate();
315 btnEl.scrollIntoView(viewEl, false);
317 if (animateConfig && animateConfig.callback) {
318 animateConfig.callback.call(animateConfig.scope || me);
322 // need to set both top/left
323 if (row && Ext.isElement(row.dom)) {
324 // Bring our row into view if necessary, so a row editor that's already
325 // visible and animated to the row will appear smooth
326 row.scrollIntoView(viewEl, false);
328 // Get the y position of the row relative to its top-most static parent.
329 // offsetTop will be relative to the table, and is incorrect
330 // when mixed with certain grid features (e.g., grouping).
331 y = row.getXY()[1] - 5;
332 rowH = row.getHeight();
333 newHeight = rowH + 10;
335 // IE doesn't set the height quite right.
336 // This isn't a border-box issue, it even happens
337 // in IE8 and IE7 quirks.
338 // TODO: Test in IE9!
343 // Set editor height to match the row height
344 if (me.getHeight() != newHeight) {
345 me.setHeight(newHeight);
354 duration: animateConfig.duration || 125,
356 afteranimate: function() {
357 invalidateScroller();
358 y = row.getXY()[1] - 5;
366 invalidateScroller();
369 if (me.getWidth() != mainBodyWidth) {
370 me.setWidth(mainBodyWidth);
375 getEditor: function(fieldInfo) {
378 if (Ext.isNumber(fieldInfo)) {
379 // Query only form fields. This just future-proofs us in case we add
380 // other components to RowEditor later on. Don't want to mess with
382 return me.query('>[isFormField]')[fieldInfo];
383 } else if (fieldInfo instanceof Ext.grid.column.Column) {
384 return fieldInfo.getEditor();
388 removeField: function(field) {
391 // Incase we pass a column instead, which is fine
392 field = me.getEditor(field);
393 me.mun(field, 'validitychange', me.onValidityChange, me);
395 // Remove field/column from our mapping, which will fire the event to
396 // remove the field from our container
397 me.columns.removeKey(field.id);
400 setField: function(column) {
404 if (Ext.isArray(column)) {
405 Ext.Array.forEach(column, me.setField, me);
409 // Get a default display field if necessary
410 field = column.getEditor(null, { xtype: 'displayfield' });
411 field.margins = '0 0 0 2';
412 field.setWidth(column.getWidth() - 2);
413 me.mon(field, 'change', me.onFieldChange, me);
415 // Maintain mapping of fields-to-columns
416 // This will fire events that maintain our container items
417 me.columns.add(field.id, column);
420 loadRecord: function(record) {
423 form.loadRecord(record);
424 if (form.isValid()) {
430 // render display fields so they honor the column renderer/template
431 Ext.Array.forEach(me.query('>displayfield'), function(field) {
432 me.renderColumnData(field, record);
436 renderColumnData: function(field, record) {
438 grid = me.editingPlugin.grid,
439 headerCt = grid.headerCt,
442 column = me.columns.get(field.id),
443 value = field.getRawValue();
445 // honor our column's renderer (TemplateHeader sets renderer for us!)
446 if (column.renderer) {
447 var metaData = { tdCls: '', style: '' },
448 rowIdx = store.indexOf(record),
449 colIdx = headerCt.getHeaderIndex(column);
451 value = column.renderer.call(
452 column.scope || headerCt.ownerCt,
463 field.setRawValue(value);
464 field.resetOriginalValue();
467 beforeEdit: function() {
470 if (me.isVisible() && !me.autoCancel && me.isDirty()) {
477 * Start editing the specified grid at the specified position.
478 * @param {Model} record The Store data record which backs the row to be edited.
479 * @param {Model} columnHeader The Column object defining the column to be edited.
481 startEdit: function(record, columnHeader) {
483 grid = me.editingPlugin.grid,
484 view = grid.getView(),
486 context = me.context = Ext.apply(me.editingPlugin.context, {
487 view: grid.getView(),
491 // make sure our row is selected before editing
492 context.grid.getSelectionModel().select(record);
494 // Reload the record data
495 me.loadRecord(record);
497 if (!me.isVisible()) {
499 me.focusContextCell();
502 callback: this.focusContextCell
507 // Focus the cell on start edit based upon the current context
508 focusContextCell: function() {
509 var field = this.getEditor(this.context.colIdx);
510 if (field && field.focus) {
515 cancelEdit: function() {
524 completeEdit: function() {
528 if (!form.isValid()) {
532 form.updateRecord(me.context.record);
539 me.callParent(arguments);
545 me.callParent(arguments);
547 me.invalidateScroller();
549 me.context.view.focus();
554 isDirty: function() {
557 return form.isDirty();
560 getToolTip: function() {
565 tip = me.tooltip = Ext.createWidget('tooltip', {
566 cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
567 title: me.errorsText,
570 closeAction: 'disable',
577 hideToolTip: function() {
579 tip = me.getToolTip();
583 me.hiddenTip = false;
586 showToolTip: function() {
588 tip = me.getToolTip(),
589 context = me.context,
590 row = Ext.get(context.row),
591 viewEl = context.grid.view.el;
594 tip.showAt([-10000, -10000]);
595 tip.body.update(me.getErrors());
596 tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
602 repositionTip: function() {
604 tip = me.getToolTip(),
605 context = me.context,
606 row = Ext.get(context.row),
607 viewEl = context.grid.view.el,
608 viewHeight = viewEl.getHeight(),
609 viewTop = me.lastScrollTop,
610 viewBottom = viewTop + viewHeight,
611 rowHeight = row.getHeight(),
612 rowTop = row.dom.offsetTop,
613 rowBottom = rowTop + rowHeight;
615 if (rowBottom > viewTop && rowTop < viewBottom) {
617 me.hiddenTip = false;
624 getErrors: function() {
626 dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
629 Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
630 errors = errors.concat(
631 Ext.Array.map(field.getErrors(), function(e) {
632 return '<li>' + e + '</li>';
637 return dirtyText + '<ul>' + errors.join('') + '</ul>';
640 invalidateScroller: function() {
642 context = me.context,
643 scroller = context.grid.verticalScroller;
646 scroller.invalidate();