/** * 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 `searchStore` * represents the potentially selected items. * * The default view defined by this class is intended to be easily replaced by deriving a * new class and overriding the appropriate methods. For example, the following is a very * different view that uses a date range and a data view: * * Ext.define('App.view.DateBoundSearch', { * extend: 'Ext.view.MultiSelectorSearch', * * makeDockedItems: function () { * return { * xtype: 'toolbar', * items: [{ * xtype: 'datefield', * emptyText: 'Start date...', * flex: 1 * },{ * xtype: 'datefield', * emptyText: 'End date...', * flex: 1 * }] * }; * }, * * makeItems: function () { * return [{ * xtype: 'dataview', * itemSelector: '.search-item', * selModel: 'rowselection', * store: this.store, * scrollable: true, * tpl: * '<tpl for=".">' + * '<div class="search-item">' + * '<img src="{icon}">' + * '<div>{name}</div>' + * '</div>' + * '</tpl>' * }]; * }, * * getSearchStore: function () { * return this.items.getAt(0).getStore(); * }, * * selectRecords: function (records) { * var view = this.items.getAt(0); * return view.getSelectionModel().select(records); * } * }); * * **Important**: This class assumes there are two components with specific `reference` * names assigned to them. These are `"searchField"` and `"searchGrid"`. These components * are produced by the `makeDockedItems` and `makeItems` method, respectively. When * overriding these it is important to remember to place these `reference` values on the * appropriate components. */Ext.define('Ext.view.MultiSelectorSearch', { extend: 'Ext.panel.Panel', xtype: 'multiselector-search', /** * @cfg layout * @inheritdoc */ layout: 'fit', /** * @cfg floating * @inheritdoc */ floating: true, /** * @cfg alignOnScroll * @inheritdoc */ alignOnScroll: false, /** * @cfg minWidth * @inheritdoc */ minWidth: 200, /** * @cfg minHeight * @inheritdoc */ minHeight: 200, /** * @cfg border * @inheritdoc */ border: true, /** * @cfg keyMap * @inheritdoc */ keyMap: { scope: 'this', ESC: 'hide' }, platformConfig: { desktop: { resizable: true }, 'tablet && rtl': { resizable: { handles: 'sw' } }, 'tablet && !rtl': { resizable: { handles: 'se' } } }, /** * @cfg defaultListenerScope * @inheritdoc */ defaultListenerScope: true, /** * @cfg referenceHolder * @inheritdoc */ referenceHolder: true, /** * @cfg {String} field * A field from your grid's store that will be used for filtering your search results. */ /** * @cfg store * @inheritdoc Ext.panel.Table#cfg-store */ /** * @cfg {String} searchText * This text is displayed as the "emptyText" of the search `textfield`. */ searchText: 'Search...', initComponent: function() { var me = this, owner = me.owner, items = me.makeItems(), i, item, records, store; me.dockedItems = me.makeDockedItems(); me.items = items; store = Ext.data.StoreManager.lookup(me.store); for (i = items.length; i--;) { if ((item = items[i]).xtype === 'grid') { item.store = store; item.isSearchGrid = true; item.selModel = item.selModel || { type: 'checkboxmodel', pruneRemoved: false, listeners: { selectionchange: 'onSelectionChange' } }; Ext.merge(item, me.grid); if (!item.columns) { item.hideHeaders = true; item.columns = [{ flex: 1, dataIndex: me.field }]; } break; } } me.callParent(); records = me.getOwnerStore().getRange(); if (!owner.convertSelectionRecord.$nullFn) { for (i = records.length; i--;) { records[i] = owner.convertSelectionRecord(records[i]); } } if (store.isLoading() || (store.loadCount === 0 && !store.getCount())) { // If it is NOT a preloaded store, then unless a Session is being used, // The newly loaded records will NOT match any in the ownerStore. // So we must match them by ID in order to select the same dataset. store.on('load', function() { if (!me.destroyed) { me.selectRecords(records); } }, null, { single: true }); } else { me.selectRecords(records); } }, getOwnerStore: function() { return this.owner.getStore(); }, afterShow: function() { var searchField; this.callParent(arguments); // Do not focus if this was invoked by a touch gesture if (!this.invocationEvent || this.invocationEvent.pointerType !== 'touch') { searchField = this.lookupReference('searchField'); if (searchField) { searchField.focus(); } } this.invocationEvent = null; }, /** * Returns the store that holds search results. By default this comes from the * "search grid". If this aspect of the view is changed sufficiently so that the * search grid cannot be found, this method should be overridden to return the proper * store. * @return {Ext.data.Store} */ getSearchStore: function() { var searchGrid = this.lookupReference('searchGrid'); return searchGrid.getStore(); }, makeDockedItems: function() { return [{ xtype: 'textfield', reference: 'searchField', dock: 'top', hideFieldLabel: true, emptyText: this.searchText, cls: Ext.baseCSSPrefix + 'multiselector-search-input', triggers: { clear: { cls: Ext.baseCSSPrefix + 'form-clear-trigger', handler: 'onClearSearch', hidden: true } }, listeners: { specialKey: 'onSpecialKey', change: { fn: 'onSearchChange', buffer: 300 } } }]; }, onSpecialKey: function(field, event) { if (event.getKey() === event.TAB && event.shiftKey) { event.preventDefault(); this.owner.searchTool.focus(); } }, makeItems: function() { return [{ xtype: 'grid', reference: 'searchGrid', trailingBufferZone: 2, leadingBufferZone: 2, viewConfig: { deferEmptyText: false, emptyText: 'No results.' } }]; }, getMatchingRecords: function(records) { var searchGrid = this.lookupReference('searchGrid'), store = searchGrid.getStore(), selections = [], i, record, len; records = Ext.isArray(records) ? records : [records]; for (i = 0, len = records.length; i < len; i++) { record = store.getById(records[i].getId()); if (record) { selections.push(record); } } return selections; }, selectRecords: function(records) { var searchGrid = this.lookupReference('searchGrid'); // match up passed records to the records in the search store so that the right // internal ids are used records = this.getMatchingRecords(records); return searchGrid.getSelectionModel().select(records); }, deselectRecords: function(records) { var searchGrid = this.lookupReference('searchGrid'); // match up passed records to the records in the search store so that the right // internal ids are used records = this.getMatchingRecords(records); return searchGrid.getSelectionModel().deselect(records); }, search: function(text) { var me = this, filter = me.searchFilter, filters = me.getSearchStore().getFilters(); if (text) { filters.beginUpdate(); if (filter) { filter.setValue(text); } else { me.searchFilter = filter = new Ext.util.Filter({ id: 'search', property: me.field, value: text }); } filters.add(filter); filters.endUpdate(); } else if (filter) { filters.remove(filter); } }, privates: { onClearSearch: function() { var searchField = this.lookupReference('searchField'); searchField.setValue(null); searchField.focus(); }, onSearchChange: function(searchField) { var value = searchField.getValue(), trigger = searchField.getTrigger('clear'); trigger.setHidden(!value); this.search(value); }, onSelectionChange: function(selModel, selection) { var owner = this.owner, store = owner.getStore(), data = store.data, remove = 0, map = {}, add, i, id, record; for (i = selection.length; i--;) { record = selection[i]; id = record.id; map[id] = record; if (!data.containsKey(id)) { (add || (add = [])).push(owner.convertSearchRecord(record)); } } for (i = data.length; i--;) { record = data.getAt(i); if (!map[record.id]) { (remove || (remove = [])).push(record); } } if (add || remove) { data.splice(data.length, remove, add); } } }});