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 me.onColumnHide(column);
448 if (me.isVisible() && me.context) {
449 me.renderColumnData(field, me.context.record);
453 loadRecord: function(record) {
456 form.loadRecord(record);
457 if (form.isValid()) {
463 // render display fields so they honor the column renderer/template
464 Ext.Array.forEach(me.query('>displayfield'), function(field) {
465 me.renderColumnData(field, record);
469 renderColumnData: function(field, record) {
471 grid = me.editingPlugin.grid,
472 headerCt = grid.headerCt,
475 column = me.columns.get(field.id),
476 value = record.get(column.dataIndex);
478 // honor our column's renderer (TemplateHeader sets renderer for us!)
479 if (column.renderer) {
480 var metaData = { tdCls: '', style: '' },
481 rowIdx = store.indexOf(record),
482 colIdx = headerCt.getHeaderIndex(column);
484 value = column.renderer.call(
485 column.scope || headerCt.ownerCt,
496 field.setRawValue(value);
497 field.resetOriginalValue();
500 beforeEdit: function() {
503 if (me.isVisible() && !me.autoCancel && me.isDirty()) {
510 * Start editing the specified grid at the specified position.
511 * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
512 * @param {Ext.data.Model} columnHeader The Column object defining the column to be edited.
514 startEdit: function(record, columnHeader) {
516 grid = me.editingPlugin.grid,
517 view = grid.getView(),
519 context = me.context = Ext.apply(me.editingPlugin.context, {
520 view: grid.getView(),
524 // make sure our row is selected before editing
525 context.grid.getSelectionModel().select(record);
527 // Reload the record data
528 me.loadRecord(record);
530 if (!me.isVisible()) {
532 me.focusContextCell();
535 callback: this.focusContextCell
540 // Focus the cell on start edit based upon the current context
541 focusContextCell: function() {
542 var field = this.getEditor(this.context.colIdx);
543 if (field && field.focus) {
548 cancelEdit: function() {
557 completeEdit: function() {
561 if (!form.isValid()) {
565 form.updateRecord(me.context.record);
572 me.callParent(arguments);
578 me.callParent(arguments);
580 me.invalidateScroller();
582 me.context.view.focus();
587 isDirty: function() {
590 return form.isDirty();
593 getToolTip: function() {
598 tip = me.tooltip = Ext.createWidget('tooltip', {
599 cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
600 title: me.errorsText,
603 closeAction: 'disable',
610 hideToolTip: function() {
612 tip = me.getToolTip();
616 me.hiddenTip = false;
619 showToolTip: function() {
621 tip = me.getToolTip(),
622 context = me.context,
623 row = Ext.get(context.row),
624 viewEl = context.grid.view.el;
627 tip.showAt([-10000, -10000]);
628 tip.body.update(me.getErrors());
629 tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
635 repositionTip: function() {
637 tip = me.getToolTip(),
638 context = me.context,
639 row = Ext.get(context.row),
640 viewEl = context.grid.view.el,
641 viewHeight = viewEl.getHeight(),
642 viewTop = me.lastScrollTop,
643 viewBottom = viewTop + viewHeight,
644 rowHeight = row.getHeight(),
645 rowTop = row.dom.offsetTop,
646 rowBottom = rowTop + rowHeight;
648 if (rowBottom > viewTop && rowTop < viewBottom) {
650 me.hiddenTip = false;
657 getErrors: function() {
659 dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
662 Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
663 errors = errors.concat(
664 Ext.Array.map(field.getErrors(), function(e) {
665 return '<li>' + e + '</li>';
670 return dirtyText + '<ul>' + errors.join('') + '</ul>';
673 invalidateScroller: function() {
675 context = me.context,
676 scroller = context.grid.verticalScroller;
679 scroller.invalidate();