]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * A selection model that renders a column of checkboxes that can be toggled to\r | |
3 | * select or deselect rows. The default mode for this selection model is MULTI.\r | |
4 | *\r | |
5 | * @example\r | |
6 | * var store = Ext.create('Ext.data.Store', {\r | |
7 | * fields: ['name', 'email', 'phone'],\r | |
8 | * data: [{\r | |
9 | * name: 'Lisa',\r | |
10 | * email: 'lisa@simpsons.com',\r | |
11 | * phone: '555-111-1224'\r | |
12 | * }, {\r | |
13 | * name: 'Bart',\r | |
14 | * email: 'bart@simpsons.com',\r | |
15 | * phone: '555-222-1234'\r | |
16 | * }, {\r | |
17 | * name: 'Homer',\r | |
18 | * email: 'homer@simpsons.com',\r | |
19 | * phone: '555-222-1244'\r | |
20 | * }, {\r | |
21 | * name: 'Marge',\r | |
22 | * email: 'marge@simpsons.com',\r | |
23 | * phone: '555-222-1254'\r | |
24 | * }]\r | |
25 | * });\r | |
26 | *\r | |
27 | * Ext.create('Ext.grid.Panel', {\r | |
28 | * title: 'Simpsons',\r | |
29 | * store: store,\r | |
30 | * columns: [{\r | |
31 | * text: 'Name',\r | |
32 | * dataIndex: 'name'\r | |
33 | * }, {\r | |
34 | * text: 'Email',\r | |
35 | * dataIndex: 'email',\r | |
36 | * flex: 1\r | |
37 | * }, {\r | |
38 | * text: 'Phone',\r | |
39 | * dataIndex: 'phone'\r | |
40 | * }],\r | |
41 | * height: 200,\r | |
42 | * width: 400,\r | |
43 | * renderTo: Ext.getBody(),\r | |
44 | * selModel: {\r | |
45 | * selType: 'checkboxmodel'\r | |
46 | * }\r | |
47 | * });\r | |
48 | *\r | |
49 | * The selection model will inject a header for the checkboxes in the first view\r | |
50 | * and according to the {@link #injectCheckbox} configuration.\r | |
51 | */\r | |
52 | Ext.define('Ext.selection.CheckboxModel', {\r | |
53 | alias: 'selection.checkboxmodel',\r | |
54 | extend: 'Ext.selection.RowModel',\r | |
55 | \r | |
56 | /**\r | |
57 | * @cfg {"SINGLE"/"SIMPLE"/"MULTI"} mode\r | |
58 | * Modes of selection.\r | |
59 | * Valid values are `"SINGLE"`, `"SIMPLE"`, and `"MULTI"`.\r | |
60 | */\r | |
61 | mode: 'MULTI',\r | |
62 | \r | |
63 | /**\r | |
64 | * @cfg {Number/String} [injectCheckbox=0]\r | |
65 | * The index at which to insert the checkbox column.\r | |
66 | * Supported values are a numeric index, and the strings 'first' and 'last'.\r | |
67 | */\r | |
68 | injectCheckbox: 0,\r | |
69 | \r | |
70 | /**\r | |
71 | * @cfg {Boolean} checkOnly\r | |
72 | * True if rows can only be selected by clicking on the checkbox column, not by clicking\r | |
73 | * on the row itself. Note that this only refers to selection via the UI, programmatic\r | |
74 | * selection will still occur regardless.\r | |
75 | */\r | |
76 | checkOnly: false,\r | |
77 | \r | |
78 | /**\r | |
79 | * @cfg {Boolean} showHeaderCheckbox\r | |
80 | * Configure as `false` to not display the header checkbox at the top of the column.\r | |
81 | * When the store is a {@link Ext.data.BufferedStore BufferedStore}, this configuration will\r | |
82 | * not be available because the buffered data set does not always contain all data. \r | |
83 | */\r | |
84 | showHeaderCheckbox: undefined,\r | |
85 | \r | |
86 | /**\r | |
87 | * @cfg {String} [checkSelector="x-grid-row-checker"]\r | |
88 | * The selector for determining whether the checkbox element is clicked. This may be changed to\r | |
89 | * allow for a wider area to be clicked, for example, the whole cell for the selector.\r | |
90 | */\r | |
91 | checkSelector: '.' + Ext.baseCSSPrefix + 'grid-row-checker',\r | |
92 | \r | |
93 | allowDeselect: true,\r | |
94 | \r | |
95 | headerWidth: 24,\r | |
96 | \r | |
97 | /**\r | |
98 | * @private\r | |
99 | */\r | |
100 | checkerOnCls: Ext.baseCSSPrefix + 'grid-hd-checker-on',\r | |
101 | \r | |
102 | tdCls: Ext.baseCSSPrefix + 'grid-cell-special ' + Ext.baseCSSPrefix + 'grid-cell-row-checker',\r | |
103 | \r | |
104 | \r | |
105 | constructor: function() {\r | |
106 | var me = this;\r | |
107 | me.callParent(arguments);\r | |
108 | \r | |
109 | // If mode is single and showHeaderCheck isn't explicity set to\r | |
110 | // true, hide it.\r | |
111 | if (me.mode === 'SINGLE') {\r | |
112 | //<debug>\r | |
113 | if (me.showHeaderCheckbox) {\r | |
114 | Ext.Error.raise('The header checkbox is not supported for SINGLE mode selection models.');\r | |
115 | }\r | |
116 | //</debug>\r | |
117 | me.showHeaderCheckbox = false;\r | |
118 | }\r | |
119 | },\r | |
120 | \r | |
121 | beforeViewRender: function(view) {\r | |
122 | var me = this,\r | |
123 | owner;\r | |
124 | \r | |
125 | me.callParent(arguments);\r | |
126 | \r | |
127 | // if we have a locked header, only hook up to the first\r | |
128 | if (!me.hasLockedHeader() || view.headerCt.lockedCt) {\r | |
129 | me.addCheckbox(view, true);\r | |
130 | owner = view.ownerCt;\r | |
131 | // Listen to the outermost reconfigure event\r | |
132 | if (view.headerCt.lockedCt) {\r | |
133 | owner = owner.ownerCt;\r | |
134 | }\r | |
135 | me.mon(owner, 'reconfigure', me.onReconfigure, me);\r | |
136 | }\r | |
137 | },\r | |
138 | \r | |
139 | bindComponent: function(view) {\r | |
140 | this.sortable = false;\r | |
141 | this.callParent(arguments);\r | |
142 | },\r | |
143 | \r | |
144 | hasLockedHeader: function(){\r | |
145 | var views = this.views,\r | |
146 | vLen = views.length,\r | |
147 | v;\r | |
148 | \r | |
149 | for (v = 0; v < vLen; v++) {\r | |
150 | if (views[v].headerCt.lockedCt) {\r | |
151 | return true;\r | |
152 | }\r | |
153 | }\r | |
154 | return false;\r | |
155 | },\r | |
156 | \r | |
157 | /**\r | |
158 | * Add the header checkbox to the header row\r | |
159 | * @private\r | |
160 | * @param {Boolean} initial True if we're binding for the first time.\r | |
161 | */\r | |
162 | addCheckbox: function(view, initial){\r | |
163 | var me = this,\r | |
164 | checkbox = me.injectCheckbox,\r | |
165 | headerCt = view.headerCt;\r | |
166 | \r | |
167 | // Preserve behaviour of false, but not clear why that would ever be done.\r | |
168 | if (checkbox !== false) {\r | |
169 | if (checkbox === 'first') {\r | |
170 | checkbox = 0;\r | |
171 | } else if (checkbox === 'last') {\r | |
172 | checkbox = headerCt.getColumnCount();\r | |
173 | }\r | |
174 | Ext.suspendLayouts();\r | |
175 | if (view.getStore().isBufferedStore) {\r | |
176 | me.showHeaderCheckbox = false;\r | |
177 | }\r | |
178 | me.column = headerCt.add(checkbox, me.getHeaderConfig());\r | |
179 | Ext.resumeLayouts();\r | |
180 | }\r | |
181 | \r | |
182 | if (initial !== true) {\r | |
183 | view.refresh();\r | |
184 | }\r | |
185 | },\r | |
186 | \r | |
187 | /**\r | |
188 | * Handles the grid's reconfigure event. Adds the checkbox header if the columns have been reconfigured.\r | |
189 | * @private\r | |
190 | * @param {Ext.panel.Table} grid\r | |
191 | * @param {Ext.data.Store} store\r | |
192 | * @param {Object[]} columns\r | |
193 | */\r | |
194 | onReconfigure: function(grid, store, columns) {\r | |
195 | if (columns) {\r | |
196 | this.addCheckbox(this.views[0]);\r | |
197 | }\r | |
198 | },\r | |
199 | \r | |
200 | /**\r | |
201 | * Toggle the ui header between checked and unchecked state.\r | |
202 | * @param {Boolean} isChecked\r | |
203 | * @private\r | |
204 | */\r | |
205 | toggleUiHeader: function(isChecked) {\r | |
206 | var view = this.views[0],\r | |
207 | headerCt = view.headerCt,\r | |
208 | checkHd = headerCt.child('gridcolumn[isCheckerHd]'),\r | |
209 | cls = this.checkerOnCls;\r | |
210 | \r | |
211 | if (checkHd) {\r | |
212 | if (isChecked) {\r | |
213 | checkHd.addCls(cls);\r | |
214 | } else {\r | |
215 | checkHd.removeCls(cls);\r | |
216 | }\r | |
217 | }\r | |
218 | },\r | |
219 | \r | |
220 | /**\r | |
221 | * Toggle between selecting all and deselecting all when clicking on\r | |
222 | * a checkbox header.\r | |
223 | */\r | |
224 | onHeaderClick: function(headerCt, header, e) {\r | |
225 | var me = this,\r | |
226 | isChecked;\r | |
227 | \r | |
228 | if (header === me.column && me.mode !== 'SINGLE') {\r | |
229 | e.stopEvent();\r | |
230 | isChecked = header.el.hasCls(Ext.baseCSSPrefix + 'grid-hd-checker-on');\r | |
231 | \r | |
232 | if (isChecked) {\r | |
233 | me.deselectAll();\r | |
234 | } else {\r | |
235 | me.selectAll();\r | |
236 | }\r | |
237 | }\r | |
238 | },\r | |
239 | \r | |
240 | /**\r | |
241 | * Retrieve a configuration to be used in a HeaderContainer.\r | |
242 | * This should be used when injectCheckbox is set to false.\r | |
243 | */\r | |
244 | getHeaderConfig: function() {\r | |
245 | var me = this,\r | |
246 | showCheck = me.showHeaderCheckbox !== false;\r | |
247 | \r | |
248 | return {\r | |
249 | xtype: 'gridcolumn',\r | |
250 | ignoreExport: true,\r | |
251 | isCheckerHd: showCheck,\r | |
252 | text : ' ',\r | |
253 | clickTargetName: 'el',\r | |
254 | width: me.headerWidth,\r | |
255 | sortable: false,\r | |
256 | draggable: false,\r | |
257 | resizable: false,\r | |
258 | hideable: false,\r | |
259 | menuDisabled: true,\r | |
260 | dataIndex: '',\r | |
261 | tdCls: me.tdCls,\r | |
262 | cls: showCheck ? Ext.baseCSSPrefix + 'column-header-checkbox ' : '',\r | |
263 | defaultRenderer: me.renderer.bind(me),\r | |
264 | editRenderer: me.editRenderer || me.renderEmpty,\r | |
265 | locked: me.hasLockedHeader(),\r | |
266 | processEvent: me.processColumnEvent\r | |
267 | };\r | |
268 | },\r | |
269 | \r | |
270 | /**\r | |
271 | * @private\r | |
272 | * Process and refire events routed from the Ext.panel.Table's processEvent method.\r | |
273 | * Also fires any configured click handlers. By default, cancels the mousedown event to prevent selection.\r | |
274 | * Returns the event handler's status to allow canceling of GridView's bubbling process.\r | |
275 | */\r | |
276 | processColumnEvent : function(type, view, cell, recordIndex, cellIndex, e, record, row) {\r | |
277 | var navModel = view.getNavigationModel();\r | |
278 | \r | |
279 | // Fire a navigate event upon SPACE in acvtionable mode.\r | |
280 | // SPACE events are ignored by the NavModel in actionable mode.\r | |
281 | if (e.type === 'keydown' && view.actionableMode && e.getKey() === e.SPACE) {\r | |
282 | navModel.fireEvent('navigate', {\r | |
283 | view: view,\r | |
284 | navigationModel: navModel,\r | |
285 | keyEvent: e,\r | |
286 | position: e.position,\r | |
287 | recordIndex: recordIndex,\r | |
288 | record: record,\r | |
289 | item: e.item,\r | |
290 | cell: e.position.cellElement,\r | |
291 | columnIndex: e.position.colIdx,\r | |
292 | column: e.position.column\r | |
293 | });\r | |
294 | }\r | |
295 | },\r | |
296 | \r | |
297 | renderEmpty: function() {\r | |
298 | return ' ';\r | |
299 | },\r | |
300 | \r | |
301 | // After refresh, ensure that the header checkbox state matches\r | |
302 | refresh: function() {\r | |
303 | this.callParent(arguments);\r | |
304 | this.updateHeaderState();\r | |
305 | },\r | |
306 | \r | |
307 | /**\r | |
308 | * Generates the HTML to be rendered in the injected checkbox column for each row.\r | |
309 | * Creates the standard checkbox markup by default; can be overridden to provide custom rendering.\r | |
310 | * See {@link Ext.grid.column.Column#renderer} for description of allowed parameters.\r | |
311 | */\r | |
312 | renderer: function(value, metaData, record, rowIndex, colIndex, store, view) {\r | |
313 | return '<div class="' + Ext.baseCSSPrefix + 'grid-row-checker" role="button" tabIndex="0"> </div>';\r | |
314 | },\r | |
315 | \r | |
316 | selectByPosition: function (position, keepExisting) {\r | |
317 | if (!position.isCellContext) {\r | |
318 | position = new Ext.grid.CellContext(this.view).setPosition(position.row, position.column);\r | |
319 | }\r | |
320 | \r | |
321 | // Do not select if checkOnly, and the requested position is not the check column\r | |
322 | if (!this.checkOnly || position.column === this.column) {\r | |
323 | this.callParent([position, keepExisting]);\r | |
324 | }\r | |
325 | },\r | |
326 | \r | |
327 | /**\r | |
328 | * Synchronize header checker value as selection changes.\r | |
329 | * @private\r | |
330 | */\r | |
331 | onSelectChange: function() {\r | |
332 | this.callParent(arguments);\r | |
333 | if (!this.suspendChange) {\r | |
334 | this.updateHeaderState();\r | |
335 | }\r | |
336 | },\r | |
337 | \r | |
338 | /**\r | |
339 | * @private\r | |
340 | */\r | |
341 | onStoreLoad: function() {\r | |
342 | this.callParent(arguments);\r | |
343 | this.updateHeaderState();\r | |
344 | },\r | |
345 | \r | |
346 | onStoreAdd: function() {\r | |
347 | this.callParent(arguments);\r | |
348 | this.updateHeaderState();\r | |
349 | },\r | |
350 | \r | |
351 | onStoreRemove: function() {\r | |
352 | this.callParent(arguments);\r | |
353 | this.updateHeaderState();\r | |
354 | },\r | |
355 | \r | |
356 | onStoreRefresh: function(){\r | |
357 | this.callParent(arguments); \r | |
358 | this.updateHeaderState();\r | |
359 | },\r | |
360 | \r | |
361 | maybeFireSelectionChange: function(fireEvent) {\r | |
362 | if (fireEvent && !this.suspendChange) {\r | |
363 | this.updateHeaderState();\r | |
364 | }\r | |
365 | this.callParent(arguments);\r | |
366 | },\r | |
367 | \r | |
368 | resumeChanges: function(){\r | |
369 | this.callParent();\r | |
370 | if (!this.suspendChange) {\r | |
371 | this.updateHeaderState();\r | |
372 | }\r | |
373 | },\r | |
374 | \r | |
375 | /**\r | |
376 | * @private\r | |
377 | */\r | |
378 | updateHeaderState: function() {\r | |
379 | // check to see if all records are selected\r | |
380 | var me = this,\r | |
381 | store = me.store,\r | |
382 | storeCount = store.getCount(),\r | |
383 | views = me.views,\r | |
384 | hdSelectStatus = false,\r | |
385 | selectedCount = 0,\r | |
386 | selected, len, i;\r | |
387 | \r | |
388 | if (!store.isBufferedStore && storeCount > 0) {\r | |
389 | selected = me.selected;\r | |
390 | hdSelectStatus = true;\r | |
391 | for (i = 0, len = selected.getCount(); i < len; ++i) {\r | |
392 | if (store.indexOfId(selected.getAt(i).id) === -1) {\r | |
393 | break;\r | |
394 | }\r | |
395 | ++selectedCount;\r | |
396 | }\r | |
397 | hdSelectStatus = storeCount === selectedCount;\r | |
398 | }\r | |
399 | \r | |
400 | if (views && views.length) {\r | |
401 | me.toggleUiHeader(hdSelectStatus);\r | |
402 | }\r | |
403 | },\r | |
404 | \r | |
405 | vetoSelection: function(e) {\r | |
406 | var me = this,\r | |
407 | column = me.column,\r | |
408 | veto, isClick, isSpace;\r | |
409 | \r | |
410 | if (me.checkOnly) {\r | |
411 | isClick = e.type === 'click' && e.getTarget(me.checkSelector);\r | |
412 | isSpace = e.getKey() === e.SPACE && e.position.column === column;\r | |
413 | veto = !(isClick || isSpace);\r | |
414 | }\r | |
415 | return veto || me.callParent([e]);\r | |
416 | },\r | |
417 | \r | |
418 | destroy: function() {\r | |
419 | this.column = null;\r | |
420 | this.callParent();\r | |
421 | },\r | |
422 | \r | |
423 | privates: {\r | |
424 | onBeforeNavigate: function(metaEvent) {\r | |
425 | var e = metaEvent.keyEvent;\r | |
426 | if (this.selectionMode !== 'SINGLE') {\r | |
427 | metaEvent.ctrlKey = metaEvent.ctrlKey || e.ctrlKey || (e.type === 'click' && !e.shiftKey) || e.getKey() === e.SPACE;\r | |
428 | }\r | |
429 | },\r | |
430 | \r | |
431 | selectWithEventMulti: function(record, e, isSelected) {\r | |
432 | var me = this;\r | |
433 | \r | |
434 | if (!e.shiftKey && !e.ctrlKey && e.getTarget(me.checkSelector)) {\r | |
435 | if (isSelected) {\r | |
436 | me.doDeselect(record);\r | |
437 | } else {\r | |
438 | me.doSelect(record, true);\r | |
439 | }\r | |
440 | } else {\r | |
441 | me.callParent([record, e, isSelected]);\r | |
442 | }\r | |
443 | }\r | |
444 | }\r | |
445 | });\r |