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
);
32 notFoundIsValid
: false,
35 // NOTE: the trigger will always be shown if allowBlank is true, setting showClearTrigger
36 // to false cannot change that
37 showClearTrigger
: false,
40 // needed to trigger onKeyUp etc.
41 enableKeyEvents
: true,
47 cls
: 'pmx-clear-trigger',
57 setValue: function(value
) {
59 let empty
= Ext
.isArray(value
) ? !value
.length
: !value
;
60 me
.triggers
.clear
.setVisible(!empty
&& (me
.allowBlank
|| me
.showClearTrigger
));
61 return me
.callParent([value
]);
64 // override ExtJS method
65 // if the field has multiSelect enabled, the store is not loaded, and
66 // the displayfield == valuefield, it saves the rawvalue as an array
67 // but the getRawValue method is only defined in the textfield class
68 // (which has not to deal with arrays) an returns the string in the
69 // field (not an array)
71 // so if we have multiselect enabled, return the rawValue (which
72 // should be an array) and else we do callParent so
73 // it should not impact any other use of the class
74 getRawValue: function() {
79 return me
.callParent();
83 getSubmitData: function() {
87 if (!me
.disabled
&& me
.submitValue
) {
88 let val
= me
.getSubmitValue();
91 data
[me
.getName()] = val
;
92 } else if (me
.getDeleteEmpty()) {
94 data
.delete = me
.getName();
100 getSubmitValue: function() {
103 let value
= me
.callParent();
108 return me
.getSkipEmptyText() ? null: value
;
111 setAllowBlank: function(allowBlank
) {
112 this.allowBlank
= allowBlank
;
116 // override ExtJS protected method
117 onBindStore: function(store
, initial
) {
121 valueCollectionConfig
;
123 // We're being bound, not unbound...
125 // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
126 if (store
.autoCreated
) {
127 me
.queryMode
= 'local';
128 me
.valueField
= me
.displayField
= 'field1';
129 if (!store
.expanded
) {
130 me
.displayField
= 'field2';
133 // displayTpl config will need regenerating with the autogenerated displayField name 'field1'
134 me
.setDisplayTpl(null);
136 if (!Ext
.isDefined(me
.valueField
)) {
137 me
.valueField
= me
.displayField
;
140 // Add a byValue index to the store so that we can efficiently look up records by the value field
141 // when setValue passes string value(s).
142 // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
143 // are found, they are all returned by the get call.
144 // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
145 // if unique is true, CollectionKey keeps the *last* matching value.
148 rootProperty
: 'data',
152 extraKeySpec
.byValue
.property
= me
.valueField
;
153 store
.setExtraKeys(extraKeySpec
);
155 if (me
.displayField
=== me
.valueField
) {
156 store
.byText
= store
.byValue
;
158 extraKeySpec
.byText
= {
159 rootProperty
: 'data',
162 extraKeySpec
.byText
.property
= me
.displayField
;
163 store
.setExtraKeys(extraKeySpec
);
166 // We hold a collection of the values which have been selected, keyed by this field's valueField.
167 // This collection also functions as the selected items collection for the BoundList's selection model
168 valueCollectionConfig
= {
169 rootProperty
: 'data',
172 property
: 'internalId',
175 property
: me
.valueField
,
176 rootProperty
: 'data',
179 // Whenever this collection is changed by anyone, whether by this field adding to it,
180 // or the BoundList operating, we must refresh our value.
182 beginupdate
: me
.onValueCollectionBeginUpdate
,
183 endupdate
: me
.onValueCollectionEndUpdate
,
188 // This becomes our collection of selected records for the Field.
189 me
.valueCollection
= new Ext
.util
.Collection(valueCollectionConfig
);
191 // We use the selected Collection as our value collection and the basis
192 // for rendering the tag list.
194 //proxmox override: since the picker is represented by a grid panel,
195 // we changed here the selection to RowModel
196 me
.pickerSelectionModel
= new Ext
.selection
.RowModel({
197 mode
: me
.multiSelect
? 'SIMPLE' : 'SINGLE',
198 // There are situations when a row is selected on mousedown but then the mouse is
199 // dragged to another row and released. In these situations, the event target for
200 // the click event won't be the row where the mouse was released but the boundview.
201 // The view will then determine that it should fire a container click, and the
202 // DataViewModel will then deselect all prior selections. Setting
203 // `deselectOnContainerClick` here will prevent the model from deselecting.
204 deselectOnContainerClick
: false,
205 enableInitialSelection
: false,
207 selected
: me
.valueCollection
,
211 lastselectedchanged
: me
.updateBindSelection
,
220 picker
.setSelectionModel(me
.pickerSelectionModel
);
221 if (picker
.getStore() !== store
) {
222 picker
.bindStore(store
);
228 // copied from ComboBox
229 createPicker: function() {
233 let pickerCfg
= Ext
.apply({
234 // proxmox overrides: display a grid for selection
241 displayField
: me
.displayField
,
242 preserveScrollOnRefresh
: true,
243 pageSize
: me
.pageSize
,
245 selModel
: me
.pickerSelectionModel
,
246 focusOnToFront
: false,
247 }, me
.listConfig
, me
.defaultListConfig
);
249 picker
= me
.picker
|| Ext
.widget(pickerCfg
);
251 if (picker
.getStore() !== me
.store
) {
252 picker
.bindStore(me
.store
);
256 picker
.pagingToolbar
.on('beforechange', me
.onPageChange
, me
);
259 // proxmox overrides: pass missing method in gridPanel to its view
260 picker
.refresh = function() {
261 picker
.getSelectionModel().select(me
.valueCollection
.getRange());
262 picker
.getView().refresh();
264 picker
.getNodeByRecord = function() {
265 picker
.getView().getNodeByRecord(arguments
);
268 // We limit the height of the picker to fit in the space above
269 // or below this field unless the picker has its own ideas about that.
270 if (!picker
.initialConfig
.maxHeight
) {
272 beforeshow
: me
.onBeforePickerShow
,
276 picker
.getSelectionModel().on({
277 beforeselect
: me
.onBeforeSelect
,
278 beforedeselect
: me
.onBeforeDeselect
,
279 focuschange
: me
.onFocusChange
,
280 selectionChange: function(sm
, selectedRecords
) {
281 if (selectedRecords
.length
) {
282 this.setValue(selectedRecords
);
283 this.fireEvent('select', me
, selectedRecords
);
290 // when the clicked item is the same as the previously selected,
291 // it does not select the item
292 // instead we hide the picker
293 if (!me
.multiSelect
) {
294 picker
.on('itemclick', function(sm
, record
) {
295 if (picker
.getSelection()[0] === record
) {
301 // when our store is not yet loaded, we increase
302 // the height of the gridpanel, so that we can see
305 // we save the minheight to reset it after the load
306 picker
.on('show', function() {
307 me
.store
.fireEvent('refresh');
308 if (me
.enableLoadMask
) {
309 me
.savedMinHeight
= me
.savedMinHeight
?? picker
.getMinHeight();
310 picker
.setMinHeight(me
.errorHeight
);
313 Proxmox
.Utils
.setErrorMask(picker
.getView(), me
.loadError
);
315 picker
.updateLayout();
319 picker
.getNavigationModel().navigateOnSpace
= false;
324 clearLocalFilter: function() {
327 if (me
.queryFilter
) {
328 me
.changingFilters
= true; // FIXME: unused?
329 me
.store
.removeFilter(me
.queryFilter
, true);
330 me
.queryFilter
= null;
331 me
.changingFilters
= false;
335 isValueInStore: function(value
) {
337 let store
= me
.store
;
344 // Make sure the current filter is removed before checking the store
345 // to prevent false negative results when iterating over a filtered store.
346 // All store.find*() method's operate on the filtered store.
347 if (me
.queryFilter
&& me
.queryMode
=== 'local' && me
.clearFilterOnBlur
) {
348 me
.clearLocalFilter();
351 if (Ext
.isArray(value
)) {
352 Ext
.Array
.each(value
, function(v
) {
353 if (store
.findRecord(me
.valueField
, v
, 0, false, true, true)) {
355 return false; // break
360 found
= !!store
.findRecord(me
.valueField
, value
, 0, false, true, true);
366 validator: function(value
) {
370 return true; // handled later by allowEmpty in the getErrors call chain
373 // we normally get here the displayField as value, but if a valueField
374 // is configured we need to get the "actual" value, to ensure it is in
375 // the store. Below check is copied from ExtJS 6.0.2 ComboBox source
377 // we also have to get the 'real' value if the we have a mulitSelect
378 // Field but got a non array value
379 if ((me
.valueField
&& me
.valueField
!== me
.displayField
) ||
380 (me
.multiSelect
&& !Ext
.isArray(value
))) {
381 value
= me
.getValue();
384 if (!(me
.notFoundIsValid
|| me
.isValueInStore(value
))) {
385 return gettext('Invalid Value');
391 // validate after enabling a field, otherwise blank fields with !allowBlank
392 // are sometimes not marked as invalid
393 setDisabled: function(value
) {
394 this.callParent([value
]);
398 initComponent: function() {
403 matchFieldWidth
: false,
406 Ext
.applyIf(me
, { value
: [] }); // hack: avoid ExtJS validate() bug
408 Ext
.applyIf(me
.listConfig
, { width
: 400 });
412 // Create the picker at an early stage, so it is available to store the previous selection
417 me
.mon(me
.store
, 'beforeload', function() {
418 if (!me
.isDisabled()) {
419 me
.enableLoadMask
= true;
423 // hack: autoSelect does not work
424 me
.mon(me
.store
, 'load', function(store
, r
, success
, o
) {
429 if (me
.enableLoadMask
) {
430 delete me
.enableLoadMask
;
432 // if the picker exists, we reset its minHeight to the previous saved one or 0
434 me
.picker
.setMinHeight(me
.savedMinHeight
|| 0);
435 Proxmox
.Utils
.setErrorMask(me
.picker
.getView());
436 delete me
.savedMinHeight
;
437 // we have to update the layout, otherwise the height gets not recalculated
438 me
.picker
.updateLayout();
442 let def
= me
.getValue() || me
.preferredValue
;
444 me
.setValue(def
, true); // sync with grid
448 found
= me
.isValueInStore(def
);
452 if (!(Ext
.isArray(def
) ? def
.length
: def
)) {
453 let rec
= me
.store
.first();
454 if (me
.autoSelect
&& rec
&& rec
.data
) {
455 def
= rec
.data
[me
.valueField
];
456 me
.setValue(def
, true);
457 } else if (!me
.allowBlank
) {
459 if (!me
.isDisabled()) {
460 me
.markInvalid(me
.blankText
);
463 } else if (!me
.notFoundIsValid
&& !me
.isDisabled()) {
464 me
.markInvalid(gettext('Invalid Value'));
468 let msg
= Proxmox
.Utils
.getResponseErrorMessage(o
.getError());
470 me
.savedMinHeight
= me
.savedMinHeight
?? me
.picker
.getMinHeight();
471 me
.picker
.setMinHeight(me
.errorHeight
);
472 Proxmox
.Utils
.setErrorMask(me
.picker
.getView(), msg
);
473 me
.picker
.updateLayout();