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