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