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,
36 // needed to trigger onKeyUp etc.
37 enableKeyEvents
: true,
43 cls
: 'pmx-clear-trigger',
53 setValue: function(value
) {
55 me
.triggers
.clear
.setVisible(!!value
&& me
.allowBlank
);
56 return me
.callParent([value
]);
59 // override ExtJS method
60 // if the field has multiSelect enabled, the store is not loaded, and
61 // the displayfield == valuefield, it saves the rawvalue as an array
62 // but the getRawValue method is only defined in the textfield class
63 // (which has not to deal with arrays) an returns the string in the
64 // field (not an array)
66 // so if we have multiselect enabled, return the rawValue (which
67 // should be an array) and else we do callParent so
68 // it should not impact any other use of the class
69 getRawValue: function() {
74 return me
.callParent();
78 getSubmitData: function() {
82 if (!me
.disabled
&& me
.submitValue
) {
83 let val
= me
.getSubmitValue();
86 data
[me
.getName()] = val
;
87 } else if (me
.getDeleteEmpty()) {
89 data
['delete'] = me
.getName();
95 getSubmitValue: function() {
98 var value
= me
.callParent();
103 return me
.getSkipEmptyText() ? null: value
;
106 setAllowBlank: function(allowBlank
) {
107 this.allowBlank
= allowBlank
;
111 // override ExtJS protected method
112 onBindStore: function(store
, initial
) {
116 valueCollectionConfig
;
118 // We're being bound, not unbound...
120 // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
121 if (store
.autoCreated
) {
122 me
.queryMode
= 'local';
123 me
.valueField
= me
.displayField
= 'field1';
124 if (!store
.expanded
) {
125 me
.displayField
= 'field2';
128 // displayTpl config will need regenerating with the autogenerated displayField name 'field1'
129 me
.setDisplayTpl(null);
131 if (!Ext
.isDefined(me
.valueField
)) {
132 me
.valueField
= me
.displayField
;
135 // Add a byValue index to the store so that we can efficiently look up records by the value field
136 // when setValue passes string value(s).
137 // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
138 // are found, they are all returned by the get call.
139 // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
140 // if unique is true, CollectionKey keeps the *last* matching value.
143 rootProperty
: 'data',
147 extraKeySpec
.byValue
.property
= me
.valueField
;
148 store
.setExtraKeys(extraKeySpec
);
150 if (me
.displayField
=== me
.valueField
) {
151 store
.byText
= store
.byValue
;
153 extraKeySpec
.byText
= {
154 rootProperty
: 'data',
157 extraKeySpec
.byText
.property
= me
.displayField
;
158 store
.setExtraKeys(extraKeySpec
);
161 // We hold a collection of the values which have been selected, keyed by this field's valueField.
162 // This collection also functions as the selected items collection for the BoundList's selection model
163 valueCollectionConfig
= {
164 rootProperty
: 'data',
167 property
: 'internalId'
170 property
: me
.valueField
,
174 // Whenever this collection is changed by anyone, whether by this field adding to it,
175 // or the BoundList operating, we must refresh our value.
177 beginupdate
: me
.onValueCollectionBeginUpdate
,
178 endupdate
: me
.onValueCollectionEndUpdate
,
183 // This becomes our collection of selected records for the Field.
184 me
.valueCollection
= new Ext
.util
.Collection(valueCollectionConfig
);
186 // We use the selected Collection as our value collection and the basis
187 // for rendering the tag list.
189 //proxmox override: since the picker is represented by a grid panel,
190 // we changed here the selection to RowModel
191 me
.pickerSelectionModel
= new Ext
.selection
.RowModel({
192 mode
: me
.multiSelect
? 'SIMPLE' : 'SINGLE',
193 // There are situations when a row is selected on mousedown but then the mouse is dragged to another row
194 // and released. In these situations, the event target for the click event won't be the row where the mouse
195 // was released but the boundview. The view will then determine that it should fire a container click, and
196 // the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will
197 // prevent the model from deselecting.
198 deselectOnContainerClick
: false,
199 enableInitialSelection
: false,
201 selected
: me
.valueCollection
,
205 lastselectedchanged
: me
.updateBindSelection
214 picker
.setSelectionModel(me
.pickerSelectionModel
);
215 if (picker
.getStore() !== store
) {
216 picker
.bindStore(store
);
222 // copied from ComboBox
223 createPicker: function() {
227 var pickerCfg
= Ext
.apply({
228 // proxmox overrides: display a grid for selection
235 displayField
: me
.displayField
,
236 preserveScrollOnRefresh
: true,
237 pageSize
: me
.pageSize
,
239 selModel
: me
.pickerSelectionModel
,
240 focusOnToFront
: false
241 }, me
.listConfig
, me
.defaultListConfig
);
243 picker
= me
.picker
|| Ext
.widget(pickerCfg
);
245 if (picker
.getStore() !== me
.store
) {
246 picker
.bindStore(me
.store
);
250 picker
.pagingToolbar
.on('beforechange', me
.onPageChange
, me
);
253 // proxmox overrides: pass missing method in gridPanel to its view
254 picker
.refresh = function() {
255 picker
.getSelectionModel().select(me
.valueCollection
.getRange());
256 picker
.getView().refresh();
258 picker
.getNodeByRecord = function() {
259 picker
.getView().getNodeByRecord(arguments
);
262 // We limit the height of the picker to fit in the space above
263 // or below this field unless the picker has its own ideas about that.
264 if (!picker
.initialConfig
.maxHeight
) {
266 beforeshow
: me
.onBeforePickerShow
,
270 picker
.getSelectionModel().on({
271 beforeselect
: me
.onBeforeSelect
,
272 beforedeselect
: me
.onBeforeDeselect
,
273 focuschange
: me
.onFocusChange
,
274 selectionChange: function (sm
, selectedRecords
) {
276 if (selectedRecords
.length
) {
277 me
.setValue(selectedRecords
);
278 me
.fireEvent('select', me
, selectedRecords
);
285 // when the clicked item is the same as the previously selected,
286 // it does not select the item
287 // instead we hide the picker
288 if (!me
.multiSelect
) {
289 picker
.on('itemclick', function (sm
,record
) {
290 if (picker
.getSelection()[0] === record
) {
296 // when our store is not yet loaded, we increase
297 // the height of the gridpanel, so that we can see
300 // we save the minheight to reset it after the load
301 picker
.on('show', function() {
302 if (me
.enableLoadMask
) {
303 me
.savedMinHeight
= picker
.getMinHeight();
304 picker
.setMinHeight(100);
308 picker
.getNavigationModel().navigateOnSpace
= false;
313 isValueInStore: function(value
) {
315 var store
= me
.store
;
322 if (Ext
.isArray(value
)) {
323 Ext
.Array
.each(value
, function(v
) {
324 if (store
.findRecord(me
.valueField
, v
)) {
326 return false; // break
330 found
= !!store
.findRecord(me
.valueField
, value
);
336 validator: function (value
) {
340 return true; // handled later by allowEmpty in the getErrors call chain
343 if (!(me
.notFoundIsValid
|| me
.isValueInStore(value
))) {
344 return gettext('Invalid Value');
350 initComponent: function() {
355 matchFieldWidth
: false
358 Ext
.applyIf(me
, { value
: ''}); // hack: avoid ExtJS validate() bug
360 Ext
.applyIf(me
.listConfig
, { width
: 400 });
364 // Create the picker at an early stage, so it is available to store the previous selection
370 // The trigger.picker causes first a focus event on the field then
371 // toggles the selection picker. Thus skip expanding in this case,
372 // else our focus listner expands and the picker.trigger then
373 // collapses it directly afterwards.
374 Ext
.override(me
.triggers
.picker
, {
375 onMouseDown : function (e
) {
376 // copied "should we focus" check from Ext.form.trigger.Trigger
377 if (e
.pointerType
!== 'touch' && !this.field
.owns(Ext
.Element
.getActiveElement())) {
378 me
.skip_expand_on_focus
= true;
380 this.callParent(arguments
);
384 me
.on("focus", function(me
) {
385 if (!me
.isExpanded
&& !me
.skip_expand_on_focus
) {
388 me
.skip_expand_on_focus
= false;
392 me
.mon(me
.store
, 'beforeload', function() {
393 if (!me
.isDisabled()) {
394 me
.enableLoadMask
= true;
398 // hack: autoSelect does not work
399 me
.mon(me
.store
, 'load', function(store
, r
, success
, o
) {
403 if (me
.enableLoadMask
) {
404 delete me
.enableLoadMask
;
406 // if the picker exists,
407 // we reset its minheight to the saved var/0
408 // we have to update the layout, otherwise the height
409 // gets not recalculated
411 me
.picker
.setMinHeight(me
.savedMinHeight
|| 0);
412 delete me
.savedMinHeight
;
413 me
.picker
.updateLayout();
417 var def
= me
.getValue() || me
.preferredValue
;
419 me
.setValue(def
, true); // sync with grid
423 found
= me
.isValueInStore(def
);
427 var rec
= me
.store
.first();
428 if (me
.autoSelect
&& rec
&& rec
.data
) {
429 def
= rec
.data
[me
.valueField
];
430 me
.setValue(def
, true);
433 if (!me
.notFoundIsValid
) {
434 me
.markInvalid(gettext('Invalid Value'));