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
9 Ext
.define('Proxmox.form.ComboGrid', {
10 extend
: 'Ext.form.field.ComboBox',
11 alias
: ['widget.proxmoxComboGrid'],
13 // this value is used as default value after load()
14 preferredValue
: undefined,
16 // hack: allow to select empty value
17 // seems extjs does not allow that when 'editable == false'
18 onKeyUp: function(e
, t
) {
22 if (!me
.editable
&& me
.allowBlank
&& !me
.multiSelect
&&
23 (key
== e
.BACKSPACE
|| key
== e
.DELETE
)) {
27 me
.callParent(arguments
);
35 // needed to trigger onKeyUp etc.
36 enableKeyEvents
: true,
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)
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() {
55 return me
.callParent();
59 getSubmitData: function() {
63 if (!me
.disabled
&& me
.submitValue
) {
64 let val
= me
.getSubmitValue();
67 data
[me
.getName()] = val
;
68 } else if (me
.getDeleteEmpty()) {
70 data
['delete'] = me
.getName();
76 getSubmitValue: function() {
79 var value
= me
.callParent();
84 return me
.getSkipEmptyText() ? null: value
;
87 setAllowBlank: function(allowBlank
) {
88 this.allowBlank
= allowBlank
;
92 // override ExtJS protected method
93 onBindStore: function(store
, initial
) {
97 valueCollectionConfig
;
99 // We're being bound, not unbound...
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';
109 // displayTpl config will need regenerating with the autogenerated displayField name 'field1'
110 me
.setDisplayTpl(null);
112 if (!Ext
.isDefined(me
.valueField
)) {
113 me
.valueField
= me
.displayField
;
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.
124 rootProperty
: 'data',
128 extraKeySpec
.byValue
.property
= me
.valueField
;
129 store
.setExtraKeys(extraKeySpec
);
131 if (me
.displayField
=== me
.valueField
) {
132 store
.byText
= store
.byValue
;
134 extraKeySpec
.byText
= {
135 rootProperty
: 'data',
138 extraKeySpec
.byText
.property
= me
.displayField
;
139 store
.setExtraKeys(extraKeySpec
);
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',
148 property
: 'internalId'
151 property
: me
.valueField
,
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.
158 beginupdate
: me
.onValueCollectionBeginUpdate
,
159 endupdate
: me
.onValueCollectionEndUpdate
,
164 // This becomes our collection of selected records for the Field.
165 me
.valueCollection
= new Ext
.util
.Collection(valueCollectionConfig
);
167 // We use the selected Collection as our value collection and the basis
168 // for rendering the tag list.
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,
182 selected
: me
.valueCollection
,
186 lastselectedchanged
: me
.updateBindSelection
195 picker
.setSelectionModel(me
.pickerSelectionModel
);
196 if (picker
.getStore() !== store
) {
197 picker
.bindStore(store
);
203 // copied from ComboBox
204 createPicker: function() {
208 var pickerCfg
= Ext
.apply({
209 // proxmox overrides: display a grid for selection
216 displayField
: me
.displayField
,
217 preserveScrollOnRefresh
: true,
218 pageSize
: me
.pageSize
,
220 selModel
: me
.pickerSelectionModel
,
221 focusOnToFront
: false
222 }, me
.listConfig
, me
.defaultListConfig
);
224 picker
= me
.picker
|| Ext
.widget(pickerCfg
);
226 if (picker
.getStore() !== me
.store
) {
227 picker
.bindStore(me
.store
);
231 picker
.pagingToolbar
.on('beforechange', me
.onPageChange
, me
);
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();
239 picker
.getNodeByRecord = function() {
240 picker
.getView().getNodeByRecord(arguments
);
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
) {
247 beforeshow
: me
.onBeforePickerShow
,
251 picker
.getSelectionModel().on({
252 beforeselect
: me
.onBeforeSelect
,
253 beforedeselect
: me
.onBeforeDeselect
,
254 focuschange
: me
.onFocusChange
,
255 selectionChange: function (sm
, selectedRecords
) {
257 if (selectedRecords
.length
) {
258 me
.setValue(selectedRecords
);
259 me
.fireEvent('select', me
, selectedRecords
);
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
) {
277 // when our store is not yet loaded, we increase
278 // the height of the gridpanel, so that we can see
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);
289 picker
.getNavigationModel().navigateOnSpace
= false;
294 initComponent: function() {
299 matchFieldWidth
: false
302 Ext
.applyIf(me
, { value
: ''}); // hack: avoid ExtJS validate() bug
304 Ext
.applyIf(me
.listConfig
, { width
: 400 });
308 // Create the picker at an early stage, so it is available to store the previous selection
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;
324 this.callParent(arguments
);
328 me
.on("focus", function(me
) {
329 if (!me
.isExpanded
&& !me
.skip_expand_on_focus
) {
332 me
.skip_expand_on_focus
= false;
336 me
.mon(me
.store
, 'beforeload', function() {
337 if (!me
.isDisabled()) {
338 me
.enableLoadMask
= true;
342 // hack: autoSelect does not work
343 me
.mon(me
.store
, 'load', function(store
, r
, success
, o
) {
347 if (me
.enableLoadMask
) {
348 delete me
.enableLoadMask
;
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
355 me
.picker
.setMinHeight(me
.savedMinHeight
|| 0);
356 delete me
.savedMinHeight
;
357 me
.picker
.updateLayout();
361 var def
= me
.getValue() || me
.preferredValue
;
363 me
.setValue(def
, true); // sync with grid
367 if (Ext
.isArray(def
)) {
368 Ext
.Array
.each(def
, function(v
) {
369 if (store
.findRecord(me
.valueField
, v
)) {
371 return false; // break
375 found
= store
.findRecord(me
.valueField
, def
);
380 var rec
= me
.store
.first();
381 if (me
.autoSelect
&& rec
&& rec
.data
) {
382 def
= rec
.data
[me
.valueField
];
383 me
.setValue(def
, true);