]> git.proxmox.com Git - extjs.git/blob - extjs/classic/classic/src/selection/CellModel.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / selection / CellModel.js
1 /**
2 * A selection model for {@link Ext.grid.Panel grid panels} which allows selection of a single cell at a time.
3 *
4 * Implements cell based navigation via keyboard.
5 *
6 * @example
7 * var store = Ext.create('Ext.data.Store', {
8 * fields: ['name', 'email', 'phone'],
9 * data: [
10 * { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
11 * { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
12 * { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
13 * { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
14 * ]
15 * });
16 *
17 * Ext.create('Ext.grid.Panel', {
18 * title: 'Simpsons',
19 * store: store,
20 * width: 400,
21 * renderTo: Ext.getBody(),
22 * columns: [
23 * { text: 'Name', dataIndex: 'name' },
24 * { text: 'Email', dataIndex: 'email', flex: 1 },
25 * { text: 'Phone', dataIndex: 'phone' }
26 * ],
27 * selModel: 'cellmodel'
28 * });
29 */
30 Ext.define('Ext.selection.CellModel', {
31 extend: 'Ext.selection.DataViewModel',
32 alias: 'selection.cellmodel',
33 requires: [
34 'Ext.grid.CellContext'
35 ],
36
37 /**
38 * @cfg {"SINGLE"} mode
39 * Mode of selection. Valid values are:
40 *
41 * - **"SINGLE"** - Only allows selecting one item at a time. This is the default.
42 */
43
44
45 isCellModel: true,
46
47 /**
48 * @inheritdoc
49 */
50 deselectOnContainerClick: false,
51
52 /**
53 * @cfg {Boolean} enableKeyNav
54 * Turns on/off keyboard navigation within the grid.
55 */
56 enableKeyNav: true,
57
58 /**
59 * @cfg {Boolean} preventWrap
60 * Set this configuration to true to prevent wrapping around of selection as
61 * a user navigates to the first or last column.
62 */
63 preventWrap: false,
64
65 /**
66 * @event deselect
67 * Fired after a cell is deselected
68 * @param {Ext.selection.CellModel} this
69 * @param {Ext.data.Model} record The record of the deselected cell
70 * @param {Number} row The row index deselected
71 * @param {Number} column The column index deselected
72 */
73
74 /**
75 * @event select
76 * Fired after a cell is selected
77 * @param {Ext.selection.CellModel} this
78 * @param {Ext.data.Model} record The record of the selected cell
79 * @param {Number} row The row index selected
80 * @param {Number} column The column index selected
81 */
82
83 bindComponent: function(view) {
84 var me = this,
85 grid;
86
87 // Unbind from a view
88 if (me.view && me.gridListeners) {
89 me.gridListeners.destroy();
90 }
91
92 // DataViewModel's bindComponent
93 me.callParent([view]);
94
95 if (view) {
96 // view.grid is present during View construction, before the view has been
97 // added as a child of the Panel, and an upward link it still needed.
98 grid = view.grid || view.ownerCt;
99
100 if (grid.optimizedColumnMove !== false) {
101 me.gridListeners = grid.on({
102 columnmove: me.onColumnMove,
103 scope: me,
104 destroyable: true
105 });
106 }
107 }
108 },
109
110 getViewListeners: function() {
111 var result = this.callParent();
112 result.refresh = this.onViewRefresh;
113 return result;
114 },
115
116 getHeaderCt: function() {
117 var selection = this.navigationModel.getPosition(),
118 view = selection ? selection.view : this.primaryView;
119
120 return view.headerCt;
121 },
122
123 // Selection blindly follows focus. For now.
124 onNavigate: function(e) {
125 // It was a navigate out event.
126 // Or stopSelection was stamped into the event by an upstream handler.
127 // This is used by ActionColumn and CheckColumn to implement their stopSelection config
128 if (!e.record || e.keyEvent.stopSelection) {
129 return;
130 }
131
132 this.setPosition(e.position);
133 },
134
135 selectWithEvent: function(record, e) {
136 this.select(record);
137 },
138 /**
139 * Selects a cell by row / column.
140 *
141 * var grid = Ext.create('Ext.grid.Panel', {
142 * title: 'Simpsons',
143 * store: {
144 * fields: ['name', 'email', 'phone'],
145 * data: [{
146 * name: "Lisa",
147 * email: "lisa@simpsons.com",
148 * phone: "555-111-1224"
149 * }]
150 * },
151 * columns: [{
152 * text: 'Name',
153 * dataIndex: 'name'
154 * }, {
155 * text: 'Email',
156 * dataIndex: 'email',
157 * hidden: true
158 * }, {
159 * text: 'Phone',
160 * dataIndex: 'phone',
161 * flex: 1
162 * }],
163 * height: 200,
164 * width: 400,
165 * renderTo: Ext.getBody(),
166 * selType: 'cellmodel',
167 * tbar: [{
168 * text: 'Select position Object',
169 * handler: function() {
170 * grid.getSelectionModel().select({
171 * row: grid.getStore().getAt(0),
172 * column: grid.down('gridcolumn[dataIndex=name]')
173 * });
174 * }
175 * }, {
176 * text: 'Select position by Number',
177 * handler: function() {
178 * grid.getSelectionModel().select({
179 * row: 0,
180 * column: 1
181 * });
182 * }
183 * }]
184 * });
185 *
186 * @param {Object} pos An object with row and column properties
187 * @param {Ext.data.Model/Number} pos.row
188 * A record or index of the record (starting at 0)
189 * @param {Ext.grid.column.Column/Number} pos.column
190 * A column or index of the column (starting at 0). Includes visible columns only.
191 */
192 select: function(pos, /* private */ keepExisting, suppressEvent) {
193 var me = this,
194 row,
195 oldPos = me.getPosition(),
196 store = me.view.store;
197
198 if (pos || pos === 0) {
199 if (pos.isModel) {
200 row = store.indexOf(pos);
201 if (row !== -1) {
202 pos = {
203 row: row,
204 column: oldPos ? oldPos.column : 0
205 };
206 } else {
207 pos = null;
208 }
209 } else if (typeof pos === 'number') {
210 pos = {
211 row: pos,
212 column: 0
213 };
214 }
215 }
216
217 if (pos) {
218 me.selectByPosition(pos, suppressEvent);
219 } else {
220 me.deselect();
221 }
222 },
223
224 /**
225 * Returns the current position in the format {row: row, column: column}
226 * @deprecated 5.0.1 This API uses column indices which include hidden columns in the count. Use {@link #getPosition} instead.
227 */
228 getCurrentPosition: function() {
229 // If it's during a select, return nextSelection since we buffer
230 // the real selection until after the event fires
231 var position = this.selecting ? this.nextSelection : this.selection;
232
233 // This is the previous Format of the private CellContext class which was used here.
234 // Do not return a CellContext so that if this object is passed into setCurrentPosition, it will be
235 // read in the legacy (including hidden columns) way.
236 return position ? {
237 view: position.view,
238 record: position.record,
239 row: position.rowIdx,
240 columnHeader: position.column,
241 // IMPORTANT: The historic API for columns has been to include hidden columns
242 // in the index. So we must report the index of the column in the "all" ColumnManager.
243 column: position.view.getColumnManager().indexOf(position.column)
244 } : position;
245 },
246
247 /**
248 * Returns the current position in the format {row: row, column: column}
249 * @return {Ext.grid.CellContext} A CellContext object describing the current cell.
250 */
251 getPosition: function() {
252 return (this.selecting ? this.nextSelection : this.selection) || null;
253 },
254
255 /**
256 * Sets the current position.
257 * @deprecated 5.0.1 This API uses column indices which include hidden columns in the count. Use {@link #setPosition} instead.
258 * @param {Ext.grid.CellContext/Object} position The position to set. May be an object of the form `{row:1, column:2}`
259 * @param {Boolean} suppressEvent True to suppress selection events
260 */
261 setCurrentPosition: function(pos, suppressEvent, /* private */ preventCheck) {
262 if (pos && !pos.isCellContext) {
263 pos = new Ext.grid.CellContext(this.view).setPosition({
264 row: pos.row,
265 // IMPORTANT: The historic API for columns has been to include hidden columns
266 // in the index. So we must index into the "all" ColumnManager.
267 column: typeof pos.column === 'number' ? this.view.getColumnManager().getColumns()[pos.column] : pos.column
268 });
269 }
270 return this.setPosition(pos, suppressEvent, preventCheck);
271 },
272
273 /**
274 * Sets the current position.
275 *
276 * Note that if passing a column index, it is the index within the *visible* column set.
277 *
278 * @param {Ext.grid.CellContext/Object} position The position to set. May be an object of the form `{row:1, column:2}`
279 * @param {Boolean} suppressEvent True to suppress selection events
280 */
281 setPosition: function(pos, suppressEvent, /* private */ preventCheck) {
282 var me = this,
283 last = me.selection;
284
285 // Normalize it into an Ext.grid.CellContext if necessary
286 if (pos) {
287 pos = pos.isCellContext ? pos.clone() : new Ext.grid.CellContext(me.view).setPosition(pos);
288 }
289 if (!preventCheck && last) {
290 // If the position is the same, jump out & don't fire the event
291 if (pos && (pos.record === last.record && pos.column === last.column && pos.view === last.view)) {
292 pos = null;
293 } else {
294 me.onCellDeselect(me.selection, suppressEvent);
295 }
296 }
297
298 if (pos) {
299 me.nextSelection = pos;
300 // set this flag here so we know to use nextSelection
301 // if the node is updated during a select
302 me.selecting = true;
303 me.onCellSelect(me.nextSelection, suppressEvent);
304 me.selecting = false;
305 // Deselect triggered by new selection will kill the selection property, so restore it here.
306 return (me.selection = pos);
307 }
308 // <debug>
309 // Enforce code correctness in unbuilt source.
310 return null;
311 // </debug>
312 },
313
314 isCellSelected: function(view, row, column) {
315 var me = this,
316 testPos,
317 pos = me.getPosition();
318
319 if (pos && pos.view === view) {
320 testPos = new Ext.grid.CellContext(view).setPosition({
321 row: row,
322 // IMPORTANT: The historic API for columns has been to include hidden columns
323 // in the index. So we must index into the "all" ColumnManager.
324 column: typeof column === 'number' ? view.getColumnManager().getColumns()[column] : column
325 });
326 return (testPos.record === pos.record) && (testPos.column === pos.column);
327 }
328 },
329
330 // Keep selection model in consistent state upon record deletion.
331 onStoreRemove: function(store, records, indices) {
332 var me = this,
333 pos = me.getPosition();
334
335 me.callParent(arguments);
336 if (pos && store.isMoving(pos.record)) {
337 return;
338 }
339
340 if (pos && store.getCount() && store.indexOf(pos.record) !== -1) {
341 pos.setRow(pos.record);
342 } else {
343 me.selection = null;
344 }
345 },
346
347 onStoreClear: function() {
348 this.callParent(arguments);
349 this.selection = null;
350 },
351
352 onStoreAdd: function() {
353 var me = this,
354 pos = me.getPosition();
355
356 me.callParent(arguments);
357 if (pos) {
358 pos.setRow(pos.record);
359 } else {
360 me.selection = null;
361 }
362 },
363
364 /**
365 * Set the current position based on where the user clicks.
366 * @private
367 * IMPORTANT* Due to V4.0.0 history, the cellIndex here is the index within ALL columns, including hidden.
368 */
369 onCellClick: function(view, cell, cellIndex, record, row, recordIndex, e) {
370 // Record index will be -1 if the clicked record is a metadata record and not selectable
371 if (recordIndex !== -1) {
372 this.setPosition(e.position);
373 }
374 },
375
376 // notify the view that the cell has been selected to update the ui
377 // appropriately and bring the cell into focus
378 onCellSelect: function(position, supressEvent) {
379 if (position && position.rowIdx !== undefined && position.rowIdx > -1) {
380 this.doSelect(position.record, /*keepExisting*/false, supressEvent);
381 }
382 },
383
384 // notify view that the cell has been deselected to update the ui
385 // appropriately
386 onCellDeselect: function(position, supressEvent) {
387 if (position && position.rowIdx !== undefined) {
388 this.doDeselect(position.record, supressEvent);
389 }
390 },
391
392 onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
393 var me = this,
394 pos, eventName, view;
395
396 if (isSelected) {
397 pos = me.nextSelection;
398 eventName = 'select';
399 } else {
400 pos = me.selection;
401 eventName = 'deselect';
402 }
403
404 // CellModel may be shared between two sides of a Lockable.
405 // The position must include a reference to the view in which the selection is current.
406 // Ensure we use the view specified by the position.
407 view = pos.view || me.primaryView;
408
409 if ((suppressEvent || me.fireEvent('before' + eventName, me, record, pos.rowIdx, pos.colIdx)) !== false &&
410 commitFn() !== false) {
411
412 if (isSelected) {
413 view.onCellSelect(pos);
414 } else {
415 view.onCellDeselect(pos);
416 delete me.selection;
417 }
418
419 if (!suppressEvent) {
420 me.fireEvent(eventName, me, record, pos.rowIdx, pos.colIdx);
421 }
422 }
423 },
424
425 refresh: function() {
426 var pos = this.getPosition(),
427 selRowIdx;
428
429 // Synchronize the current position's row with the row of the last selected record.
430 if (pos && (selRowIdx = this.store.indexOf(this.selected.last())) !== -1) {
431 pos.rowIdx = selRowIdx;
432 }
433 },
434
435 /**
436 * @private
437 * When grid uses {@link Ext.panel.Table#optimizedColumnMove optimizedColumnMove} (the default), this is added as a
438 * {@link Ext.panel.Table#columnmove columnmove} handler to correctly maintain the
439 * selected column using the same column header.
440 *
441 * If optimizedColumnMove === false, (which some grid Features set) then the view is refreshed,
442 * so this is not added as a handler because the selected column.
443 */
444 onColumnMove: function(headerCt, header, fromIdx, toIdx) {
445 var grid = headerCt.up('tablepanel');
446 if (grid) {
447 this.onViewRefresh(grid.view);
448 }
449 },
450
451 onUpdate: function(record) {
452 var me = this,
453 pos;
454
455 if (me.isSelected(record)) {
456 pos = me.selecting ? me.nextSelection : me.selection;
457 me.view.onCellSelect(pos);
458 }
459 },
460
461 onViewRefresh: function(view) {
462 var me = this,
463 pos = me.getPosition(),
464 newPos,
465 headerCt = view.headerCt,
466 record, column;
467
468 // Re-establish selection of the same cell coordinate.
469 // DO NOT fire events because the selected
470 if (pos && pos.view === view) {
471 record = pos.record;
472 column = pos.column;
473
474 // After a refresh, recreate the selection using the same record and grid column as before
475 if (!column.isDescendantOf(headerCt)) {
476 // column header is not a child of the header container
477 // this happens when the grid is reconfigured with new columns
478 // make a best effor to select something by matching on id, then text, then dataIndex
479 column = headerCt.queryById(column.id) ||
480 headerCt.down('[text="' + column.text + '"]') ||
481 headerCt.down('[dataIndex="' + column.dataIndex + '"]');
482 }
483
484 // If we have a columnHeader (either the column header that already exists in
485 // the headerCt, or a suitable match that was found after reconfiguration)
486 // AND the record still exists in the store (or a record matching the id of
487 // the previously selected record) We are ok to go ahead and set the selection
488 if (pos.record) {
489 if (column && (view.store.indexOfId(record.getId()) !== -1)) {
490 newPos = new Ext.grid.CellContext(view).setPosition({
491 row: record,
492 column: column
493 });
494 me.setPosition(newPos);
495 }
496 } else {
497 me.selection = null;
498 }
499 }
500 },
501
502 /**
503 * @private
504 * Used internally by CellEditing
505 */
506 selectByPosition: function(position, suppressEvent) {
507 this.setPosition(position, suppressEvent);
508 }
509 });