pve-manager/www/manager6/form/ComboGrid.js
Dominik Csapak f2a6ce6cf1 ext6migrate: fix store load race condition
when we click on a node/container/vm and quickly
click on something else, there might be a race condition,
where the store finished loading and we try to change
dom elements, which are not there anymore

so we change the store.on to me.mon, which
deletes the handler when the component is gone

in the logview panel we use API2Request, where
we cannot do this, so we check if the component
is destroyed manually

also we change this whenever we change a dom element
in a callback

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2016-03-14 17:16:37 +01:00

291 lines
9.6 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('PVE.form.ComboGrid', {
extend: 'Ext.form.field.ComboBox',
alias: ['widget.PVE.form.ComboGrid'],
// 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);
},
// 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.
//pve 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({
// pve 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(store);
}
if (me.pageSize) {
picker.pagingToolbar.on('beforechange', me.onPageChange, me);
}
// pve 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();
}
});
}
picker.getNavigationModel().navigateOnSpace = false;
return picker;
},
initComponent: function() {
var me = this;
if (me.initialConfig.editable === undefined) {
me.editable = false;
}
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.setDisabled(true);
me.enableAfterLoad = true;
}
});
// hack: autoSelect does not work
me.mon(me.store, 'load', function(store, r, success, o) {
if (success) {
me.clearInvalid();
if (me.enableAfterLoad) {
delete me.enableAfterLoad;
me.setDisabled(false);
}
var def = me.getValue() || me.preferredValue;
if (def) {
me.setValue(def, true); // sync with grid
}
var found = false;
if (def) {
if (Ext.isArray(def)) {
Ext.Array.each(def, function(v) {
if (store.findRecord(me.valueField, v)) {
found = true;
return false; // break
}
});
} else {
found = store.findRecord(me.valueField, def);
}
}
if (!found) {
var rec = me.store.first();
if (me.autoSelect && rec && rec.data) {
def = rec.data[me.valueField];
me.setValue(def, true);
} else {
me.setValue(me.editable ? def : '', true);
}
}
}
});
}
});