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 // Change the hideMode to offsets so that we get accurate measurements when
37 // the roweditor is hidden for laying out things like a TriggerField.
40 initComponent: function() {
44 me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
51 // Maintain field-to-column mapping
52 // It's easy to get a field from a column, but not vice versa
53 me.columns = Ext.create('Ext.util.HashMap');
54 me.columns.getKey = function(columnHeader) {
56 if (columnHeader.getEditor) {
57 f = columnHeader.getEditor();
62 return columnHeader.id;
66 remove: me.onFieldRemove,
67 replace: me.onFieldReplace,
71 me.callParent(arguments);
74 me.setField(me.fields);
79 form.trackResetOnLoad = true;
82 onFieldChange: function() {
85 valid = form.isValid();
86 if (me.errorSummary && me.isVisible()) {
87 me[valid ? 'hideToolTip' : 'showToolTip']();
89 if (me.floatingButtons) {
90 me.floatingButtons.child('#update').setDisabled(!valid);
95 afterRender: function() {
97 plugin = me.editingPlugin;
99 me.callParent(arguments);
100 me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
102 // Prevent from bubbling click events to the grid view
105 stopPropagation: true
113 me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
114 enter: plugin.completeEdit,
115 esc: plugin.onEscKey,
119 me.mon(plugin.view, {
120 beforerefresh: me.onBeforeViewRefresh,
121 refresh: me.onViewRefresh,
126 onBeforeViewRefresh: function(view) {
128 viewDom = view.el.dom;
130 if (me.el.dom.parentNode === viewDom) {
131 viewDom.removeChild(me.el.dom);
135 onViewRefresh: function(view) {
137 viewDom = view.el.dom,
138 context = me.context,
141 viewDom.appendChild(me.el.dom);
143 // Recover our row node after a view refresh
144 if (context && (idx = context.store.indexOf(context.record)) >= 0) {
145 context.row = view.getNode(idx);
147 if (me.tooltip && me.tooltip.isVisible()) {
148 me.tooltip.setTarget(context.row);
151 me.editingPlugin.cancelEdit();
155 onCtScroll: function(e, target) {
157 scrollTop = target.scrollTop,
158 scrollLeft = target.scrollLeft;
160 if (scrollTop !== me.lastScrollTop) {
161 me.lastScrollTop = scrollTop;
162 if ((me.tooltip && me.tooltip.isVisible()) || me.hiddenTip) {
166 if (scrollLeft !== me.lastScrollLeft) {
167 me.lastScrollLeft = scrollLeft;
172 onColumnAdd: function(column) {
173 this.setField(column);
176 onColumnRemove: function(column) {
177 this.columns.remove(column);
180 onColumnResize: function(column, width) {
181 column.getEditor().setWidth(width - 2);
182 if (this.isVisible()) {
187 onColumnHide: function(column) {
188 column.getEditor().hide();
189 if (this.isVisible()) {
194 onColumnShow: function(column) {
195 var field = column.getEditor();
196 field.setWidth(column.getWidth() - 2).show();
197 if (this.isVisible()) {
202 onColumnMove: function(column, fromIdx, toIdx) {
203 var field = column.getEditor();
204 if (this.items.indexOf(field) != toIdx) {
205 this.move(fromIdx, toIdx);
209 onFieldAdd: function(map, fieldId, column) {
211 colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
212 field = column.getEditor({ xtype: 'displayfield' });
214 me.insert(colIdx, field);
217 onFieldRemove: function(map, fieldId, column) {
219 field = column.getEditor(),
221 me.remove(field, false);
227 onFieldReplace: function(map, fieldId, column, oldColumn) {
229 me.onFieldRemove(map, fieldId, oldColumn);
232 clearFields: function() {
235 map.each(function(fieldId) {
236 map.removeAtKey(fieldId);
240 getFloatingButtons: function() {
242 cssPrefix = Ext.baseCSSPrefix,
243 btnsCss = cssPrefix + 'grid-row-editor-buttons',
244 plugin = me.editingPlugin,
247 if (!me.floatingButtons) {
248 btns = me.floatingButtons = Ext.create('Ext.Container', {
250 '<div class="{baseCls}-ml"></div>',
251 '<div class="{baseCls}-mr"></div>',
252 '<div class="{baseCls}-bl"></div>',
253 '<div class="{baseCls}-br"></div>',
254 '<div class="{baseCls}-bc"></div>'
270 handler: plugin.completeEdit,
272 text: me.saveBtnText,
273 disabled: !me.isValid
277 handler: plugin.cancelEdit,
279 text: me.cancelBtnText
283 // Prevent from bubbling click events to the grid view
285 // BrowserBug: Opera 11.01
286 // causes the view to scroll when a button is focused from mousedown
287 mousedown: Ext.emptyFn,
292 return me.floatingButtons;
295 reposition: function(animateConfig) {
297 context = me.context,
298 row = context && Ext.get(context.row),
299 btns = me.getFloatingButtons(),
301 grid = me.editingPlugin.grid,
302 viewEl = grid.view.el,
303 scroller = grid.verticalScroller,
305 // always get data from ColumnModel as its what drives
306 // the GridView's sizing
307 mainBodyWidth = grid.headerCt.getFullWidth(),
308 scrollerWidth = grid.getWidth(),
310 // use the minimum as the columns may not fill up the entire grid
312 width = Math.min(mainBodyWidth, scrollerWidth),
313 scrollLeft = grid.view.el.dom.scrollLeft,
314 btnWidth = btns.getWidth(),
315 left = (width - btnWidth) / 2 + scrollLeft,
318 invalidateScroller = function() {
320 scroller.invalidate();
321 btnEl.scrollIntoView(viewEl, false);
323 if (animateConfig && animateConfig.callback) {
324 animateConfig.callback.call(animateConfig.scope || me);
328 // need to set both top/left
329 if (row && Ext.isElement(row.dom)) {
330 // Bring our row into view if necessary, so a row editor that's already
331 // visible and animated to the row will appear smooth
332 row.scrollIntoView(viewEl, false);
334 // Get the y position of the row relative to its top-most static parent.
335 // offsetTop will be relative to the table, and is incorrect
336 // when mixed with certain grid features (e.g., grouping).
337 y = row.getXY()[1] - 5;
338 rowH = row.getHeight();
339 newHeight = rowH + 10;
341 // IE doesn't set the height quite right.
342 // This isn't a border-box issue, it even happens
343 // in IE8 and IE7 quirks.
344 // TODO: Test in IE9!
349 // Set editor height to match the row height
350 if (me.getHeight() != newHeight) {
351 me.setHeight(newHeight);
360 duration: animateConfig.duration || 125,
362 afteranimate: function() {
363 invalidateScroller();
364 y = row.getXY()[1] - 5;
372 invalidateScroller();
375 if (me.getWidth() != mainBodyWidth) {
376 me.setWidth(mainBodyWidth);
381 getEditor: function(fieldInfo) {
384 if (Ext.isNumber(fieldInfo)) {
385 // Query only form fields. This just future-proofs us in case we add
386 // other components to RowEditor later on. Don't want to mess with
388 return me.query('>[isFormField]')[fieldInfo];
389 } else if (fieldInfo instanceof Ext.grid.column.Column) {
390 return fieldInfo.getEditor();
394 removeField: function(field) {
397 // Incase we pass a column instead, which is fine
398 field = me.getEditor(field);
399 me.mun(field, 'validitychange', me.onValidityChange, me);
401 // Remove field/column from our mapping, which will fire the event to
402 // remove the field from our container
403 me.columns.removeKey(field.id);
406 setField: function(column) {
410 if (Ext.isArray(column)) {
411 Ext.Array.forEach(column, me.setField, me);
415 // Get a default display field if necessary
416 field = column.getEditor(null, {
417 xtype: 'displayfield',
418 // Default display fields will not return values. This is done because
419 // the display field will pick up column renderers from the grid.
420 getModelData: function() {
424 field.margins = '0 0 0 2';
425 field.setWidth(column.getDesiredWidth() - 2);
426 me.mon(field, 'change', me.onFieldChange, me);
428 // Maintain mapping of fields-to-columns
429 // This will fire events that maintain our container items
430 me.columns.add(field.id, column);
432 if (me.isVisible() && me.context) {
433 me.renderColumnData(field, me.context.record);
437 loadRecord: function(record) {
440 form.loadRecord(record);
441 if (form.isValid()) {
447 // render display fields so they honor the column renderer/template
448 Ext.Array.forEach(me.query('>displayfield'), function(field) {
449 me.renderColumnData(field, record);
453 renderColumnData: function(field, record) {
455 grid = me.editingPlugin.grid,
456 headerCt = grid.headerCt,
459 column = me.columns.get(field.id),
460 value = record.get(column.dataIndex);
462 // honor our column's renderer (TemplateHeader sets renderer for us!)
463 if (column.renderer) {
464 var metaData = { tdCls: '', style: '' },
465 rowIdx = store.indexOf(record),
466 colIdx = headerCt.getHeaderIndex(column);
468 value = column.renderer.call(
469 column.scope || headerCt.ownerCt,
480 field.setRawValue(value);
481 field.resetOriginalValue();
484 beforeEdit: function() {
487 if (me.isVisible() && !me.autoCancel && me.isDirty()) {
494 * Start editing the specified grid at the specified position.
495 * @param {Model} record The Store data record which backs the row to be edited.
496 * @param {Model} columnHeader The Column object defining the column to be edited.
498 startEdit: function(record, columnHeader) {
500 grid = me.editingPlugin.grid,
501 view = grid.getView(),
503 context = me.context = Ext.apply(me.editingPlugin.context, {
504 view: grid.getView(),
508 // make sure our row is selected before editing
509 context.grid.getSelectionModel().select(record);
511 // Reload the record data
512 me.loadRecord(record);
514 if (!me.isVisible()) {
516 me.focusContextCell();
519 callback: this.focusContextCell
524 // Focus the cell on start edit based upon the current context
525 focusContextCell: function() {
526 var field = this.getEditor(this.context.colIdx);
527 if (field && field.focus) {
532 cancelEdit: function() {
541 completeEdit: function() {
545 if (!form.isValid()) {
549 form.updateRecord(me.context.record);
556 me.callParent(arguments);
562 me.callParent(arguments);
564 me.invalidateScroller();
566 me.context.view.focus();
571 isDirty: function() {
574 return form.isDirty();
577 getToolTip: function() {
582 tip = me.tooltip = Ext.createWidget('tooltip', {
583 cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
584 title: me.errorsText,
587 closeAction: 'disable',
594 hideToolTip: function() {
596 tip = me.getToolTip();
600 me.hiddenTip = false;
603 showToolTip: function() {
605 tip = me.getToolTip(),
606 context = me.context,
607 row = Ext.get(context.row),
608 viewEl = context.grid.view.el;
611 tip.showAt([-10000, -10000]);
612 tip.body.update(me.getErrors());
613 tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
619 repositionTip: function() {
621 tip = me.getToolTip(),
622 context = me.context,
623 row = Ext.get(context.row),
624 viewEl = context.grid.view.el,
625 viewHeight = viewEl.getHeight(),
626 viewTop = me.lastScrollTop,
627 viewBottom = viewTop + viewHeight,
628 rowHeight = row.getHeight(),
629 rowTop = row.dom.offsetTop,
630 rowBottom = rowTop + rowHeight;
632 if (rowBottom > viewTop && rowTop < viewBottom) {
634 me.hiddenTip = false;
641 getErrors: function() {
643 dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
646 Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
647 errors = errors.concat(
648 Ext.Array.map(field.getErrors(), function(e) {
649 return '<li>' + e + '</li>';
654 return dirtyText + '<ul>' + errors.join('') + '</ul>';
657 invalidateScroller: function() {
659 context = me.context,
660 scroller = context.grid.verticalScroller;
663 scroller.invalidate();