]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/grid/plugin/Editing.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / grid / plugin / Editing.js
CommitLineData
6527f429
DM
1/**\r
2 * This class provides an abstract grid editing plugin on selected {@link Ext.grid.column.Column columns}.\r
3 * The editable columns are specified by providing an {@link Ext.grid.column.Column#editor editor}\r
4 * in the {@link Ext.grid.column.Column column configuration}.\r
5 *\r
6 * **Note:** This class should not be used directly. See {@link Ext.grid.plugin.CellEditing} and\r
7 * {@link Ext.grid.plugin.RowEditing}.\r
8 */\r
9Ext.define('Ext.grid.plugin.Editing', {\r
10 extend: 'Ext.plugin.Abstract',\r
11 alias: 'editing.editing',\r
12\r
13 requires: [\r
14 'Ext.grid.column.Column',\r
15 'Ext.util.KeyNav',\r
16 // Requiring Ext.form.field.Base and Ext.view.Table ensures that grid editor sass\r
17 // variables can derive from both form field vars and grid vars in the neutral theme\r
18 'Ext.form.field.Base',\r
19 'Ext.view.Table'\r
20 ],\r
21\r
22 mixins: [\r
23 'Ext.mixin.Observable'\r
24 ],\r
25\r
26 /**\r
27 * @cfg {Number} clicksToEdit\r
28 * The number of clicks on a grid required to display the editor.\r
29 * The only accepted values are **1** and **2**.\r
30 */\r
31 clicksToEdit: 2,\r
32\r
33 /**\r
34 * @cfg {String} triggerEvent\r
35 * The event which triggers editing. Supersedes the {@link #clicksToEdit} configuration. May be one of:\r
36 *\r
37 * * cellclick\r
38 * * celldblclick\r
39 * * cellfocus\r
40 * * rowfocus\r
41 */\r
42 triggerEvent: undefined,\r
43\r
44 /**\r
45 * @property {Boolean} editing\r
46 * Set to `true` while the editing plugin is active and an Editor is visible.\r
47 */\r
48\r
49 relayedEvents: [\r
50 'beforeedit',\r
51 'edit',\r
52 'validateedit',\r
53 'canceledit'\r
54 ],\r
55\r
56 /**\r
57 * @cfg {String} default UI for editor fields\r
58 */\r
59 defaultFieldUI: 'default',\r
60\r
61 // @private\r
62 defaultFieldXType: 'textfield',\r
63\r
64 // cell, row, form\r
65 editStyle: '',\r
66\r
67 /**\r
68 * @event beforeedit\r
69 * Fires before editing is triggered. Return false from event handler to stop the editing.\r
70 *\r
71 * @param {Ext.grid.plugin.Editing} editor\r
72 * @param {Object} context The editing context with the following properties:\r
73 * @param {Ext.grid.Panel} context.grid The owning grid Panel.\r
74 * @param {Ext.data.Model} context.record The record being edited.\r
75 * @param {String} context.field The name of the field being edited.\r
76 * @param {Mixed} context.value The field's current value.\r
77 * @param {HTMLElement} context.row The grid row element.\r
78 * @param {Ext.grid.column.Column} context.column The Column being edited.\r
79 * @param {Number} context.rowIdx The index of the row being edited.\r
80 * @param {Number} context.colIdx The index of the column being edited.\r
81 * @param {Boolean} context.cancel Set this to `true` to cancel the edit or return false from your handler.\r
82 * @param {Mixed} context.originalValue Alias for value (only when using {@link Ext.grid.plugin.CellEditing CellEditing}).\r
83 */\r
84\r
85 /**\r
86 * @event edit\r
87 * Fires after editing. Usage example:\r
88 *\r
89 * grid.on('edit', function(editor, e) {\r
90 * // commit the changes right after editing finished\r
91 * e.record.commit();\r
92 * });\r
93 *\r
94 * @param {Ext.grid.plugin.Editing} editor\r
95 * @param {Object} context The editing context with the following properties:\r
96 * @param {Ext.grid.Panel} context.grid The owning grid Panel.\r
97 * @param {Ext.data.Model} context.record The record being edited.\r
98 * @param {String} context.field The name of the field being edited.\r
99 * @param {Mixed} context.value The field's current value.\r
100 * @param {HTMLElement} context.row The grid row element.\r
101 * @param {Ext.grid.column.Column} context.column The Column being edited.\r
102 * @param {Number} context.rowIdx The index of the row being edited.\r
103 * @param {Number} context.colIdx The index of the column being edited.\r
104 */\r
105\r
106 /**\r
107 * @event validateedit\r
108 * Fires after editing, but before the value is set in the record. Return false from event handler to\r
109 * cancel the change.\r
110 *\r
111 * Usage example showing how to remove the red triangle (dirty record indicator) from some records (not all). By\r
112 * observing the grid's validateedit event, it can be cancelled if the edit occurs on a targeted row (for example)\r
113 * and then setting the field's new value in the Record directly:\r
114 *\r
115 * grid.on('validateedit', function (editor, context) {\r
116 * var myTargetRow = 6;\r
117 *\r
118 * if (context.rowIdx === myTargetRow) {\r
119 * context.record.data[context.field] = context.value;\r
120 * }\r
121 * });\r
122 *\r
123 * @param {Ext.grid.plugin.Editing} editor\r
124 * @param {Object} context The editing context with the following properties:\r
125 * @param {Ext.grid.Panel} context.grid The owning grid Panel.\r
126 * @param {Ext.data.Model} context.record The record being edited.\r
127 * @param {String} context.field The name of the field being edited.\r
128 * @param {Mixed} context.value The field's current value.\r
129 * @param {HTMLElement} context.row The grid row element.\r
130 * @param {Ext.grid.column.Column} context.column The Column being edited.\r
131 * @param {Number} context.rowIdx The index of the row being edited.\r
132 * @param {Number} context.colIdx The index of the column being edited.\r
133 */\r
134\r
135 /**\r
136 * @event canceledit\r
137 * Fires when the user started editing but then cancelled the edit.\r
138 * @param {Ext.grid.plugin.Editing} editor\r
139 * @param {Object} context The editing context with the following properties:\r
140 * @param {Ext.grid.Panel} context.grid The owning grid Panel.\r
141 * @param {Ext.data.Model} context.record The record being edited.\r
142 * @param {String} context.field The name of the field being edited.\r
143 * @param {Mixed} context.value The field's current value.\r
144 * @param {HTMLElement} context.row The grid row element.\r
145 * @param {Ext.grid.column.Column} context.column The Column being edited.\r
146 * @param {Number} context.rowIdx The index of the row being edited.\r
147 * @param {Number} context.colIdx The index of the column being edited.\r
148 */\r
149\r
150 constructor: function(config) {\r
151 var me = this;\r
152\r
153 me.callParent([config]);\r
154 me.mixins.observable.constructor.call(me);\r
155 // TODO: Deprecated, remove in 5.0\r
156 me.on("edit", function(editor, e) {\r
157 me.fireEvent("afteredit", editor, e);\r
158 });\r
159 },\r
160\r
161 // @private\r
162 init: function(grid) {\r
163 var me = this,\r
164 ownerLockable = grid.ownerLockable;\r
165\r
166 me.grid = grid;\r
167 me.view = grid.view;\r
168 me.initEvents();\r
169\r
170 // Set up fields at render and reconfigure time\r
171 if (grid.rendered) {\r
172 me.setup();\r
173 } else {\r
174 me.mon(grid, {\r
175 beforereconfigure: me.onBeforeReconfigure,\r
176 reconfigure: me.onReconfigure,\r
177 scope: me,\r
178 beforerender: {\r
179 fn: me.onBeforeRender,\r
180 single: true,\r
181 scope: me\r
182 }\r
183 });\r
184 }\r
185\r
186 grid.editorEventRelayers = grid.relayEvents(me, me.relayedEvents);\r
187\r
188 // If the editable grid is owned by a lockable, relay up another level.\r
189 if (ownerLockable) {\r
190 ownerLockable.editorEventRelayers = ownerLockable.relayEvents(me, me.relayedEvents);\r
191 }\r
192 // Marks the grid as editable, so that the SelectionModel\r
193 // can make appropriate decisions during navigation\r
194 grid.isEditable = true;\r
195 grid.editingPlugin = grid.view.editingPlugin = me;\r
196 },\r
197\r
198 onBeforeReconfigure: function() {\r
199 this.reconfiguring = true;\r
200 },\r
201\r
202 /**\r
203 * Fires after the grid is reconfigured\r
204 * @protected\r
205 */\r
206 onReconfigure: function() {\r
207 this.setup();\r
208 delete this.reconfiguring;\r
209 },\r
210\r
211 onBeforeRender: function() {\r
212 this.setup();\r
213 },\r
214\r
215 setup: function() {\r
216 // In a Lockable assembly, the owner's view aggregates all grid columns across both sides.\r
217 // We grab all columns here.\r
218 this.initFieldAccessors(this.grid.getTopLevelColumnManager().getColumns());\r
219 },\r
220\r
221 destroy: function() {\r
222 var me = this,\r
223 grid = me.grid;\r
224\r
225 Ext.destroy(me.keyNav);\r
226 \r
227 // Clear all listeners from all our events, clear all managed listeners we added to other Observables\r
228 me.clearListeners();\r
229\r
230 if (grid) {\r
231 if (grid.ownerLockable) {\r
232 Ext.destroy(grid.ownerLockable.editorEventRelayers);\r
233 grid.ownerLockable.editorEventRelayers = null;\r
234 }\r
235 \r
236 Ext.destroy(grid.editorEventRelayers);\r
237 grid.editorEventRelayers = null;\r
238 \r
239 grid.editingPlugin = grid.view.editingPlugin = me.grid = me.view = me.editor = me.keyNav = null;\r
240 }\r
241\r
242 me.callParent();\r
243 },\r
244\r
245 // @private\r
246 getEditStyle: function() {\r
247 return this.editStyle;\r
248 },\r
249\r
250 // @private\r
251 initFieldAccessors: function(columns) {\r
252 // If we have been passed a group header, process its leaf headers\r
253 if (columns.isGroupHeader) {\r
254 columns = columns.getGridColumns();\r
255 }\r
256\r
257 // Ensure we are processing an array\r
258 else if (!Ext.isArray(columns)) {\r
259 columns = [columns];\r
260 }\r
261\r
262 var me = this,\r
263 c,\r
264 cLen = columns.length,\r
265 getEditor = function(record, defaultField) {\r
266 return me.getColumnField(this, defaultField);\r
267 },\r
268 hasEditor = function() {\r
269 return me.hasColumnField(this);\r
270 },\r
271 setEditor = function(field) {\r
272 me.setColumnField(this, field);\r
273 },\r
274 column;\r
275\r
276 for (c = 0; c < cLen; c++) {\r
277 column = columns[c];\r
278\r
279 if (!column.getEditor) {\r
280 column.getEditor = getEditor;\r
281 }\r
282 if (!column.hasEditor) {\r
283 column.hasEditor = hasEditor;\r
284 }\r
285 if (!column.setEditor) {\r
286 column.setEditor = setEditor;\r
287 }\r
288 }\r
289 },\r
290\r
291 // @private\r
292 removeFieldAccessors: function(columns) {\r
293 // If we have been passed a group header, process its leaf headers\r
294 if (columns.isGroupHeader) {\r
295 columns = columns.getGridColumns();\r
296 }\r
297\r
298 // Ensure we are processing an array\r
299 else if (!Ext.isArray(columns)) {\r
300 columns = [columns];\r
301 }\r
302\r
303 var c,\r
304 cLen = columns.length,\r
305 column;\r
306\r
307 for (c = 0; c < cLen; c++) {\r
308 column = columns[c];\r
309 column.getEditor = column.hasEditor = column.setEditor = column.field = column.editor = null;\r
310 }\r
311 },\r
312\r
313 // @private\r
314 // remaps to the public API of Ext.grid.column.Column.getEditor\r
315 getColumnField: function(columnHeader, defaultField) {\r
316 var me = this,\r
317 field = columnHeader.field;\r
318\r
319 if (!(field && field.isFormField)) {\r
320 field = columnHeader.field = me.createColumnField(columnHeader, defaultField);\r
321 }\r
322\r
323 if (field && field.ui === 'default' && !field.hasOwnProperty('ui')) {\r
324 field.ui = me.defaultFieldUI;\r
325 }\r
326 return field;\r
327 },\r
328\r
329 // @private\r
330 // remaps to the public API of Ext.grid.column.Column.hasEditor\r
331 hasColumnField: function(columnHeader) {\r
332 return !!(columnHeader.field && columnHeader.field.isComponent);\r
333 },\r
334\r
335 // @private\r
336 // remaps to the public API of Ext.grid.column.Column.setEditor\r
337 setColumnField: function(columnHeader, field) {\r
338 columnHeader.field = field;\r
339 columnHeader.field = this.createColumnField(columnHeader);\r
340 },\r
341\r
342 createColumnField: function (column, defaultField) {\r
343 var field = column.field,\r
344 dataIndex;\r
345\r
346 if (!field && column.editor) {\r
347 field = column.editor;\r
348 column.editor = null;\r
349 }\r
350\r
351 if (!field && defaultField) {\r
352 field = defaultField;\r
353 }\r
354\r
355 if (field) {\r
356 dataIndex = column.dataIndex;\r
357\r
358 if (field.isComponent) {\r
359 field.column = column;\r
360 } else {\r
361 if (Ext.isString(field)) {\r
362 field = {\r
363 name: dataIndex,\r
364 xtype: field,\r
365 column: column\r
366 };\r
367 } else {\r
368 field = Ext.apply({\r
369 name: dataIndex,\r
370 column: column\r
371 }, field);\r
372 }\r
373 field = Ext.ComponentManager.create(field, this.defaultFieldXType);\r
374 }\r
375\r
376 // Stamp on the dataIndex which will serve as a reliable lookup regardless\r
377 // of how the editor was defined (as a config or as an existing component).\r
378 // See EXTJSIV-11650.\r
379 field.dataIndex = dataIndex;\r
380\r
381 field.isEditorComponent = true;\r
382 column.field = field;\r
383 }\r
384 return field;\r
385 },\r
386\r
387 // @private\r
388 initEvents: function() {\r
389 var me = this;\r
390 me.initEditTriggers();\r
391 me.initCancelTriggers();\r
392 },\r
393\r
394 // @abstract\r
395 initCancelTriggers: Ext.emptyFn,\r
396\r
397 // @private\r
398 initEditTriggers: function() {\r
399 var me = this,\r
400 view = me.view;\r
401\r
402 // Listen for the edit trigger event.\r
403 if (me.triggerEvent === 'cellfocus') {\r
404 me.mon(view, 'cellfocus', me.onCellFocus, me);\r
405 } else if (me.triggerEvent === 'rowfocus') {\r
406 me.mon(view, 'rowfocus', me.onRowFocus, me);\r
407 } else {\r
408\r
409 // Prevent the View from processing when the SelectionModel focuses.\r
410 // This is because the SelectionModel processes the mousedown event, and\r
411 // focusing causes a scroll which means that the subsequent mouseup might\r
412 // take place at a different document XY position, and will therefore\r
413 // not trigger a click.\r
414 // This Editor must call the View's focusCell method directly when we recieve a request to edit\r
415 if (view.getSelectionModel().isCellModel) {\r
416 view.onCellFocus = me.beforeViewCellFocus.bind(me);\r
417 }\r
418\r
419 // Listen for whichever click event we are configured to use\r
420 me.mon(view, me.triggerEvent || ('cell' + (me.clicksToEdit === 1 ? 'click' : 'dblclick')), me.onCellClick, me);\r
421 }\r
422\r
423 // add/remove header event listeners need to be added immediately because\r
424 // columns can be added/removed before render\r
425 me.initAddRemoveHeaderEvents();\r
426\r
427 // Attach new bindings to the View's NavigationModel which processes cellkeydown events.\r
428 me.view.getNavigationModel().addKeyBindings({\r
429 esc: me.onEscKey,\r
430 scope: me\r
431 });\r
432 },\r
433\r
434 // Override of View's method so that we can pre-empt the View's processing if the view is being triggered by a mousedown\r
435 beforeViewCellFocus: function(position) {\r
436 // Pass call on to view if the navigation is from the keyboard, or we are not going to edit this cell.\r
437 if (this.view.selModel.keyNavigation || !this.editing || !this.isCellEditable || !this.isCellEditable(position.row, position.columnHeader)) {\r
438 this.view.focusCell.apply(this.view, arguments);\r
439 }\r
440 },\r
441\r
442 // @private Used if we are triggered by the rowfocus event\r
443 onRowFocus: function(record, row, rowIdx) {\r
444 this.startEdit(row, 0);\r
445 },\r
446\r
447 // @private Used if we are triggered by the cellfocus event\r
448 onCellFocus: function(record, cell, position) {\r
449 this.startEdit(position.row, position.column);\r
450 },\r
451\r
452 // @private Used if we are triggered by a cellclick event\r
453 // *IMPORTANT* Due to V4.0.0 history, the colIdx here is the index within ALL columns, including hidden.\r
454 onCellClick: function(view, cell, colIdx, record, row, rowIdx, e) {\r
455 // Make sure that the column has an editor. In the case of CheckboxModel,\r
456 // calling startEdit doesn't make sense when the checkbox is clicked.\r
457 // Also, cancel editing if the element that was clicked was a tree expander.\r
458 var expanderSelector = view.expanderSelector,\r
459 // Use getColumnManager() in this context because colIdx includes hidden columns.\r
460 columnHeader = view.ownerCt.getColumnManager().getHeaderAtIndex(colIdx),\r
461 editor = columnHeader.getEditor(record);\r
462\r
463 if (this.shouldStartEdit(editor) && (!expanderSelector || !e.getTarget(expanderSelector))) {\r
464 view.ownerGrid.setActionableMode(true, e.position);\r
465 }\r
466 },\r
467\r
468 initAddRemoveHeaderEvents: function(){\r
469 var me = this,\r
470 headerCt = me.grid.headerCt;\r
471\r
472 me.mon(headerCt, {\r
473 scope: me,\r
474 add: me.onColumnAdd,\r
475 columnmove: me.onColumnMove,\r
476 beforedestroy: me.beforeGridHeaderDestroy\r
477 });\r
478 },\r
479\r
480 // @private\r
481 onColumnAdd: function(ct, column) {\r
482 this.initFieldAccessors(column);\r
483 },\r
484\r
485 // Template method which may be implemented in subclasses (RowEditing and CellEditing)\r
486 onColumnMove: Ext.emptyFn,\r
487\r
488 // @private\r
489 onEscKey: function(e) {\r
490 if (this.editing) {\r
491 var targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId'));\r
492\r
493 // ESCAPE when a picker is expanded does not cancel the edit\r
494 if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) {\r
495 return this.cancelEdit();\r
496 }\r
497 }\r
498 },\r
499\r
500 /**\r
501 * @method\r
502 * @private\r
503 * @template\r
504 * Template method called before editing begins.\r
505 * @param {Object} context The current editing context\r
506 * @return {Boolean} Return false to cancel the editing process\r
507 */\r
508 beforeEdit: Ext.emptyFn,\r
509\r
510 shouldStartEdit: function(editor) {\r
511 return !!editor;\r
512 },\r
513\r
514 /**\r
515 * @private\r
516 * Collects all information necessary for any subclasses to perform their editing functions.\r
517 * @param {Ext.data.Model/Number} record The record or record index to edit.\r
518 * @param {Ext.grid.column.Column/Number} columnHeader The column of column index to edit.\r
519 * @return {Ext.grid.CellContext/undefined} The editing context based upon the passed record and column\r
520 */\r
521 getEditingContext: function(record, columnHeader) {\r
522 var me = this,\r
523 grid = me.grid,\r
524 colMgr = grid.visibleColumnManager,\r
525 view,\r
526 gridRow,\r
527 rowIdx, colIdx,\r
528 result,\r
529 layoutView = me.grid.lockable ? me.grid : me.view;\r
530\r
531 // The view must have had a layout to show the editor correctly, defer until that time.\r
532 // In case a grid's startup code invokes editing immediately.\r
533 if (!layoutView.componentLayoutCounter) {\r
534 layoutView.on({\r
535 boxready: Ext.Function.bind(me.startEdit, me, [record, columnHeader]),\r
536 single: true\r
537 });\r
538 return;\r
539 }\r
540\r
541 // If disabled or grid collapsed, or view not truly visible, don't calculate a context - we cannot edit\r
542 if (me.disabled || me.grid.collapsed || !me.grid.view.isVisible(true)) {\r
543 return;\r
544 }\r
545\r
546 // They've asked to edit by column number.\r
547 // Note that in a locked grid, the columns are enumerated in a unified set for this purpose.\r
548 if (Ext.isNumber(columnHeader)) {\r
549 columnHeader = colMgr.getHeaderAtIndex(columnHeader);\r
550 }\r
551\r
552 // No corresponding column. Possible if all columns have been moved to the other side of a lockable grid pair\r
553 if (!columnHeader) {\r
554 return;\r
555 }\r
556\r
557 // Coerce the column to the closest visible column\r
558 if (columnHeader.hidden) {\r
559 columnHeader = columnHeader.next(':not([hidden])') || columnHeader.prev(':not([hidden])');\r
560 }\r
561\r
562 // Navigate to the view and grid which the column header relates to.\r
563 view = columnHeader.getView();\r
564 grid = view.ownerCt;\r
565\r
566 // Ensure the row we want to edit is in the rendered range if the view is buffer rendered\r
567 grid.ensureVisible(record, {\r
568 column : columnHeader\r
569 });\r
570 \r
571 gridRow = view.getRow(record);\r
572\r
573 // An intervening listener may have deleted the Record.\r
574 if (!gridRow) {\r
575 return;\r
576 }\r
577\r
578 // Column index must be relative to the View the Context is using.\r
579 // It must be the real owning View, NOT the lockable pseudo view.\r
580 colIdx = view.getVisibleColumnManager().indexOf(columnHeader);\r
581\r
582 if (Ext.isNumber(record)) {\r
583 // look up record if numeric row index was passed\r
584 rowIdx = record;\r
585 record = view.getRecord(gridRow);\r
586 } else {\r
587 rowIdx = view.indexOf(gridRow);\r
588 }\r
589\r
590 // The record may be removed from the store but the view\r
591 // not yet updated, so check it exists\r
592 if (!record) {\r
593 return;\r
594 }\r
595\r
596 // Create a new CellContext\r
597 result = new Ext.grid.CellContext(view).setAll(view, rowIdx, colIdx, record, columnHeader);\r
598\r
599 // Add extra Editing information\r
600 result.grid = grid;\r
601 result.store = view.dataSource;\r
602 result.field = columnHeader.dataIndex;\r
603 result.value = result.originalValue = record.get(columnHeader.dataIndex);\r
604 result.row = gridRow;\r
605 result.node = view.getNode(record);\r
606 result.cell = view.getCellByPosition(result, true);\r
607\r
608 return result;\r
609 },\r
610\r
611 /**\r
612 * Cancels any active edit that is in progress.\r
613 */\r
614 cancelEdit: function() {\r
615 var me = this;\r
616\r
617 me.editing = false;\r
618 me.fireEvent('canceledit', me, me.context);\r
619 },\r
620\r
621 /**\r
622 * Completes the edit if there is an active edit in progress.\r
623 */\r
624 completeEdit: function() {\r
625 var me = this;\r
626\r
627 if (me.editing && me.validateEdit()) {\r
628 me.fireEvent('edit', me, me.context);\r
629 }\r
630\r
631 me.context = null;\r
632 me.editing = false;\r
633 },\r
634\r
635 // @abstract\r
636 validateEdit: function(context) {\r
637 var me = this;\r
638\r
639 return me.fireEvent('validateedit', me, context) !== false && !context.cancel;\r
640 }\r
641});