]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - form/ComboGrid.js
add css and image directory to packaging
[proxmox-widget-toolkit.git] / form / ComboGrid.js
CommitLineData
066babdc
DM
1/*
2 * ComboGrid component: a ComboBox where the dropdown menu (the
3 * "Picker") is a Grid with Rows and Columns expects a listConfig
4 * object with a columns property roughly based on the GridPicker from
5 * https://www.sencha.com/forum/showthread.php?299909
6 *
7*/
8
9Ext.define('Proxmox.form.ComboGrid', {
10 extend: 'Ext.form.field.ComboBox',
11 alias: ['widget.proxmoxComboGrid'],
12
13 // this value is used as default value after load()
14 preferredValue: undefined,
15
16 // hack: allow to select empty value
17 // seems extjs does not allow that when 'editable == false'
18 onKeyUp: function(e, t) {
19 var me = this;
20 var key = e.getKey();
21
22 if (!me.editable && me.allowBlank && !me.multiSelect &&
23 (key == e.BACKSPACE || key == e.DELETE)) {
24 me.setValue('');
25 }
26
27 me.callParent(arguments);
28 },
29
227159ec
TL
30 config: {
31 skipEmptyText: false,
1ccb53ec 32 notFoundIsValid: false,
227159ec
TL
33 deleteEmpty: false,
34 },
35
066babdc
DM
36 // needed to trigger onKeyUp etc.
37 enableKeyEvents: true,
38
3ab80554
DC
39 editable: false,
40
066babdc
DM
41 // override ExtJS method
42 // if the field has multiSelect enabled, the store is not loaded, and
43 // the displayfield == valuefield, it saves the rawvalue as an array
44 // but the getRawValue method is only defined in the textfield class
45 // (which has not to deal with arrays) an returns the string in the
46 // field (not an array)
47 //
48 // so if we have multiselect enabled, return the rawValue (which
49 // should be an array) and else we do callParent so
50 // it should not impact any other use of the class
51 getRawValue: function() {
52 var me = this;
53 if (me.multiSelect) {
54 return me.rawValue;
55 } else {
56 return me.callParent();
57 }
58 },
59
227159ec
TL
60 getSubmitData: function() {
61 var me = this;
62
63 let data = null;
64 if (!me.disabled && me.submitValue) {
65 let val = me.getSubmitValue();
66 if (val !== null) {
67 data = {};
68 data[me.getName()] = val;
69 } else if (me.getDeleteEmpty()) {
70 data = {};
71 data['delete'] = me.getName();
72 }
73 }
74 return data;
75 },
76
77 getSubmitValue: function() {
78 var me = this;
79
80 var value = me.callParent();
81 if (value !== '') {
82 return value;
83 }
84
85 return me.getSkipEmptyText() ? null: value;
86 },
87
a8131b5b
TL
88 setAllowBlank: function(allowBlank) {
89 this.allowBlank = allowBlank;
90 this.validate();
91 },
92
066babdc
DM
93// override ExtJS protected method
94 onBindStore: function(store, initial) {
95 var me = this,
96 picker = me.picker,
97 extraKeySpec,
98 valueCollectionConfig;
99
100 // We're being bound, not unbound...
101 if (store) {
102 // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
103 if (store.autoCreated) {
104 me.queryMode = 'local';
105 me.valueField = me.displayField = 'field1';
106 if (!store.expanded) {
107 me.displayField = 'field2';
108 }
109
110 // displayTpl config will need regenerating with the autogenerated displayField name 'field1'
111 me.setDisplayTpl(null);
112 }
113 if (!Ext.isDefined(me.valueField)) {
114 me.valueField = me.displayField;
115 }
116
117 // Add a byValue index to the store so that we can efficiently look up records by the value field
118 // when setValue passes string value(s).
119 // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
120 // are found, they are all returned by the get call.
121 // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
122 // if unique is true, CollectionKey keeps the *last* matching value.
123 extraKeySpec = {
124 byValue: {
125 rootProperty: 'data',
126 unique: false
127 }
128 };
129 extraKeySpec.byValue.property = me.valueField;
130 store.setExtraKeys(extraKeySpec);
131
132 if (me.displayField === me.valueField) {
133 store.byText = store.byValue;
134 } else {
135 extraKeySpec.byText = {
136 rootProperty: 'data',
137 unique: false
138 };
139 extraKeySpec.byText.property = me.displayField;
140 store.setExtraKeys(extraKeySpec);
141 }
142
143 // We hold a collection of the values which have been selected, keyed by this field's valueField.
144 // This collection also functions as the selected items collection for the BoundList's selection model
145 valueCollectionConfig = {
146 rootProperty: 'data',
147 extraKeys: {
148 byInternalId: {
149 property: 'internalId'
150 },
151 byValue: {
152 property: me.valueField,
153 rootProperty: 'data'
154 }
155 },
156 // Whenever this collection is changed by anyone, whether by this field adding to it,
157 // or the BoundList operating, we must refresh our value.
158 listeners: {
159 beginupdate: me.onValueCollectionBeginUpdate,
160 endupdate: me.onValueCollectionEndUpdate,
161 scope: me
162 }
163 };
164
165 // This becomes our collection of selected records for the Field.
166 me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
167
168 // We use the selected Collection as our value collection and the basis
169 // for rendering the tag list.
170
171 //proxmox override: since the picker is represented by a grid panel,
172 // we changed here the selection to RowModel
173 me.pickerSelectionModel = new Ext.selection.RowModel({
174 mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
175 // There are situations when a row is selected on mousedown but then the mouse is dragged to another row
176 // and released. In these situations, the event target for the click event won't be the row where the mouse
177 // was released but the boundview. The view will then determine that it should fire a container click, and
178 // the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will
179 // prevent the model from deselecting.
180 deselectOnContainerClick: false,
181 enableInitialSelection: false,
182 pruneRemoved: false,
183 selected: me.valueCollection,
184 store: store,
185 listeners: {
186 scope: me,
187 lastselectedchanged: me.updateBindSelection
188 }
189 });
190
191 if (!initial) {
192 me.resetToDefault();
193 }
194
195 if (picker) {
196 picker.setSelectionModel(me.pickerSelectionModel);
197 if (picker.getStore() !== store) {
198 picker.bindStore(store);
199 }
200 }
201 }
202 },
203
204 // copied from ComboBox
205 createPicker: function() {
206 var me = this;
207 var picker;
208
209 var pickerCfg = Ext.apply({
210 // proxmox overrides: display a grid for selection
211 xtype: 'gridpanel',
212 id: me.pickerId,
213 pickerField: me,
214 floating: true,
215 hidden: true,
216 store: me.store,
217 displayField: me.displayField,
218 preserveScrollOnRefresh: true,
219 pageSize: me.pageSize,
220 tpl: me.tpl,
221 selModel: me.pickerSelectionModel,
222 focusOnToFront: false
223 }, me.listConfig, me.defaultListConfig);
224
225 picker = me.picker || Ext.widget(pickerCfg);
226
227 if (picker.getStore() !== me.store) {
228 picker.bindStore(me.store);
229 }
230
231 if (me.pageSize) {
232 picker.pagingToolbar.on('beforechange', me.onPageChange, me);
233 }
234
235 // proxmox overrides: pass missing method in gridPanel to its view
236 picker.refresh = function() {
237 picker.getSelectionModel().select(me.valueCollection.getRange());
238 picker.getView().refresh();
239 };
240 picker.getNodeByRecord = function() {
241 picker.getView().getNodeByRecord(arguments);
242 };
243
244 // We limit the height of the picker to fit in the space above
245 // or below this field unless the picker has its own ideas about that.
246 if (!picker.initialConfig.maxHeight) {
247 picker.on({
248 beforeshow: me.onBeforePickerShow,
249 scope: me
250 });
251 }
252 picker.getSelectionModel().on({
253 beforeselect: me.onBeforeSelect,
254 beforedeselect: me.onBeforeDeselect,
255 focuschange: me.onFocusChange,
256 selectionChange: function (sm, selectedRecords) {
257 var me = this;
258 if (selectedRecords.length) {
259 me.setValue(selectedRecords);
260 me.fireEvent('select', me, selectedRecords);
261 }
262 },
263 scope: me
264 });
265
266 // hack for extjs6
267 // when the clicked item is the same as the previously selected,
268 // it does not select the item
269 // instead we hide the picker
270 if (!me.multiSelect) {
271 picker.on('itemclick', function (sm,record) {
272 if (picker.getSelection()[0] === record) {
273 picker.hide();
274 }
275 });
276 }
277
278 // when our store is not yet loaded, we increase
279 // the height of the gridpanel, so that we can see
280 // the loading mask
281 //
282 // we save the minheight to reset it after the load
283 picker.on('show', function() {
284 if (me.enableLoadMask) {
285 me.savedMinHeight = picker.getMinHeight();
286 picker.setMinHeight(100);
287 }
288 });
289
290 picker.getNavigationModel().navigateOnSpace = false;
291
292 return picker;
293 },
294
1ccb53ec
TL
295 isValueInStore: function(value) {
296 var me = this;
297 var store = me.store;
298 var found = false;
299
300 if (!store) {
301 return found;
302 }
303
304 if (Ext.isArray(value)) {
305 Ext.Array.each(value, function(v) {
306 if (store.findRecord(me.valueField, v)) {
307 found = true;
308 return false; // break
309 }
310 });
311 } else {
312 found = !!store.findRecord(me.valueField, value);
313 }
314
315 return found;
316 },
317
318 validator: function (value) {
319 var me = this;
320
321 if (!value) {
322 return true; // handled later by allowEmpty in the getErrors call chain
323 }
324
325 if (!(me.notFoundIsValid || me.isValueInStore(value))) {
326 return gettext('Invalid Value');
327 }
328
329 return true;
330 },
331
066babdc
DM
332 initComponent: function() {
333 var me = this;
334
066babdc
DM
335 Ext.apply(me, {
336 queryMode: 'local',
337 matchFieldWidth: false
338 });
339
340 Ext.applyIf(me, { value: ''}); // hack: avoid ExtJS validate() bug
341
342 Ext.applyIf(me.listConfig, { width: 400 });
343
344 me.callParent();
345
346 // Create the picker at an early stage, so it is available to store the previous selection
347 if (!me.picker) {
348 me.createPicker();
349 }
350
39d99149
DC
351 if (me.editable) {
352 // The trigger.picker causes first a focus event on the field then
353 // toggles the selection picker. Thus skip expanding in this case,
354 // else our focus listner expands and the picker.trigger then
355 // collapses it directly afterwards.
356 Ext.override(me.triggers.picker, {
357 onMouseDown : function (e) {
358 // copied "should we focus" check from Ext.form.trigger.Trigger
359 if (e.pointerType !== 'touch' && !this.field.owns(Ext.Element.getActiveElement())) {
360 me.skip_expand_on_focus = true;
361 }
362 this.callParent(arguments);
363 }
364 });
365
366 me.on("focus", function(me) {
367 if (!me.isExpanded && !me.skip_expand_on_focus) {
368 me.expand();
369 }
370 me.skip_expand_on_focus = false;
371 });
372 }
373
066babdc
DM
374 me.mon(me.store, 'beforeload', function() {
375 if (!me.isDisabled()) {
376 me.enableLoadMask = true;
377 }
378 });
379
380 // hack: autoSelect does not work
381 me.mon(me.store, 'load', function(store, r, success, o) {
382 if (success) {
383 me.clearInvalid();
384
385 if (me.enableLoadMask) {
386 delete me.enableLoadMask;
387
388 // if the picker exists,
389 // we reset its minheight to the saved var/0
390 // we have to update the layout, otherwise the height
391 // gets not recalculated
392 if (me.picker) {
393 me.picker.setMinHeight(me.savedMinHeight || 0);
394 delete me.savedMinHeight;
395 me.picker.updateLayout();
396 }
397 }
398
399 var def = me.getValue() || me.preferredValue;
400 if (def) {
401 me.setValue(def, true); // sync with grid
402 }
403 var found = false;
404 if (def) {
1ccb53ec 405 found = me.isValueInStore(def);
066babdc
DM
406 }
407
408 if (!found) {
409 var rec = me.store.first();
410 if (me.autoSelect && rec && rec.data) {
411 def = rec.data[me.valueField];
412 me.setValue(def, true);
413 } else {
f59a7b23 414 me.setValue(def);
1ccb53ec
TL
415 if (!me.notFoundIsValid) {
416 me.markInvalid(gettext('Invalid Value'));
417 }
066babdc
DM
418 }
419 }
420 }
421 });
422 }
423});