/** * An internally used DataView for {@link Ext.form.field.ComboBox ComboBox}. */Ext.define('Ext.view.BoundList', { extend: 'Ext.view.View', alias: 'widget.boundlist', alternateClassName: 'Ext.BoundList', requires: [ 'Ext.view.BoundListKeyNav', 'Ext.layout.component.BoundList', 'Ext.toolbar.Paging' ], mixins: [ 'Ext.mixin.Queryable' ], /** * @cfg {Number} pageSize * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed at the bottom of the list * and store queries will execute with page {@link Ext.data.operation.Read#start start} and * {@link Ext.data.operation.Read#limit limit} parameters. */ pageSize: 0, /** * @cfg {String} [displayField=""] * The field from the store to show in the view. */ /** * @property {Ext.toolbar.Paging} pagingToolbar * A reference to the PagingToolbar instance in this view. Only populated if {@link #pageSize} * is greater than zero and the BoundList has been rendered. */ /** * @cfg baseCls * @inheritdoc */ baseCls: Ext.baseCSSPrefix + 'boundlist', /** * @cfg itemCls * @inheritdoc */ itemCls: Ext.baseCSSPrefix + 'boundlist-item', listItemCls: '', /** * @cfg shadow * @inheritdoc */ shadow: false, /** * @cfg trackOver * @inheritdoc */ trackOver: true, /** * @cfg preserveScrollOnRefresh * @inheritdoc */ preserveScrollOnRefresh: true, enableInitialSelection: false, refreshSelmodelOnRefresh: true, /** * @cfg componentLayout * @inheritdoc */ componentLayout: 'boundlist', /** * @cfg navigationModel * @inheritdoc */ navigationModel: 'boundlist', /** * @cfg scrollable * @inheritdoc */ scrollable: true, /** * @property ariaEl * @inheritdoc */ ariaEl: 'listEl', /** * @cfg tabIndex * @inheritdoc */ tabIndex: -1, /** * @cfg childEls * @inheritdoc */ childEls: [ 'listWrap', 'listEl' ], /* eslint-disable indent */ /** * @cfg renderTpl * @inheritdoc */ renderTpl: [ '<div id="{id}-listWrap" data-ref="listWrap"', ' class="{baseCls}-list-ct ', Ext.dom.Element.unselectableCls, '">', '<ul id="{id}-listEl" data-ref="listEl" class="', Ext.baseCSSPrefix, 'list-plain"', '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>', '>', '</ul>', '</div>', '{%', 'var pagingToolbar=values.$comp.pagingToolbar;', 'if (pagingToolbar) {', 'Ext.DomHelper.generateMarkup(pagingToolbar.getRenderTree(), out);', '}', '%}', { disableFormats: true } ], /* eslint-enable indent */ /** * @cfg {String/Ext.XTemplate} tpl * A String or Ext.XTemplate instance to apply to inner template. * * {@link Ext.view.BoundList} is used for the dropdown list of * {@link Ext.form.field.ComboBox}. To customize the template you can set the tpl on * the combobox config object: * * Ext.create('Ext.form.field.ComboBox', { * fieldLabel : 'State', * queryMode : 'local', * displayField : 'text', * valueField : 'abbr', * store : Ext.create('StateStore', { * fields : ['abbr', 'text'], * data : [ * {"abbr":"AL", "name":"Alabama"}, * {"abbr":"AK", "name":"Alaska"}, * {"abbr":"AZ", "name":"Arizona"} * //... * ] * }), * // Template for the dropdown menu. * // Note the use of the "x-list-plain" and "x-boundlist-item" class, * // this is required to make the items selectable. * tpl: Ext.create('Ext.XTemplate', * '<ul class="x-list-plain"><tpl for=".">', * '<li role="option" class="x-boundlist-item">{abbr} - {name}</li>', * '</tpl></ul>' * ), * }); * * Defaults to: * * Ext.create('Ext.XTemplate', * '<ul><tpl for=".">', * '<li role="option" class="' + itemCls + '">' + me.getInnerTpl(me.displayField) + * '</li>', * '</tpl></ul>' * ); * */ // Override because on non-touch devices, the bound field // retains focus so that typing may narrow the list. // Only when the show is triggered by a touch does the BoundList // get explicitly focused so that the keyboard does not appear. /** * @cfg focusOnToFront * @inheritdoc */ focusOnToFront: false, /** * @cfg alignOnScroll * @inheritdoc */ alignOnScroll: false, initComponent: function() { var me = this, baseCls = me.baseCls, itemCls = me.itemCls; me.selectedItemCls = baseCls + '-selected'; if (me.trackOver) { me.overItemCls = baseCls + '-item-over'; } me.itemSelector = '.' + itemCls; if (me.floating) { me.addCls(baseCls + '-floating'); } if (!me.tpl) { // should be setting aria-posinset based on entire set of data // not filtered set me.generateTpl(); } else if (!me.tpl.isTemplate) { me.tpl = new Ext.XTemplate(me.tpl); } if (me.pageSize) { me.pagingToolbar = me.createPagingToolbar(); } me.callParent(); }, /** * Allow tpl to be generated programmatically to respond to changes in displayField * @private */ generateTpl: function() { var me = this; /* eslint-disable indent, max-len */ me.tpl = new Ext.XTemplate( '<tpl for=".">', '<li role="option" unselectable="on" class="' + me.itemCls + '">' + me.getInnerTpl(me.displayField) + '</li>', '</tpl>' ); /* eslint-enable indent, max-len */ }, /** * Updates the display field for this view. This will automatically trigger * an regeneration of the tpl so that the updated displayField can be used * @param {String} displayField */ setDisplayField: function(displayField) { this.displayField = displayField; this.generateTpl(); }, getRefOwner: function() { return this.pickerField || this.callParent(); }, getRefItems: function() { var result = this.callParent(), toolbar = this.pagingToolbar; if (toolbar) { result.push(toolbar); } return result; }, createPagingToolbar: function() { var me = this; return new Ext.toolbar.Paging({ id: me.id + '-paging-toolbar', pageSize: me.pageSize, store: me.dataSource, border: false, ownerCt: me, ownerLayout: me.getComponentLayout() }); }, refresh: function() { var me = this, tpl = me.tpl; // Allow access to the context for XTemplate scriptlets tpl.field = me.pickerField; tpl.store = me.store; me.callParent(); tpl.field = tpl.store = null; if (!me.ariaStaticRoles[me.ariaRole]) { me.refreshAriaAttributes(); } // The view selectively removes item nodes, so the toolbar // will be preserved in the DOM }, refreshAriaAttributes: function() { var me = this, store = me.store, selModel = me.getSelectionModel(), multiSelect, totalCount, nodes, node, record, index, i, len; // When the store is filtered or paged, we want to let the Assistive Technology // users know that there are more records than currently displayed. This is not // a requirement when the whole dataset fits the DOM. // Note that it is possible for the store to be filtered but not fit the DOM. // In that case we use filtered count as the set size. totalCount = store.isFiltered() ? store.getCount() : store.getTotalCount() || store.getCount(); nodes = me.getNodes(); multiSelect = me.pickerField && me.pickerField.multiSelect; for (i = 0, len = nodes.length; i < len; i++) { node = nodes[i]; record = null; if (totalCount !== len) { record = me.getRecord(node); index = store.indexOf(record); node.setAttribute('aria-setsize', totalCount); node.setAttribute('aria-posinset', index); } // For single-select combos aria-selected must be undefined if (multiSelect) { record = record || me.getRecord(node); node.setAttribute('aria-selected', selModel.isSelected(record)); } } }, bindStore: function(store, initial) { var toolbar = this.pagingToolbar; this.callParent(arguments); if (toolbar) { toolbar.bindStore(store, initial); } }, /** * A method that returns the inner template for displaying items in the list. * This method is useful to override when using a more complex display value, for example * inserting an icon along with the text. * * The XTemplate is created with a reference to the owning form field in the `field` property * to provide access to context. For example to highlight the currently typed value, * the getInnerTpl may be configured into a ComboBox as part of the * {@link Ext.form.field.ComboBox#listConfig listConfig}: * * listConfig: { * getInnerTpl: function() { * return '{[values.name.replace(this.field.getRawValue(), "<b>" + * this.field.getRawValue() + "</b>")]}'; * } * } * @param {String} displayField The {@link #cfg!displayField} for the BoundList. * @return {String} The inner template */ getInnerTpl: function(displayField) { return '{' + displayField + '}'; }, onShow: function() { var field = this.pickerField; this.callParent(); // If the input field is not focused, then focus it. if (field && field.rendered && !field.hasFocus && !Ext.isTouchMode()) { field.focus(); } }, afterComponentLayout: function(width, height, oldWidth, oldHeight) { var field = this.pickerField; this.callParent([width, height, oldWidth, oldHeight]); // Bound list may change size, so realign on layout // **if the field is an Ext.form.field.Picker which has alignPicker!** if (field && field.alignPicker) { field.alignPicker(); } }, onItemSelect: function(record) { var me = this, node; node = me.callParent([record]); if (node) { if (me.ariaSelectable) { node.setAttribute('aria-selected', 'true'); } else { node.removeAttribute('aria-selected'); } } return node; }, onItemDeselect: function(record) { var me = this, node; node = me.callParent([record]); if (node && me.ariaSelectable) { if (me.pickerField && me.pickerField.multiSelect) { node.setAttribute('aria-selected', 'false'); } else { node.removeAttribute('aria-selected'); } } return node; }, // Clicking on an already selected item collapses the picker onItemClick: function(record) { // The selection change events won't fire when clicking on the selected element. // Detect it here. var me = this, field = me.pickerField, valueField, selected; if (!field) { return; } valueField = field.valueField; selected = me.getSelectionModel().getSelection(); if (!field.multiSelect && selected.length) { selected = selected[0]; // Not all pickerField's have a collapse API, i.e. Ext.ux.form.MultiSelect. if (selected && field.isEqual(record.get(valueField), selected.get(valueField)) && field.collapse) { field.collapse(); } } }, onContainerClick: function(e) { var toolbar = this.pagingToolbar, clientRegion; // Ext.view.View template method // Do not continue to process the event as a container click // if it is within the pagingToolbar if (toolbar && toolbar.rendered && e.within(toolbar.el)) { return false; } // IE10 and IE11 will fire pointer events when user drags listWrap scrollbars, // which may result in selection being reset. if (Ext.isIE10 || Ext.isIE11) { clientRegion = this.listWrap.getClientRegion(); if (!e.getPoint().isContainedBy(clientRegion)) { return false; } } }, doDestroy: function() { this.pagingToolbar = Ext.destroy(this.pagingToolbar); this.callParent(); }, privates: { /** * @method getNodeContainer * @private * @inheritdoc */ getNodeContainer: function() { return this.listEl; }, getTargetEl: function() { return this.listEl; }, getOverflowEl: function() { return this.listWrap; }, // Do the job of a container layout at this point even though we are not a Container. finishRenderChildren: function() { var toolbar = this.pagingToolbar; this.callParent(arguments); if (toolbar) { toolbar.finishRender(); } } }});