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