]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /*\r |
2 | * Note that this control will most likely remain as an example, and not as a core Ext form\r | |
3 | * control. However, the API will be changing in a future release and so should not yet be\r | |
4 | * treated as a final, stable API at this time.\r | |
5 | */\r | |
6 | \r | |
7 | /**\r | |
8 | * A control that allows selection of between two Ext.ux.form.MultiSelect controls.\r | |
9 | */\r | |
10 | Ext.define('Ext.ux.form.ItemSelector', {\r | |
11 | extend: 'Ext.ux.form.MultiSelect',\r | |
12 | alias: ['widget.itemselectorfield', 'widget.itemselector'],\r | |
13 | alternateClassName: ['Ext.ux.ItemSelector'],\r | |
14 | requires: [\r | |
15 | 'Ext.button.Button',\r | |
16 | 'Ext.ux.form.MultiSelect'\r | |
17 | ],\r | |
18 | \r | |
19 | /**\r | |
20 | * @cfg {Boolean} [hideNavIcons=false] True to hide the navigation icons\r | |
21 | */\r | |
22 | hideNavIcons:false,\r | |
23 | \r | |
24 | /**\r | |
25 | * @cfg {Array} buttons Defines the set of buttons that should be displayed in between the ItemSelector\r | |
26 | * fields. Defaults to <tt>['top', 'up', 'add', 'remove', 'down', 'bottom']</tt>. These names are used\r | |
27 | * to build the button CSS class names, and to look up the button text labels in {@link #buttonsText}.\r | |
28 | * This can be overridden with a custom Array to change which buttons are displayed or their order.\r | |
29 | */\r | |
30 | buttons: ['top', 'up', 'add', 'remove', 'down', 'bottom'],\r | |
31 | \r | |
32 | /**\r | |
33 | * @cfg {Object} buttonsText The tooltips for the {@link #buttons}.\r | |
34 | * Labels for buttons.\r | |
35 | */\r | |
36 | buttonsText: {\r | |
37 | top: "Move to Top",\r | |
38 | up: "Move Up",\r | |
39 | add: "Add to Selected",\r | |
40 | remove: "Remove from Selected",\r | |
41 | down: "Move Down",\r | |
42 | bottom: "Move to Bottom"\r | |
43 | },\r | |
44 | \r | |
45 | layout: {\r | |
46 | type: 'hbox',\r | |
47 | align: 'stretch'\r | |
48 | },\r | |
49 | \r | |
50 | ariaRole: 'group',\r | |
51 | \r | |
52 | initComponent: function() {\r | |
53 | var me = this;\r | |
54 | \r | |
55 | me.ddGroup = me.id + '-dd';\r | |
56 | me.ariaRenderAttributes = me.ariaRenderAttributes || {};\r | |
57 | me.ariaRenderAttributes['aria-labelledby'] = me.id + '-labelEl';\r | |
58 | \r | |
59 | me.callParent();\r | |
60 | \r | |
61 | // bindStore must be called after the fromField has been created because\r | |
62 | // it copies records from our configured Store into the fromField's Store\r | |
63 | me.bindStore(me.store);\r | |
64 | },\r | |
65 | \r | |
66 | createList: function(title){\r | |
67 | var me = this;\r | |
68 | \r | |
69 | return Ext.create('Ext.ux.form.MultiSelect', {\r | |
70 | // We don't want the multiselects themselves to act like fields,\r | |
71 | // so override these methods to prevent them from including\r | |
72 | // any of their values\r | |
73 | submitValue: false,\r | |
74 | getSubmitData: function(){\r | |
75 | return null;\r | |
76 | },\r | |
77 | getModelData: function(){\r | |
78 | return null; \r | |
79 | },\r | |
80 | flex: 1,\r | |
81 | dragGroup: me.ddGroup,\r | |
82 | dropGroup: me.ddGroup,\r | |
83 | title: title,\r | |
84 | store: {\r | |
85 | model: me.store.model,\r | |
86 | data: []\r | |
87 | },\r | |
88 | displayField: me.displayField,\r | |
89 | valueField: me.valueField,\r | |
90 | disabled: me.disabled,\r | |
91 | listeners: {\r | |
92 | boundList: {\r | |
93 | scope: me,\r | |
94 | itemdblclick: me.onItemDblClick,\r | |
95 | drop: me.syncValue\r | |
96 | }\r | |
97 | }\r | |
98 | });\r | |
99 | },\r | |
100 | \r | |
101 | setupItems: function() {\r | |
102 | var me = this;\r | |
103 | \r | |
104 | me.fromField = me.createList(me.fromTitle);\r | |
105 | me.toField = me.createList(me.toTitle);\r | |
106 | \r | |
107 | return [\r | |
108 | me.fromField,\r | |
109 | {\r | |
110 | xtype: 'toolbar',\r | |
111 | margin: '0 4',\r | |
112 | padding: 0,\r | |
113 | layout: {\r | |
114 | type: 'vbox',\r | |
115 | pack: 'center'\r | |
116 | },\r | |
117 | items: me.createButtons()\r | |
118 | },\r | |
119 | me.toField\r | |
120 | ];\r | |
121 | },\r | |
122 | \r | |
123 | createButtons: function() {\r | |
124 | var me = this,\r | |
125 | buttons = [];\r | |
126 | \r | |
127 | if (!me.hideNavIcons) {\r | |
128 | Ext.Array.forEach(me.buttons, function(name) {\r | |
129 | buttons.push({\r | |
130 | xtype: 'button',\r | |
131 | ui: 'default',\r | |
132 | tooltip: me.buttonsText[name],\r | |
133 | ariaLabel: me.buttonsText[name],\r | |
134 | handler: me['on' + Ext.String.capitalize(name) + 'BtnClick'],\r | |
135 | cls: Ext.baseCSSPrefix + 'form-itemselector-btn',\r | |
136 | iconCls: Ext.baseCSSPrefix + 'form-itemselector-' + name,\r | |
137 | navBtn: true,\r | |
138 | scope: me,\r | |
139 | margin: '4 0 0 0'\r | |
140 | });\r | |
141 | });\r | |
142 | }\r | |
143 | return buttons;\r | |
144 | },\r | |
145 | \r | |
146 | /**\r | |
147 | * Get the selected records from the specified list.\r | |
148 | * \r | |
149 | * Records will be returned *in store order*, not in order of selection.\r | |
150 | * @param {Ext.view.BoundList} list The list to read selections from.\r | |
151 | * @return {Ext.data.Model[]} The selected records in store order.\r | |
152 | * \r | |
153 | */\r | |
154 | getSelections: function(list) {\r | |
155 | var store = list.getStore();\r | |
156 | \r | |
157 | return Ext.Array.sort(list.getSelectionModel().getSelection(), function(a, b) {\r | |
158 | a = store.indexOf(a);\r | |
159 | b = store.indexOf(b);\r | |
160 | \r | |
161 | if (a < b) {\r | |
162 | return -1;\r | |
163 | } else if (a > b) {\r | |
164 | return 1;\r | |
165 | }\r | |
166 | return 0;\r | |
167 | });\r | |
168 | },\r | |
169 | \r | |
170 | onTopBtnClick : function() {\r | |
171 | var list = this.toField.boundList,\r | |
172 | store = list.getStore(),\r | |
173 | selected = this.getSelections(list);\r | |
174 | \r | |
175 | store.suspendEvents();\r | |
176 | store.remove(selected, true);\r | |
177 | store.insert(0, selected);\r | |
178 | store.resumeEvents();\r | |
179 | list.refresh();\r | |
180 | this.syncValue(); \r | |
181 | list.getSelectionModel().select(selected);\r | |
182 | },\r | |
183 | \r | |
184 | onBottomBtnClick : function() {\r | |
185 | var list = this.toField.boundList,\r | |
186 | store = list.getStore(),\r | |
187 | selected = this.getSelections(list);\r | |
188 | \r | |
189 | store.suspendEvents();\r | |
190 | store.remove(selected, true);\r | |
191 | store.add(selected);\r | |
192 | store.resumeEvents();\r | |
193 | list.refresh();\r | |
194 | this.syncValue();\r | |
195 | list.getSelectionModel().select(selected);\r | |
196 | },\r | |
197 | \r | |
198 | onUpBtnClick : function() {\r | |
199 | var list = this.toField.boundList,\r | |
200 | store = list.getStore(),\r | |
201 | selected = this.getSelections(list),\r | |
202 | rec,\r | |
203 | i = 0,\r | |
204 | len = selected.length,\r | |
205 | index = 0;\r | |
206 | \r | |
207 | // Move each selection up by one place if possible\r | |
208 | store.suspendEvents();\r | |
209 | for (; i < len; ++i, index++) {\r | |
210 | rec = selected[i];\r | |
211 | index = Math.max(index, store.indexOf(rec) - 1);\r | |
212 | store.remove(rec, true);\r | |
213 | store.insert(index, rec);\r | |
214 | }\r | |
215 | store.resumeEvents();\r | |
216 | list.refresh();\r | |
217 | this.syncValue();\r | |
218 | list.getSelectionModel().select(selected);\r | |
219 | },\r | |
220 | \r | |
221 | onDownBtnClick : function() {\r | |
222 | var list = this.toField.boundList,\r | |
223 | store = list.getStore(),\r | |
224 | selected = this.getSelections(list),\r | |
225 | rec,\r | |
226 | i = selected.length - 1,\r | |
227 | index = store.getCount() - 1;\r | |
228 | \r | |
229 | // Move each selection down by one place if possible\r | |
230 | store.suspendEvents();\r | |
231 | for (; i > -1; --i, index--) {\r | |
232 | rec = selected[i];\r | |
233 | index = Math.min(index, store.indexOf(rec) + 1);\r | |
234 | store.remove(rec, true);\r | |
235 | store.insert(index, rec);\r | |
236 | }\r | |
237 | store.resumeEvents();\r | |
238 | list.refresh();\r | |
239 | this.syncValue();\r | |
240 | list.getSelectionModel().select(selected);\r | |
241 | },\r | |
242 | \r | |
243 | onAddBtnClick : function() {\r | |
244 | var me = this,\r | |
245 | selected = me.getSelections(me.fromField.boundList);\r | |
246 | \r | |
247 | me.moveRec(true, selected);\r | |
248 | me.toField.boundList.getSelectionModel().select(selected);\r | |
249 | },\r | |
250 | \r | |
251 | onRemoveBtnClick : function() {\r | |
252 | var me = this,\r | |
253 | selected = me.getSelections(me.toField.boundList);\r | |
254 | \r | |
255 | me.moveRec(false, selected);\r | |
256 | me.fromField.boundList.getSelectionModel().select(selected);\r | |
257 | },\r | |
258 | \r | |
259 | moveRec: function(add, recs) {\r | |
260 | var me = this,\r | |
261 | fromField = me.fromField,\r | |
262 | toField = me.toField,\r | |
263 | fromStore = add ? fromField.store : toField.store,\r | |
264 | toStore = add ? toField.store : fromField.store;\r | |
265 | \r | |
266 | fromStore.suspendEvents();\r | |
267 | toStore.suspendEvents();\r | |
268 | fromStore.remove(recs);\r | |
269 | toStore.add(recs);\r | |
270 | fromStore.resumeEvents();\r | |
271 | toStore.resumeEvents();\r | |
272 | \r | |
273 | // If the list item was focused when moved (e.g. via double-click)\r | |
274 | // then removing it will cause the focus to be thrown back to the\r | |
275 | // document body. Which might disrupt things if ItemSelector is\r | |
276 | // contained by a floating thingie like a Menu.\r | |
277 | // Focusing the list itself will prevent that.\r | |
278 | if (fromField.boundList.containsFocus) {\r | |
279 | fromField.boundList.focus();\r | |
280 | }\r | |
281 | \r | |
282 | fromField.boundList.refresh();\r | |
283 | toField.boundList.refresh();\r | |
284 | \r | |
285 | me.syncValue();\r | |
286 | },\r | |
287 | \r | |
288 | // Synchronizes the submit value with the current state of the toStore\r | |
289 | syncValue: function() {\r | |
290 | var me = this; \r | |
291 | me.mixins.field.setValue.call(me, me.setupValue(me.toField.store.getRange()));\r | |
292 | },\r | |
293 | \r | |
294 | onItemDblClick: function(view, rec) {\r | |
295 | this.moveRec(view === this.fromField.boundList, rec);\r | |
296 | },\r | |
297 | \r | |
298 | setValue: function(value) {\r | |
299 | var me = this,\r | |
300 | fromField = me.fromField,\r | |
301 | toField = me.toField,\r | |
302 | fromStore = fromField.store,\r | |
303 | toStore = toField.store,\r | |
304 | selected;\r | |
305 | \r | |
306 | // Wait for from store to be loaded\r | |
307 | if (!me.fromStorePopulated) {\r | |
308 | me.fromField.store.on({\r | |
309 | load: Ext.Function.bind(me.setValue, me, [value]),\r | |
310 | single: true\r | |
311 | });\r | |
312 | return;\r | |
313 | }\r | |
314 | \r | |
315 | value = me.setupValue(value);\r | |
316 | me.mixins.field.setValue.call(me, value);\r | |
317 | \r | |
318 | selected = me.getRecordsForValue(value);\r | |
319 | \r | |
320 | // Clear both left and right Stores.\r | |
321 | // Both stores must not fire events during this process.\r | |
322 | fromStore.suspendEvents();\r | |
323 | toStore.suspendEvents();\r | |
324 | fromStore.removeAll();\r | |
325 | toStore.removeAll();\r | |
326 | \r | |
327 | // Reset fromStore\r | |
328 | me.populateFromStore(me.store);\r | |
329 | \r | |
330 | // Copy selection across to toStore\r | |
331 | Ext.Array.forEach(selected, function(rec){\r | |
332 | // In the from store, move it over\r | |
333 | if (fromStore.indexOf(rec) > -1) {\r | |
334 | fromStore.remove(rec);\r | |
335 | }\r | |
336 | toStore.add(rec);\r | |
337 | });\r | |
338 | \r | |
339 | // Stores may now fire events\r | |
340 | fromStore.resumeEvents();\r | |
341 | toStore.resumeEvents();\r | |
342 | \r | |
343 | // Refresh both sides and then update the app layout\r | |
344 | Ext.suspendLayouts();\r | |
345 | fromField.boundList.refresh();\r | |
346 | toField.boundList.refresh();\r | |
347 | Ext.resumeLayouts(true); \r | |
348 | },\r | |
349 | \r | |
350 | onBindStore: function(store, initial) {\r | |
351 | var me = this;\r | |
352 | \r | |
353 | if (me.fromField) {\r | |
354 | me.fromField.store.removeAll();\r | |
355 | me.toField.store.removeAll();\r | |
356 | \r | |
357 | // Add everything to the from field as soon as the Store is loaded\r | |
358 | if (store.getCount()) {\r | |
359 | me.populateFromStore(store);\r | |
360 | } else {\r | |
361 | me.store.on('load', me.populateFromStore, me);\r | |
362 | }\r | |
363 | }\r | |
364 | },\r | |
365 | \r | |
366 | populateFromStore: function(store) {\r | |
367 | var fromStore = this.fromField.store;\r | |
368 | \r | |
369 | // Flag set when the fromStore has been loaded\r | |
370 | this.fromStorePopulated = true;\r | |
371 | \r | |
372 | fromStore.add(store.getRange());\r | |
373 | \r | |
374 | // setValue waits for the from Store to be loaded\r | |
375 | fromStore.fireEvent('load', fromStore);\r | |
376 | },\r | |
377 | \r | |
378 | onEnable: function(){\r | |
379 | var me = this;\r | |
380 | \r | |
381 | me.callParent();\r | |
382 | me.fromField.enable();\r | |
383 | me.toField.enable();\r | |
384 | \r | |
385 | Ext.Array.forEach(me.query('[navBtn]'), function(btn){\r | |
386 | btn.enable();\r | |
387 | });\r | |
388 | },\r | |
389 | \r | |
390 | onDisable: function(){\r | |
391 | var me = this;\r | |
392 | \r | |
393 | me.callParent();\r | |
394 | me.fromField.disable();\r | |
395 | me.toField.disable();\r | |
396 | \r | |
397 | Ext.Array.forEach(me.query('[navBtn]'), function(btn){\r | |
398 | btn.disable();\r | |
399 | });\r | |
400 | },\r | |
401 | \r | |
402 | onDestroy: function(){\r | |
403 | this.bindStore(null);\r | |
404 | this.callParent();\r | |
405 | }\r | |
406 | });\r |