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