]> git.proxmox.com Git - extjs.git/blob - extjs/classic/classic/test/specs/grid/plugin/RowEditing.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / test / specs / grid / plugin / RowEditing.js
1 describe('Ext.grid.plugin.RowEditing', function () {
2 var store, plugin, grid, view, column,
3 synchronousLoad = true,
4 proxyStoreLoad = Ext.data.ProxyStore.prototype.load,
5 loadStore;
6
7 function makeGrid(pluginCfg, gridCfg, storeCfg) {
8 var gridPlugins = gridCfg && gridCfg.plugins,
9 plugins;
10
11 store = new Ext.data.Store(Ext.apply({
12 fields: ['name', 'email', 'phone'],
13 data: [
14 {'name': 'Lisa', 'email': 'lisa@simpsons.com', 'phone': '555-111-1224'},
15 {'name': 'Bart', 'email': 'bart@simpsons.com', 'phone': '555-222-1234'},
16 {'name': 'Homer', 'email': 'homer@simpsons.com', 'phone': '555-222-1244'},
17 {'name': 'Marge', 'email': 'marge@simpsons.com', 'phone': '555-222-1254'}
18 ],
19 autoDestroy: true
20 }, storeCfg));
21
22 plugin = new Ext.grid.plugin.RowEditing(pluginCfg);
23
24 if (gridPlugins) {
25 plugins = [].concat(plugin, gridPlugins);
26 delete gridCfg.plugins;
27 }
28
29 grid = new Ext.grid.Panel(Ext.apply({
30 columns: [
31 {header: 'Name', dataIndex: 'name', editor: 'textfield'},
32 {header: 'Email', dataIndex: 'email',
33 editor: {
34 xtype: 'textfield',
35 allowBlank: false
36 }
37 },
38 {header: 'Phone', dataIndex: 'phone'}
39 ],
40 store: store,
41 plugins: plugins || [plugin],
42 width: 400,
43 height: 400,
44 renderTo: document.body
45 }, gridCfg));
46
47 view = grid.view;
48 }
49
50 beforeEach(function() {
51 // Override so that we can control asynchronous loading
52 loadStore = Ext.data.ProxyStore.prototype.load = function() {
53 proxyStoreLoad.apply(this, arguments);
54 if (synchronousLoad) {
55 this.flushLoad.apply(this, arguments);
56 }
57 return this;
58 };
59 });
60
61 afterEach(function () {
62 // Undo the overrides.
63 Ext.data.ProxyStore.prototype.load = proxyStoreLoad;
64
65 store = plugin = grid = view = column = Ext.destroy(grid);
66 });
67
68 describe('Widget column', function() {
69 it('should work', function() {
70 makeGrid({
71 clicksToEdit: 2
72 }, {
73 columns: [
74 {header: 'Name', dataIndex: 'name', editor: 'textfield'},
75 {header: 'Email', dataIndex: 'email',
76 editor: {
77 xtype: 'textfield',
78 allowBlank: false
79 }
80 },
81 {header: 'Phone', dataIndex: 'phone'},
82 {
83 xtype: 'widgetcolumn',
84 widget: {
85 xtype: 'button',
86 text: 'Delete',
87 handler: onDeleteClick
88 }
89 }
90 ]
91 });
92
93 var storeCount = store.getCount(),
94 editPos = new Ext.grid.CellContext(view).setPosition(0, 3),
95 cell = editPos.getCell(true);
96
97 function onDeleteClick(btn) {
98 var rec = btn.getWidgetRecord();
99 store.remove(rec);
100 }
101
102 // Programatically focus because simulated mousedown event does not focus, so
103 // The tabIndex will NOT be -1, so it will process as if mousedowning on an active widget.
104 view.getNavigationModel().setPosition(editPos);
105
106 // First click should delete the record.
107 // Second click - the dblclick - should not edit being on a focusable widget
108 jasmine.fireMouseEvent(cell.firstChild.firstChild, 'dblclick');
109
110 // Some browsers process the first and second click separately and will delete two rows.
111 // So just check that the store size has been reduced.
112 expect(store.getCount()).toBeLessThan(storeCount);
113
114 // Editing should never start; flag should be undefined/falsy
115 expect(plugin.editing).not.toBe(true);
116 });
117 });
118
119 describe('should work', function () {
120 var node;
121
122 afterEach(function () {
123 node = null;
124 });
125
126 it('should display the row editor for the grid in editing mode', function () {
127 makeGrid();
128
129 node = grid.view.getNode(0);
130
131 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell-inner', true), 'dblclick');
132
133 expect(plugin.editor).toBeDefined();
134 expect(plugin.editing).toBe(true);
135 });
136 });
137
138 describe('renderers', function () {
139 it('should be called with the correct scope for the defaultRenderer (column)', function () {
140 // See EXTJS-15047.
141 var record, scope;
142
143 makeGrid(null, {
144 columns: [
145 {text: 'Foo', width: 50,
146 defaultRenderer: function () {
147 scope = this;
148 return 'some text';
149 }
150 },
151 {header: 'Name', dataIndex: 'name', editor: 'textfield'},
152 {header: 'Email', dataIndex: 'email',
153 editor: {
154 xtype: 'textfield',
155 allowBlank: false
156 }
157 },
158 {header: 'Phone', dataIndex: 'phone'}
159 ]
160 });
161
162 record = grid.store.getAt(0);
163 column = grid.columns[0];
164 plugin.startEdit(record, column);
165
166 expect(scope === grid.columns[0]).toBe(true);
167 });
168 });
169
170 describe('starting the edit', function () {
171 var combo, textfield, record, items;
172
173 describe('should work', function () {
174 beforeEach(function () {
175 combo = new Ext.form.field.ComboBox({
176 queryMode: 'local',
177 valueField: 'name',
178 displayField: 'name',
179 store: {
180 fields: ['name'],
181 data: [
182 { name: 'Lisa' },
183 { name: 'Bart' },
184 { name: 'Homer' },
185 { name: 'Marge' }
186 ]
187 }
188 });
189
190 textfield = new Ext.form.field.Text();
191
192 makeGrid(null, {
193 columns: [
194 {header: 'Name', dataIndex: 'name', editor: combo},
195 {header: 'Email', dataIndex: 'email',
196 editor: {
197 xtype: 'textfield',
198 allowBlank: false
199 }
200 },
201 {header: 'Phone', dataIndex: 'phone', editor: textfield}
202 ]
203 });
204
205 record = grid.store.getAt(0);
206 column = grid.columns[0];
207
208 plugin.startEdit(record, column);
209 });
210
211 afterEach(function () {
212 record = items = null;
213 });
214
215 describe('initial values', function () {
216 it('should give each editor a dataIndex property', function () {
217 items = plugin.editor.items;
218
219 expect(items.getAt(0).dataIndex).toBe('name');
220 expect(items.getAt(1).dataIndex).toBe('email');
221 expect(items.getAt(2).dataIndex).toBe('phone');
222 });
223
224 it('should start the editor with values taken from the model', function () {
225 items = plugin.editor.items;
226
227 expect(items.getAt(0).getValue()).toBe('Lisa');
228 expect(items.getAt(1).getValue()).toBe('lisa@simpsons.com');
229 expect(items.getAt(2).getValue()).toBe('555-111-1224');
230 });
231 });
232
233 describe('using an existing component as an editor', function () {
234 it('should be able to lookup its value from the corresponding model field', function () {
235 items = plugin.editor.items;
236
237 // The combo editor is an existing component.
238 expect(items.getAt(0).getValue()).toBe('Lisa');
239
240 // The textfield editor is an existing component.
241 expect(items.getAt(2).getValue()).toBe('555-111-1224');
242 });
243 });
244 });
245
246 describe('calling startEdit with different columnHeader values', function () {
247 it('should allow columnHeader to be a Number', function () {
248 makeGrid();
249
250 record = grid.store.getAt(0);
251
252 // Will return `true` if the edit was successfully started.
253 expect(plugin.startEdit(record, 0)).toBe(true);
254 });
255
256 it('should allow columnHeader to be a Column instance', function () {
257 makeGrid();
258
259 record = grid.store.getAt(0);
260 column = grid.columns[0];
261
262 // Will return `true` if the edit was successfully started.
263 expect(plugin.startEdit(record, column)).toBe(true);
264 });
265
266 it('should default to the first visible column if unspecified', function () {
267 makeGrid();
268
269 record = grid.store.getAt(0);
270
271 // Will return `true` if the edit was successfully started.
272 expect(plugin.startEdit(record)).toBe(true);
273 });
274 });
275
276 describe('adding new rows to the view', function () {
277 var viewEl, count, record, editor;
278
279 function addRecord(index) {
280 var el;
281
282 plugin.cancelEdit();
283 store.insert(index, {name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244'});
284 record = store.getAt(index ? index - 1 : 0);
285 plugin.startEdit(record, 0);
286 editor = plugin.editor;
287
288 el = Ext.fly(view.getNode(record));
289
290 return new Ext.util.Point(el.getX(), el.getY());
291 }
292
293 afterEach(function () {
294 count = viewEl = record = editor = null;
295 });
296
297 it('should be contained by and visible in the view', function () {
298 makeGrid(null, {
299 height: 100
300 });
301
302 count = store.getCount();
303 viewEl = view.getEl();
304
305 // Add to the beginning.
306 expect(addRecord(0).isContainedBy(viewEl)).toBe(true);
307 expect(addRecord(0).isContainedBy(viewEl)).toBe(true);
308 expect(addRecord(0).isContainedBy(viewEl)).toBe(true);
309 expect(addRecord(0).isContainedBy(viewEl)).toBe(true);
310
311 // Add to the end.
312 expect(addRecord(count).isContainedBy(viewEl)).toBe(true);
313 expect(addRecord(count).isContainedBy(viewEl)).toBe(true);
314 expect(addRecord(count).isContainedBy(viewEl)).toBe(true);
315 expect(addRecord(count).isContainedBy(viewEl)).toBe(true);
316 });
317
318 describe('scrolling into view', function () {
319 function buffered(buffered) {
320 describe('buffered renderer = ' + buffered, function () {
321 beforeEach(function () {
322 makeGrid(null, {
323 buffered: buffered,
324 height: 100
325 });
326
327 count = store.getCount();
328 viewEl = view.getEl();
329 });
330
331 it('should scroll when adding to the beginning', function () {
332 addRecord(0);
333 expect(editor.isVisible()).toBe(true);
334 expect(editor.context.record).toBe(record);
335 });
336
337 it('should scroll when adding to the end', function () {
338 addRecord(store.getCount());
339 expect(editor.isVisible()).toBe(true);
340 expect(editor.context.record).toBe(record);
341 });
342 });
343 }
344
345 buffered(false);
346 buffered(true);
347 });
348 });
349 });
350
351 describe('completing the edit', function () {
352 var combo, record, items;
353
354 beforeEach(function () {
355 combo = new Ext.form.field.ComboBox({
356 queryMode: 'local',
357 valueField: 'name',
358 displayField: 'name',
359 store: {
360 fields: ['name'],
361 data: [
362 { name: 'Lisa' },
363 { name: 'Bart' },
364 { name: 'Homer' },
365 { name: 'Marge' }
366 ]
367 }
368 });
369
370 makeGrid(null, {
371 columns: [
372 {header: 'Name', dataIndex: 'name', editor: combo},
373 {header: 'Email', dataIndex: 'email',
374 editor: {
375 xtype: 'textfield',
376 allowBlank: false
377 }
378 }
379 ]
380 });
381
382 record = grid.store.getAt(0);
383 column = grid.columns[0];
384
385 plugin.startEdit(record, column);
386 });
387
388 afterEach(function () {
389 combo = record = items = null;
390 });
391
392 describe('using an existing component as an editor', function () {
393 it('should update the underlying cell and the record', function () {
394 column.getEditor().setValue('utley');
395 plugin.editor.completeEdit();
396
397 expect(Ext.fly(grid.view.getNode(record)).down('.x-grid-cell-inner', true).innerHTML).toBe('utley');
398 expect(store.getAt(0).get('name')).toBe('utley');
399 });
400 });
401 });
402
403 describe('canceledit', function () {
404 var editorContext = {},
405 record;
406
407 beforeEach(function () {
408 makeGrid({
409 listeners: {
410 canceledit: function (editor, context) {
411 editorContext = context;
412 }
413 }
414 });
415
416 record = grid.store.getAt(0);
417 column = grid.columns[0];
418
419 plugin.startEdit(record, column);
420 });
421
422 afterEach(function () {
423 editorContext = record = null;
424 });
425
426 it('should be able to get the original value when canceling the edit', function() {
427 column.getEditor().setValue('baz');
428 plugin.cancelEdit();
429
430 expect(editorContext.originalValues.name).toBe('Lisa');
431 });
432
433 it('should be able to get the edited value when canceling the edit', function() {
434 column.getEditor().setValue('foo');
435 plugin.cancelEdit();
436
437 expect(editorContext.newValues.name).toBe('foo');
438 });
439
440 it('should have different values for edited value and original value when canceling', function() {
441 column.getEditor().setValue('foo');
442 plugin.cancelEdit();
443
444 expect(editorContext.newValues.name).not.toBe(editorContext.originalValues.name);
445 });
446
447 it('should be able to capture falsey values when canceled', function() {
448 column.getEditor().setValue('');
449 plugin.cancelEdit();
450
451 expect(editorContext.newValues.name).toBe('');
452 });
453 });
454
455 describe('locked grid', function () {
456 var suiteCfg = {
457 columns: [
458 {header: 'Name', dataIndex: 'name', width: 100, locked: true, editor: true},
459 {header: 'Email', dataIndex: 'email', width: 100, editor: true},
460 {header: 'Phone', dataIndex: 'phone', width: 100, editor: true}
461 ],
462 plugins: {
463 ptype: 'rowediting'
464 }
465 },
466 node;
467
468 beforeEach(function () {
469 makeGrid(null, suiteCfg);
470 });
471
472 afterEach(function () {
473 node = null;
474 });
475
476 it('should display the row editor for the locked grid in editing mode', function () {
477 node = grid.lockedGrid.view.getNode(0);
478
479 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell-inner', true), 'dblclick');
480
481 plugin = grid.findPlugin('rowediting');
482
483 expect(plugin.editor !== null).toBe(true);
484 expect(plugin.editing).toBe(true);
485 });
486
487 it('should display the row editor for the normal grid in editing mode', function () {
488 node = grid.normalGrid.view.getNode(0);
489
490 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell-inner', true), 'dblclick');
491
492 plugin = grid.findPlugin('rowediting');
493
494 expect(plugin.editor !== null).toBe(true);
495 expect(plugin.editing).toBe(true);
496 });
497
498 describe('with grouping feature', function () {
499 describe('when the activeRecord of the activeEditor has been filtered', function () {
500 // These specs simulate the filtering of the data store when the row editing plugin is active
501 // and over a record that has been filtered after the row editor was activated/started editing.
502 // The bug appeared in KS when the row editor was open and the dataset was filtered by the grid
503 // filter feature. Since the locking partners share the same store, the normal grid can't look
504 // up the record in its store if the locked grid has already filtered the store, which is the
505 // case here. Note that the bug only occurred when the editor was started from the normalGrid,
506 // NOT the lockedGrid (since the store hadn't been filtered yet).
507 //
508 // To simulate, simply filter the store after the plugin has been activated. During the filter
509 // operation, it will try to lookup the row record by its internal id in the GroupStore, but it
510 // will fail because the dataset has been filtered and the GroupStore#getByInternalId method will
511 // lookup the record in the data store. The fix is to lookup the record in the snapshot collection,
512 // if it exists. This mimics the solution implemented by v5 which solves this by maintaining another
513 // unfiltered collection, Ext.util.CollectionKey. So, because we can get the record shows that the
514 // bug has been fixed, since the record is being found (regardless of filtering).
515 // See EXTJS-13374.
516 //
517 // Note these specs must use the bufferedrenderer plugin.
518 var normalView, lockedView, record;
519
520 beforeEach(function () {
521 grid.destroy();
522
523 makeGrid(null, Ext.applyIf({
524 features: {
525 ftype : 'groupingsummary',
526 groupHeaderTpl : '{name}'
527 },
528 plugins: ['bufferedrenderer'],
529 lockedGridConfig: null,
530 normalGridConfig: null
531 }, suiteCfg), {
532 groupField: 'name'
533 });
534
535 normalView = grid.normalGrid.view;
536 lockedView = grid.lockedGrid.view;
537 });
538
539 afterEach(function () {
540 normalView = lockedView = record = null;
541 });
542
543 describe('activating the editor from the normal view', function () {
544 beforeEach(function () {
545 node = normalView.getNode(0);
546
547 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell-inner', true), 'dblclick');
548
549 // Now filter the store. Make sure that the row that's clicked on has been filtered
550 // and is no longer in the filtered data collection. This is what triggered the bug
551 // because the GroupStore is trying to look up the record in the filtered collection.
552 store.filter('email', /home/);
553 record = normalView.getRecord(node);
554 });
555
556 it('should still be able to lookup the record in the datastore when filtered', function () {
557 expect(record).toBeDefined();
558 expect(record.get('email')).toBe('bart@simpsons.com');
559 });
560
561 it('should close the editor', function () {
562 expect(plugin.editing).toBe(false);
563 });
564 });
565
566 describe('activating the editor from the locked view', function () {
567 beforeEach(function () {
568 node = lockedView.getNode(0);
569
570 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell-inner', true), 'dblclick');
571
572 store.filter('email', /home/);
573 record = lockedView.getRecord(node);
574 });
575
576 it('should still be able to lookup the record in the datastore when filtered', function () {
577 expect(record).toBeDefined();
578 expect(record.get('email')).toBe('bart@simpsons.com');
579 });
580
581 it('should close the editor', function () {
582 expect(plugin.editing).toBe(false);
583 });
584 });
585 });
586 });
587 });
588
589 describe('clicksToEdit', function () {
590 var node, record;
591
592 afterEach(function () {
593 node = record = null;
594 });
595
596 describe('2 clicks', function () {
597 beforeEach(function () {
598 makeGrid();
599 });
600
601 it('should default to 2', function () {
602 expect(plugin.clicksToEdit).toBe(2);
603 });
604
605 it('should begin editing when double-clicked', function () {
606 record = grid.store.getAt(0);
607 node = grid.view.getNodeByRecord(record);
608 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell'), 'dblclick');
609
610 expect(plugin.editor).not.toBeFalsy();
611 });
612
613 it('should not begin editing when single-clicked', function () {
614 record = grid.store.getAt(0);
615 node = grid.view.getNodeByRecord(record);
616 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell'), 'click');
617
618 expect(plugin.editor).toBeFalsy();
619 });
620 });
621
622 describe('1 click', function () {
623 beforeEach(function () {
624 makeGrid({
625 clicksToEdit: 1
626 });
627 });
628
629 it('should honor a different number than the default', function () {
630 expect(plugin.clicksToEdit).toBe(1);
631 });
632
633 it('should begin editing when single-clicked', function () {
634 record = grid.store.getAt(0);
635 node = grid.view.getNodeByRecord(record);
636 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell'), 'click');
637
638 expect(plugin.editor).not.toBeFalsy();
639 });
640
641 it('should not begin editing when double-clicked', function () {
642 record = grid.store.getAt(0);
643 node = grid.view.getNodeByRecord(record);
644 jasmine.fireMouseEvent(Ext.fly(node).down('.x-grid-cell'), 'dblclick');
645
646 expect(plugin.editor).not.toBeFalsy();
647 });
648 });
649 });
650
651 describe('the RowEditor', function () {
652 var field;
653
654 afterEach(function () {
655 field = null;
656 });
657
658 describe('as textfield', function () {
659 beforeEach(function () {
660 makeGrid();
661
662 column = grid.columns[0];
663 plugin.startEdit(store.getAt(0), column);
664 field = column.field;
665 });
666
667 it('should start the edit when ENTER is pressed', function () {
668 var node;
669
670 // First complete the edit (we start an edit in the top-level beforeEach).
671 plugin.completeEdit();
672 // Let's just do a sanity to make sure we're really not currently editing.
673 expect(plugin.editing).toBe(false);
674
675 node = view.body.query('td', true)[0];
676 jasmine.fireKeyEvent(node, 'keydown', 13);
677
678 waitsFor(function () {
679 return plugin.editing;
680 });
681
682 runs(function () {
683 expect(plugin.editing).toBe(true);
684 });
685 });
686
687 describe('when currently editing', function () {
688 it('should complete the edit when ENTER is pressed', function () {
689 var str = 'Utley is Top Dog',
690 model = store.getAt(0);
691
692 expect(model.get('name')).toBe('Lisa');
693 field.setValue(str);
694
695 jasmine.fireKeyEvent(field.inputEl, 'keydown', 13);
696
697 waitsFor(function () {
698 return model.get('name') === str;
699 });
700
701 runs(function () {
702 expect(model.get('name')).toBe(str);
703 });
704 });
705
706 it('should cancel the edit when ESCAPE is pressed', function () {
707 spyOn(plugin, 'cancelEdit');
708
709 jasmine.fireKeyEvent(field.inputEl, 'keydown', 27);
710
711 expect(plugin.cancelEdit).toHaveBeenCalled();
712 });
713 });
714 });
715
716 describe('as textarea', function () {
717 beforeEach(function () {
718 makeGrid();
719
720 column = grid.columns[1];
721 plugin.startEdit(store.getAt(0), column);
722 field = column.field;
723 });
724
725 it('should start the edit when ENTER is pressed', function () {
726 var node;
727
728 // First complete the edit (we start an edit in the top-level beforeEach).
729 plugin.completeEdit();
730 // Let's just do a sanity to make sure we're really not currently editing.
731 expect(plugin.editing).toBe(false);
732
733 node = view.body.query('td', true)[1];
734 jasmine.fireKeyEvent(node, 'keydown', 13);
735
736 expect(plugin.editing).toBe(true);
737 });
738
739 describe('when currently editing', function () {
740 it('should complete the edit when ENTER is pressed', function () {
741 spyOn(plugin, 'completeEdit');
742
743 jasmine.fireKeyEvent(field.inputEl, 'keydown', 13);
744
745 expect(plugin.completeEdit).toHaveBeenCalled();
746 });
747
748 it('should not cancel the edit when ENTER is pressed', function () {
749 spyOn(plugin, 'cancelEdit');
750
751 jasmine.fireKeyEvent(field.inputEl, 'keydown', 13);
752
753 expect(plugin.cancelEdit).not.toHaveBeenCalled();
754 });
755
756 it('should cancel the edit when ESCAPE is pressed', function () {
757 spyOn(plugin, 'cancelEdit');
758
759 jasmine.fireKeyEvent(field.inputEl, 'keydown', 27);
760
761 expect(plugin.cancelEdit).toHaveBeenCalled();
762 });
763 });
764 });
765 });
766 });