mirror of
https://git.proxmox.com/git/proxmox-widget-toolkit
synced 2025-05-05 08:53:06 +00:00

when a combobox was editable, a click inside did not open the picker, but it would if the combobox was not editable. Since this is 1. Inconsistent 2. Inconvenient (the user has to specifically press the arrow) we already had this implemented for our ComboGrid, but not for regular comboboxes This patch moves the code for this to an override for ComboBox, which our ComboGrid then inherits (so we do not need it there anymore) while at it, do some non-significant code-cleanup * whitespace fixes * don't shadow 'me' in the focus callback * fix typo in comment Originally this was implemented in pve-manager, commit 851c032d69ad5ae23725dd1add9e4084ebc12650 https://git.proxmox.com/?p=pve-manager.git;a=commitdiff;h=851c032d69ad5ae23725dd1add9e4084ebc12650 Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> Originally-by: Thomas Lamprecht <t.lamprecht@proxmox.com> Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
457 lines
14 KiB
JavaScript
457 lines
14 KiB
JavaScript
/*
|
|
* ComboGrid component: a ComboBox where the dropdown menu (the
|
|
* "Picker") is a Grid with Rows and Columns expects a listConfig
|
|
* object with a columns property roughly based on the GridPicker from
|
|
* https://www.sencha.com/forum/showthread.php?299909
|
|
*
|
|
*/
|
|
|
|
Ext.define('Proxmox.form.ComboGrid', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: ['widget.proxmoxComboGrid'],
|
|
|
|
// this value is used as default value after load()
|
|
preferredValue: undefined,
|
|
|
|
// hack: allow to select empty value
|
|
// seems extjs does not allow that when 'editable == false'
|
|
onKeyUp: function(e, t) {
|
|
var me = this;
|
|
var key = e.getKey();
|
|
|
|
if (!me.editable && me.allowBlank && !me.multiSelect &&
|
|
(key == e.BACKSPACE || key == e.DELETE)) {
|
|
me.setValue('');
|
|
}
|
|
|
|
me.callParent(arguments);
|
|
},
|
|
|
|
config: {
|
|
skipEmptyText: false,
|
|
notFoundIsValid: false,
|
|
deleteEmpty: false,
|
|
},
|
|
|
|
// needed to trigger onKeyUp etc.
|
|
enableKeyEvents: true,
|
|
|
|
editable: false,
|
|
|
|
triggers: {
|
|
clear: {
|
|
cls: 'pmx-clear-trigger',
|
|
weight: -1,
|
|
hidden: true,
|
|
handler: function() {
|
|
var me = this;
|
|
me.setValue('');
|
|
}
|
|
}
|
|
},
|
|
|
|
setValue: function(value) {
|
|
var me = this;
|
|
let empty = Ext.isArray(value) ? !value.length : !value;
|
|
me.triggers.clear.setVisible(!empty && me.allowBlank);
|
|
return me.callParent([value]);
|
|
},
|
|
|
|
// override ExtJS method
|
|
// if the field has multiSelect enabled, the store is not loaded, and
|
|
// the displayfield == valuefield, it saves the rawvalue as an array
|
|
// but the getRawValue method is only defined in the textfield class
|
|
// (which has not to deal with arrays) an returns the string in the
|
|
// field (not an array)
|
|
//
|
|
// so if we have multiselect enabled, return the rawValue (which
|
|
// should be an array) and else we do callParent so
|
|
// it should not impact any other use of the class
|
|
getRawValue: function() {
|
|
var me = this;
|
|
if (me.multiSelect) {
|
|
return me.rawValue;
|
|
} else {
|
|
return me.callParent();
|
|
}
|
|
},
|
|
|
|
getSubmitData: function() {
|
|
var me = this;
|
|
|
|
let data = null;
|
|
if (!me.disabled && me.submitValue) {
|
|
let val = me.getSubmitValue();
|
|
if (val !== null) {
|
|
data = {};
|
|
data[me.getName()] = val;
|
|
} else if (me.getDeleteEmpty()) {
|
|
data = {};
|
|
data['delete'] = me.getName();
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
|
|
getSubmitValue: function() {
|
|
var me = this;
|
|
|
|
var value = me.callParent();
|
|
if (value !== '') {
|
|
return value;
|
|
}
|
|
|
|
return me.getSkipEmptyText() ? null: value;
|
|
},
|
|
|
|
setAllowBlank: function(allowBlank) {
|
|
this.allowBlank = allowBlank;
|
|
this.validate();
|
|
},
|
|
|
|
// override ExtJS protected method
|
|
onBindStore: function(store, initial) {
|
|
var me = this,
|
|
picker = me.picker,
|
|
extraKeySpec,
|
|
valueCollectionConfig;
|
|
|
|
// We're being bound, not unbound...
|
|
if (store) {
|
|
// If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
|
|
if (store.autoCreated) {
|
|
me.queryMode = 'local';
|
|
me.valueField = me.displayField = 'field1';
|
|
if (!store.expanded) {
|
|
me.displayField = 'field2';
|
|
}
|
|
|
|
// displayTpl config will need regenerating with the autogenerated displayField name 'field1'
|
|
me.setDisplayTpl(null);
|
|
}
|
|
if (!Ext.isDefined(me.valueField)) {
|
|
me.valueField = me.displayField;
|
|
}
|
|
|
|
// Add a byValue index to the store so that we can efficiently look up records by the value field
|
|
// when setValue passes string value(s).
|
|
// The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
|
|
// are found, they are all returned by the get call.
|
|
// This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
|
|
// if unique is true, CollectionKey keeps the *last* matching value.
|
|
extraKeySpec = {
|
|
byValue: {
|
|
rootProperty: 'data',
|
|
unique: false
|
|
}
|
|
};
|
|
extraKeySpec.byValue.property = me.valueField;
|
|
store.setExtraKeys(extraKeySpec);
|
|
|
|
if (me.displayField === me.valueField) {
|
|
store.byText = store.byValue;
|
|
} else {
|
|
extraKeySpec.byText = {
|
|
rootProperty: 'data',
|
|
unique: false
|
|
};
|
|
extraKeySpec.byText.property = me.displayField;
|
|
store.setExtraKeys(extraKeySpec);
|
|
}
|
|
|
|
// We hold a collection of the values which have been selected, keyed by this field's valueField.
|
|
// This collection also functions as the selected items collection for the BoundList's selection model
|
|
valueCollectionConfig = {
|
|
rootProperty: 'data',
|
|
extraKeys: {
|
|
byInternalId: {
|
|
property: 'internalId'
|
|
},
|
|
byValue: {
|
|
property: me.valueField,
|
|
rootProperty: 'data'
|
|
}
|
|
},
|
|
// Whenever this collection is changed by anyone, whether by this field adding to it,
|
|
// or the BoundList operating, we must refresh our value.
|
|
listeners: {
|
|
beginupdate: me.onValueCollectionBeginUpdate,
|
|
endupdate: me.onValueCollectionEndUpdate,
|
|
scope: me
|
|
}
|
|
};
|
|
|
|
// This becomes our collection of selected records for the Field.
|
|
me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
|
|
|
|
// We use the selected Collection as our value collection and the basis
|
|
// for rendering the tag list.
|
|
|
|
//proxmox override: since the picker is represented by a grid panel,
|
|
// we changed here the selection to RowModel
|
|
me.pickerSelectionModel = new Ext.selection.RowModel({
|
|
mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
|
|
// There are situations when a row is selected on mousedown but then the mouse is dragged to another row
|
|
// and released. In these situations, the event target for the click event won't be the row where the mouse
|
|
// was released but the boundview. The view will then determine that it should fire a container click, and
|
|
// the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will
|
|
// prevent the model from deselecting.
|
|
deselectOnContainerClick: false,
|
|
enableInitialSelection: false,
|
|
pruneRemoved: false,
|
|
selected: me.valueCollection,
|
|
store: store,
|
|
listeners: {
|
|
scope: me,
|
|
lastselectedchanged: me.updateBindSelection
|
|
}
|
|
});
|
|
|
|
if (!initial) {
|
|
me.resetToDefault();
|
|
}
|
|
|
|
if (picker) {
|
|
picker.setSelectionModel(me.pickerSelectionModel);
|
|
if (picker.getStore() !== store) {
|
|
picker.bindStore(store);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// copied from ComboBox
|
|
createPicker: function() {
|
|
var me = this;
|
|
var picker;
|
|
|
|
var pickerCfg = Ext.apply({
|
|
// proxmox overrides: display a grid for selection
|
|
xtype: 'gridpanel',
|
|
id: me.pickerId,
|
|
pickerField: me,
|
|
floating: true,
|
|
hidden: true,
|
|
store: me.store,
|
|
displayField: me.displayField,
|
|
preserveScrollOnRefresh: true,
|
|
pageSize: me.pageSize,
|
|
tpl: me.tpl,
|
|
selModel: me.pickerSelectionModel,
|
|
focusOnToFront: false
|
|
}, me.listConfig, me.defaultListConfig);
|
|
|
|
picker = me.picker || Ext.widget(pickerCfg);
|
|
|
|
if (picker.getStore() !== me.store) {
|
|
picker.bindStore(me.store);
|
|
}
|
|
|
|
if (me.pageSize) {
|
|
picker.pagingToolbar.on('beforechange', me.onPageChange, me);
|
|
}
|
|
|
|
// proxmox overrides: pass missing method in gridPanel to its view
|
|
picker.refresh = function() {
|
|
picker.getSelectionModel().select(me.valueCollection.getRange());
|
|
picker.getView().refresh();
|
|
};
|
|
picker.getNodeByRecord = function() {
|
|
picker.getView().getNodeByRecord(arguments);
|
|
};
|
|
|
|
// We limit the height of the picker to fit in the space above
|
|
// or below this field unless the picker has its own ideas about that.
|
|
if (!picker.initialConfig.maxHeight) {
|
|
picker.on({
|
|
beforeshow: me.onBeforePickerShow,
|
|
scope: me
|
|
});
|
|
}
|
|
picker.getSelectionModel().on({
|
|
beforeselect: me.onBeforeSelect,
|
|
beforedeselect: me.onBeforeDeselect,
|
|
focuschange: me.onFocusChange,
|
|
selectionChange: function (sm, selectedRecords) {
|
|
var me = this;
|
|
if (selectedRecords.length) {
|
|
me.setValue(selectedRecords);
|
|
me.fireEvent('select', me, selectedRecords);
|
|
}
|
|
},
|
|
scope: me
|
|
});
|
|
|
|
// hack for extjs6
|
|
// when the clicked item is the same as the previously selected,
|
|
// it does not select the item
|
|
// instead we hide the picker
|
|
if (!me.multiSelect) {
|
|
picker.on('itemclick', function (sm,record) {
|
|
if (picker.getSelection()[0] === record) {
|
|
picker.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
// when our store is not yet loaded, we increase
|
|
// the height of the gridpanel, so that we can see
|
|
// the loading mask
|
|
//
|
|
// we save the minheight to reset it after the load
|
|
picker.on('show', function() {
|
|
if (me.enableLoadMask) {
|
|
me.savedMinHeight = picker.getMinHeight();
|
|
picker.setMinHeight(100);
|
|
}
|
|
});
|
|
|
|
picker.getNavigationModel().navigateOnSpace = false;
|
|
|
|
return picker;
|
|
},
|
|
|
|
clearLocalFilter: function() {
|
|
var me = this,
|
|
filter = me.queryFilter;
|
|
|
|
if (filter) {
|
|
me.queryFilter = null;
|
|
me.changingFilters = true;
|
|
me.store.removeFilter(filter, true);
|
|
me.changingFilters = false;
|
|
}
|
|
},
|
|
|
|
isValueInStore: function(value) {
|
|
var me = this;
|
|
var store = me.store;
|
|
var found = false;
|
|
|
|
if (!store) {
|
|
return found;
|
|
}
|
|
|
|
// Make sure the current filter is removed before checking the store
|
|
// to prevent false negative results when iterating over a filtered store.
|
|
// All store.find*() method's operate on the filtered store.
|
|
if (me.queryFilter && me.queryMode === 'local' && me.clearFilterOnBlur) {
|
|
me.clearLocalFilter();
|
|
}
|
|
|
|
if (Ext.isArray(value)) {
|
|
Ext.Array.each(value, function(v) {
|
|
if (store.findRecord(me.valueField, v)) {
|
|
found = true;
|
|
return false; // break
|
|
}
|
|
});
|
|
} else {
|
|
found = !!store.findRecord(me.valueField, value);
|
|
}
|
|
|
|
return found;
|
|
},
|
|
|
|
validator: function (value) {
|
|
var me = this;
|
|
|
|
if (!value) {
|
|
return true; // handled later by allowEmpty in the getErrors call chain
|
|
}
|
|
|
|
// we normally get here the displayField as value, but if a valueField
|
|
// is configured we need to get the "actual" value, to ensure it is in
|
|
// the store. Below check is copied from ExtJS 6.0.2 ComboBox source
|
|
//
|
|
// we also have to get the 'real' value if the we have a mulitSelect
|
|
// Field but got a non array value
|
|
if ((me.valueField && me.valueField !== me.displayField) ||
|
|
(me.multiSelect && !Ext.isArray(value))) {
|
|
value = me.getValue();
|
|
}
|
|
|
|
if (!(me.notFoundIsValid || me.isValueInStore(value))) {
|
|
return gettext('Invalid Value');
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
// validate after enabling a field, otherwise blank fields with !allowBlank
|
|
// are sometimes not marked as invalid
|
|
setDisabled: function(value) {
|
|
this.callParent([value]);
|
|
this.validate();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
Ext.apply(me, {
|
|
queryMode: 'local',
|
|
matchFieldWidth: false
|
|
});
|
|
|
|
Ext.applyIf(me, { value: ''}); // hack: avoid ExtJS validate() bug
|
|
|
|
Ext.applyIf(me.listConfig, { width: 400 });
|
|
|
|
me.callParent();
|
|
|
|
// Create the picker at an early stage, so it is available to store the previous selection
|
|
if (!me.picker) {
|
|
me.createPicker();
|
|
}
|
|
|
|
me.mon(me.store, 'beforeload', function() {
|
|
if (!me.isDisabled()) {
|
|
me.enableLoadMask = true;
|
|
}
|
|
});
|
|
|
|
// hack: autoSelect does not work
|
|
me.mon(me.store, 'load', function(store, r, success, o) {
|
|
if (success) {
|
|
me.clearInvalid();
|
|
|
|
if (me.enableLoadMask) {
|
|
delete me.enableLoadMask;
|
|
|
|
// if the picker exists,
|
|
// we reset its minheight to the saved var/0
|
|
// we have to update the layout, otherwise the height
|
|
// gets not recalculated
|
|
if (me.picker) {
|
|
me.picker.setMinHeight(me.savedMinHeight || 0);
|
|
delete me.savedMinHeight;
|
|
me.picker.updateLayout();
|
|
}
|
|
}
|
|
|
|
var def = me.getValue() || me.preferredValue;
|
|
if (def) {
|
|
me.setValue(def, true); // sync with grid
|
|
}
|
|
var found = false;
|
|
if (def) {
|
|
found = me.isValueInStore(def);
|
|
}
|
|
|
|
if (!found) {
|
|
var rec = me.store.first();
|
|
if (me.autoSelect && rec && rec.data) {
|
|
def = rec.data[me.valueField];
|
|
me.setValue(def, true);
|
|
} else if (!me.allowBlank && !(Ext.isArray(def) ? def.length : def)) {
|
|
me.setValue(def);
|
|
if (!me.notFoundIsValid && !me.isDisabled()) {
|
|
me.markInvalid(me.blankText);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|