3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
15 // Currently has the following issues:
16 // - Does not handle postEditValue
17 // - Fields without editors need to sync with their values in Store
18 // - starting to edit another record while already editing and dirty should probably prevent it
19 // - aggregating validation messages
20 // - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
21 // - layout issues when changing sizes/width while hidden (layout bug)
24 * @class Ext.grid.RowEditor
25 * @extends Ext.form.Panel
27 * Internal utility class used to provide row editing functionality. For developers, they should use
28 * the RowEditing plugin to use this functionality with a grid.
32 Ext.define('Ext.grid.RowEditor', {
33 extend: 'Ext.form.Panel',
40 saveBtnText : 'Update',
41 cancelBtnText: 'Cancel',
43 dirtyText: 'You need to commit or cancel your changes',
50 // Change the hideMode to offsets so that we get accurate measurements when
51 // the roweditor is hidden for laying out things like a TriggerField.
54 initComponent: function() {
58 me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
65 // Maintain field-to-column mapping
66 // It's easy to get a field from a column, but not vice versa
67 me.columns = Ext.create('Ext.util.HashMap');
68 me.columns.getKey = function(columnHeader) {
70 if (columnHeader.getEditor) {
71 f = columnHeader.getEditor();
76 return columnHeader.id;
80 remove: me.onFieldRemove,
81 replace: me.onFieldReplace,
85 me.callParent(arguments);
88 me.setField(me.fields);
93 form.trackResetOnLoad = true;
96 onFieldChange: function() {
99 valid = form.isValid();
100 if (me.errorSummary && me.isVisible()) {
101 me[valid ? 'hideToolTip' : 'showToolTip']();
103 if (me.floatingButtons) {
104 me.floatingButtons.child('#update').setDisabled(!valid);
109 afterRender: function() {
111 plugin = me.editingPlugin;
113 me.callParent(arguments);
114 me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
116 // Prevent from bubbling click events to the grid view
119 stopPropagation: true
127 me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
128 enter: plugin.completeEdit,
129 esc: plugin.onEscKey,
133 me.mon(plugin.view, {
134 beforerefresh: me.onBeforeViewRefresh,
135 refresh: me.onViewRefresh,
140 onBeforeViewRefresh: function(view) {
142 viewDom = view.el.dom;
144 if (me.el.dom.parentNode === viewDom) {
145 viewDom.removeChild(me.el.dom);
149 onViewRefresh: function(view) {
151 viewDom = view.el.dom,
152 context = me.context,
155 viewDom.appendChild(me.el.dom);
157 // Recover our row node after a view refresh
158 if (context && (idx = context.store.indexOf(context.record)) >= 0) {
159 context.row = view.getNode(idx);
161 if (me.tooltip && me.tooltip.isVisible()) {
162 me.tooltip.setTarget(context.row);
165 me.editingPlugin.cancelEdit();
169 onCtScroll: function(e, target) {
171 scrollTop = target.scrollTop,
172 scrollLeft = target.scrollLeft;
174 if (scrollTop !== me.lastScrollTop) {
175 me.lastScrollTop = scrollTop;
176 if ((me.tooltip && me.tooltip.isVisible()) || me.hiddenTip) {
180 if (scrollLeft !== me.lastScrollLeft) {
181 me.lastScrollLeft = scrollLeft;
186 onColumnAdd: function(column) {
187 this.setField(column);
190 onColumnRemove: function(column) {
191 this.columns.remove(column);
194 onColumnResize: function(column, width) {
195 column.getEditor().setWidth(width - 2);
196 if (this.isVisible()) {
201 onColumnHide: function(column) {
202 column.getEditor().hide();
203 if (this.isVisible()) {
208 onColumnShow: function(column) {
209 var field = column.getEditor();
210 field.setWidth(column.getWidth() - 2).show();
211 if (this.isVisible()) {
216 onColumnMove: function(column, fromIdx, toIdx) {
217 var field = column.getEditor();
218 if (this.items.indexOf(field) != toIdx) {
219 this.move(fromIdx, toIdx);
223 onFieldAdd: function(map, fieldId, column) {
225 colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
226 field = column.getEditor({ xtype: 'displayfield' });
228 me.insert(colIdx, field);
231 onFieldRemove: function(map, fieldId, column) {
233 field = column.getEditor(),
235 me.remove(field, false);
241 onFieldReplace: function(map, fieldId, column, oldColumn) {
243 me.onFieldRemove(map, fieldId, oldColumn);
246 clearFields: function() {
249 map.each(function(fieldId) {
250 map.removeAtKey(fieldId);
254 getFloatingButtons: function() {
256 cssPrefix = Ext.baseCSSPrefix,
257 btnsCss = cssPrefix + 'grid-row-editor-buttons',
258 plugin = me.editingPlugin,
261 if (!me.floatingButtons) {
262 btns = me.floatingButtons = Ext.create('Ext.Container', {
264 '<div class="{baseCls}-ml"></div>',
265 '<div class="{baseCls}-mr"></div>',
266 '<div class="{baseCls}-bl"></div>',
267 '<div class="{baseCls}-br"></div>',
268 '<div class="{baseCls}-bc"></div>'
284 handler: plugin.completeEdit,
286 text: me.saveBtnText,
287 disabled: !me.isValid
291 handler: plugin.cancelEdit,
293 text: me.cancelBtnText
297 // Prevent from bubbling click events to the grid view
299 // BrowserBug: Opera 11.01
300 // causes the view to scroll when a button is focused from mousedown
301 mousedown: Ext.emptyFn,
306 return me.floatingButtons;
309 reposition: function(animateConfig) {
311 context = me.context,
312 row = context && Ext.get(context.row),
313 btns = me.getFloatingButtons(),
315 grid = me.editingPlugin.grid,
316 viewEl = grid.view.el,
317 scroller = grid.verticalScroller,
319 // always get data from ColumnModel as its what drives
320 // the GridView's sizing
321 mainBodyWidth = grid.headerCt.getFullWidth(),
322 scrollerWidth = grid.getWidth(),
324 // use the minimum as the columns may not fill up the entire grid
326 width = Math.min(mainBodyWidth, scrollerWidth),
327 scrollLeft = grid.view.el.dom.scrollLeft,
328 btnWidth = btns.getWidth(),
329 left = (width - btnWidth) / 2 + scrollLeft,
332 invalidateScroller = function() {
334 scroller.invalidate();
335 btnEl.scrollIntoView(viewEl, false);
337 if (animateConfig && animateConfig.callback) {
338 animateConfig.callback.call(animateConfig.scope || me);
342 // need to set both top/left
343 if (row && Ext.isElement(row.dom)) {
344 // Bring our row into view if necessary, so a row editor that's already
345 // visible and animated to the row will appear smooth
346 row.scrollIntoView(viewEl, false);
348 // Get the y position of the row relative to its top-most static parent.
349 // offsetTop will be relative to the table, and is incorrect
350 // when mixed with certain grid features (e.g., grouping).
351 y = row.getXY()[1] - 5;
352 rowH = row.getHeight();
353 newHeight = rowH + 10;
355 // IE doesn't set the height quite right.
356 // This isn't a border-box issue, it even happens
357 // in IE8 and IE7 quirks.
358 // TODO: Test in IE9!
363 // Set editor height to match the row height
364 if (me.getHeight() != newHeight) {
365 me.setHeight(newHeight);
374 duration: animateConfig.duration || 125,
376 afteranimate: function() {
377 invalidateScroller();
378 y = row.getXY()[1] - 5;
386 invalidateScroller();
389 if (me.getWidth() != mainBodyWidth) {
390 me.setWidth(mainBodyWidth);
395 getEditor: function(fieldInfo) {
398 if (Ext.isNumber(fieldInfo)) {
399 // Query only form fields. This just future-proofs us in case we add
400 // other components to RowEditor later on. Don't want to mess with
402 return me.query('>[isFormField]')[fieldInfo];
403 } else if (fieldInfo instanceof Ext.grid.column.Column) {
404 return fieldInfo.getEditor();
408 removeField: function(field) {
411 // Incase we pass a column instead, which is fine
412 field = me.getEditor(field);
413 me.mun(field, 'validitychange', me.onValidityChange, me);
415 // Remove field/column from our mapping, which will fire the event to
416 // remove the field from our container
417 me.columns.removeKey(field.id);
420 setField: function(column) {
424 if (Ext.isArray(column)) {
425 Ext.Array.forEach(column, me.setField, me);
429 // Get a default display field if necessary
430 field = column.getEditor(null, {
431 xtype: 'displayfield',
432 // Default display fields will not return values. This is done because
433 // the display field will pick up column renderers from the grid.
434 getModelData: function() {
438 field.margins = '0 0 0 2';
439 field.setWidth(column.getDesiredWidth() - 2);
440 me.mon(field, 'change', me.onFieldChange, me);
442 // Maintain mapping of fields-to-columns
443 // This will fire events that maintain our container items
444 me.columns.add(field.id, column);
446 if (me.isVisible() && me.context) {
447 me.renderColumnData(field, me.context.record);
451 loadRecord: function(record) {
454 form.loadRecord(record);
455 if (form.isValid()) {
461 // render display fields so they honor the column renderer/template
462 Ext.Array.forEach(me.query('>displayfield'), function(field) {
463 me.renderColumnData(field, record);
467 renderColumnData: function(field, record) {
469 grid = me.editingPlugin.grid,
470 headerCt = grid.headerCt,
473 column = me.columns.get(field.id),
474 value = record.get(column.dataIndex);
476 // honor our column's renderer (TemplateHeader sets renderer for us!)
477 if (column.renderer) {
478 var metaData = { tdCls: '', style: '' },
479 rowIdx = store.indexOf(record),
480 colIdx = headerCt.getHeaderIndex(column);
482 value = column.renderer.call(
483 column.scope || headerCt.ownerCt,
494 field.setRawValue(value);
495 field.resetOriginalValue();
498 beforeEdit: function() {
501 if (me.isVisible() && !me.autoCancel && me.isDirty()) {
508 * Start editing the specified grid at the specified position.
509 * @param {Model} record The Store data record which backs the row to be edited.
510 * @param {Model} columnHeader The Column object defining the column to be edited.
512 startEdit: function(record, columnHeader) {
514 grid = me.editingPlugin.grid,
515 view = grid.getView(),
517 context = me.context = Ext.apply(me.editingPlugin.context, {
518 view: grid.getView(),
522 // make sure our row is selected before editing
523 context.grid.getSelectionModel().select(record);
525 // Reload the record data
526 me.loadRecord(record);
528 if (!me.isVisible()) {
530 me.focusContextCell();
533 callback: this.focusContextCell
538 // Focus the cell on start edit based upon the current context
539 focusContextCell: function() {
540 var field = this.getEditor(this.context.colIdx);
541 if (field && field.focus) {
546 cancelEdit: function() {
555 completeEdit: function() {
559 if (!form.isValid()) {
563 form.updateRecord(me.context.record);
570 me.callParent(arguments);
576 me.callParent(arguments);
578 me.invalidateScroller();
580 me.context.view.focus();
585 isDirty: function() {
588 return form.isDirty();
591 getToolTip: function() {
596 tip = me.tooltip = Ext.createWidget('tooltip', {
597 cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
598 title: me.errorsText,
601 closeAction: 'disable',
608 hideToolTip: function() {
610 tip = me.getToolTip();
614 me.hiddenTip = false;
617 showToolTip: function() {
619 tip = me.getToolTip(),
620 context = me.context,
621 row = Ext.get(context.row),
622 viewEl = context.grid.view.el;
625 tip.showAt([-10000, -10000]);
626 tip.body.update(me.getErrors());
627 tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
633 repositionTip: function() {
635 tip = me.getToolTip(),
636 context = me.context,
637 row = Ext.get(context.row),
638 viewEl = context.grid.view.el,
639 viewHeight = viewEl.getHeight(),
640 viewTop = me.lastScrollTop,
641 viewBottom = viewTop + viewHeight,
642 rowHeight = row.getHeight(),
643 rowTop = row.dom.offsetTop,
644 rowBottom = rowTop + rowHeight;
646 if (rowBottom > viewTop && rowTop < viewBottom) {
648 me.hiddenTip = false;
655 getErrors: function() {
657 dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
660 Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
661 errors = errors.concat(
662 Ext.Array.map(field.getErrors(), function(e) {
663 return '<li>' + e + '</li>';
668 return dirtyText + '<ul>' + errors.join('') + '</ul>';
671 invalidateScroller: function() {
673 context = me.context,
674 scroller = context.grid.verticalScroller;
677 scroller.invalidate();