/**
* This component provides a grid holding selected items from a second store of potential
* members. The `store` of this component represents the selected items. The "search store"
* represents the potentially selected items.
*
* While this component is a grid and so you can configure `columns`, it is best to leave
* that to this class in its `initComponent` method. That allows this class to create the
* extra column that allows the user to remove rows. Instead use `{@link #fieldName}` and
* `{@link #fieldTitle}` to configure the primary column's `dataIndex` and column `text`,
* respectively.
*
* @since 5.0.0
*/
Ext.define('Ext.view.MultiSelector', {
extend: 'Ext.grid.Panel',
xtype: 'multiselector',
config: {
/**
* @cfg {Object} search
* This object configures the search popup component. By default this contains the
* `xtype` for a `Ext.view.MultiSelectorSearch` component and specifies `autoLoad`
* for its `store`.
*/
search: {
xtype: 'multiselector-search',
width: 200,
height: 200,
store: {
autoLoad: true
}
}
},
/**
* @cfg {String} [fieldName="name"]
* The name of the data field to display in the primary column of the grid.
* @since 5.0.0
*/
fieldName: 'name',
/**
* @cfg {String} [fieldTitle]
* The text to display in the column header for the primary column of the grid.
* @since 5.0.0
*/
fieldTitle: null,
/**
* @cfg {String} removeRowText
* The text to display in the "remove this row" column. By default this is a Unicode
* "X" looking glyph.
* @since 5.0.0
*/
removeRowText: '\u2716',
/**
* @cfg {String} removeRowTip
* The tooltip to display when the user hovers over the remove cell.
* @since 5.0.0
*/
removeRowTip: 'Remove this item',
emptyText: 'Nothing selected',
/**
* @cfg {String} addToolText
* The tooltip to display when the user hovers over the "+" tool in the panel header.
* @since 5.0.0
*/
addToolText: 'Search for items to add',
initComponent: function() {
var me = this,
emptyText = me.emptyText,
store = me.getStore(),
search = me.getSearch(),
fieldTitle = me.fieldTitle,
searchStore, model;
//<debug>
if (!search) {
Ext.raise('The search configuration is required for the multi selector');
}
//</debug>
searchStore = search.store;
if (searchStore.isStore) {
model = searchStore.getModel();
}
else {
model = searchStore.model;
}
if (!store) {
me.store = {
model: model
};
}
if (emptyText && !me.viewConfig) {
me.viewConfig = {
deferEmptyText: false,
emptyText: emptyText
};
}
if (!me.columns) {
me.hideHeaders = !fieldTitle;
me.columns = [
{ text: fieldTitle, dataIndex: me.fieldName, flex: 1 },
me.makeRemoveRowColumn()
];
}
me.callParent();
},
addTools: function() {
var me = this;
me.addTool({
type: 'plus',
tooltip: me.addToolText,
callback: 'onShowSearch',
scope: me
});
me.searchTool = me.tools[me.tools.length - 1];
},
convertSearchRecord: Ext.identityFn,
convertSelectionRecord: Ext.identityFn,
makeRemoveRowColumn: function() {
var me = this;
return {
width: 32,
align: 'center',
menuDisabled: true,
tdCls: Ext.baseCSSPrefix + 'multiselector-remove',
processEvent: me.processRowEvent.bind(me),
renderer: me.renderRemoveRow,
updater: Ext.emptyFn,
scope: me
};
},
processRowEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
var body = Ext.getBody();
if (e.type === 'click' ||
(e.type === 'keydown' && (e.keyCode === e.SPACE || e.keyCode === e.ENTER))) {
// Deleting the focused row will momentarily focusLeave
// That would dismiss the popup, so disable that.
body.suspendFocusEvents();
this.store.remove(record);
body.resumeFocusEvents();
if (this.searchPopup) {
this.searchPopup.deselectRecords(record);
}
}
},
renderRemoveRow: function() {
return '<span data-qtip="' + this.removeRowTip + '" role="button" tabIndex="0">' +
this.removeRowText + '</span>';
},
onFocusLeave: function(e) {
this.onDismissSearch();
this.callParent([e]);
},
afterComponentLayout: function(width, height, prevWidth, prevHeight) {
var me = this,
popup = me.searchPopup;
me.callParent([width, height, prevWidth, prevHeight]);
if (popup && popup.isVisible()) {
popup.showBy(me, me.popupAlign);
}
},
privates: {
popupAlign: 'tl-tr?',
onGlobalScroll: function(scroller) {
// Collapse if the scroll is anywhere but inside this selector or the popup
if (!this.owns(scroller.getElement())) {
this.onDismissSearch();
}
},
onDismissSearch: function(e) {
var searchPopup = this.searchPopup;
if (searchPopup &&
(!e || !(searchPopup.owns(e.getTarget()) || this.owns(e.getTarget())))) {
this.scrollListeners.destroy();
this.touchListeners.destroy();
searchPopup.hide();
}
},
onShowSearch: function(panel, tool, event) {
var me = this,
searchPopup = me.searchPopup,
store = me.getStore();
if (!searchPopup) {
searchPopup = Ext.merge({
owner: me,
field: me.fieldName,
floating: true,
alignOnScroll: false
}, me.getSearch());
me.searchPopup = searchPopup = me.add(searchPopup);
// If we were configured with records prior to the UI requesting the popup,
// ensure that the records are selected in the popup.
if (store.getCount()) {
searchPopup.selectRecords(store.getRange());
}
}
searchPopup.invocationEvent = event;
searchPopup.showBy(me, me.popupAlign);
// It only autofocuses its defaultFocus target if it was hidden.
// If they're reactivating the show tool, they'll expect to focus the search.
if (!event || event.pointerType !== 'touch') {
searchPopup.lookupReference('searchField').focus();
}
me.scrollListeners = Ext.on({
scroll: 'onGlobalScroll',
scope: me,
destroyable: true
});
// Dismiss on touch outside this component tree.
// Because touch platforms do not focus document.body on touch
// so no focusleave would occur to trigger a collapse.
me.touchListeners = Ext.getDoc().on({
// Do not translate on non-touch platforms.
// mousedown will blur the field.
translate: false,
touchstart: me.onDismissSearch,
scope: me,
delegated: false,
destroyable: true
});
}
}
});