You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1321 lines
44 KiB
JavaScript

/**
* @class Ext.ux.form.field.BoxSelect
* @extends Ext.form.field.ComboBox
*
* BoxSelect for ExtJS 4, a combo box improved for multiple value querying, selection and management.
*
* A friendlier combo box for multiple selections that creates easily individually
* removable labels for each selection, as seen on facebook and other sites. Querying
* and type-ahead support are also improved for multiple selections.
*
* Options and usage mostly remain consistent with the {@link Ext.form.field.ComboBox}
* control. Some default configuration options have changed, but should still
* work properly if overridden.
*
* Inspired by the SuperBoxSelect component for ExtJS 3 (http://technomedia.co.uk/SuperBoxSelect/examples3.html),
* which in turn was inspired by the BoxSelect component for ExtJS 2 (http://efattal.fr/en/extjs/extuxboxselect/).
*
* Various contributions and suggestions made by many members of the ExtJS community which can be seen
* in the user extension posting: http://www.sencha.com/forum/showthread.php?134751-Ext.ux.form.field.BoxSelect
*
* Many thanks go out to all of those who have contributed, this extension would not be
* possible without your help.
*
* @author kvee_iv http://www.sencha.com/forum/member.php?29437-kveeiv
* @version 1.3.1
* @requires BoxSelect.css
* @xtype boxselect
*/
Ext.define('Ext.ux.form.field.BoxSelect', {
extend:'Ext.form.field.ComboBox',
alias: ['widget.comboboxselect', 'widget.boxselect'],
requires: ['Ext.selection.Model', 'Ext.data.Store'],
/**
* @cfg {Boolean} multiSelect
* If set to <code>true</code>, allows the combo field to hold more than one value at a time, and allows selecting
* multiple items from the dropdown list. (Defaults to <code>true</code>, the default usage for BoxSelect)
*/
multiSelect: true,
/**
* @cfg {Boolean} forceSelection
* <code>true</code> to restrict the selected value to one of the values in the list,
* <code>false</code> to allow the user to set arbitrary text into the field (defaults to <code>true</code>, the default usage for BoxSelect)
*/
forceSelection: true,
/**
* @cfg {Boolean} selectOnFocus <code>true</code> to automatically select any existing field text when the field
* receives input focus (defaults to <code>true</code> for best multi-select usability during querying)
*/
selectOnFocus: true,
/**
* @cfg {Boolean} triggerOnClick <code>true</code> to activate the trigger when clicking in empty space
* in the field. Note that the subsequent behavior of this is controlled by the field's {@link #triggerAction}.
* This behavior is similar to that of a basic ComboBox with {@link #editable} <code>false</code>.
* (defaults to <code>true</code>).
*/
triggerOnClick: true,
/**
* @cfg {Boolean} createNewOnEnter
* When forceSelection is false, new records can be created by the user. These records are not added to the
* combo's store. By default, this creation is triggered by typing the configured 'delimiter'. With
* createNewOnEnter set to true, this creation can also be triggered by the 'enter' key. This configuration
* option has no effect if forceSelection is true. (defaults to <code>false</code>)
* <code>true</code> to allow the user to press 'enter' to create a new record
* <code>false</code> to only allow the user to type the configured 'delimiter' to create a new record
*/
createNewOnEnter: false,
/**
* @cfg {Boolean} createNewOnBlur
* Similar to {@link #createNewOnEnter}, createNewOnBlur will create a new record when the field loses focus.
* This configuration option has no effect if forceSelection is true. Please note that this behavior is also
* affected by the configuration options {@link #autoSelect} and {@link #selectOnTab}. If those are true
* and an existing item would have been selected as a result, the partial text the user has entered will
* be discarded.
* <code>true</code> to create a new record when the field loses focus
* <code>false</code> to not create a new record on blur
*/
createNewOnBlur: false,
/**
* @cfg {Boolean} encodeSubmitValue
* Controls the formatting of the form submit value of the field. (defaults to <code>false</code>). This
* is not applicable of {@link #multiSelect} is false.
* <code>true</code> for the field value to submit as a json encoded array in a single POST variable
* <code>false</code> for the field to submit as an array of POST variables
*/
encodeSubmitValue: false,
/**
* @cfg {Boolean} stacked
* When stacked is true, each labelled item will fill to the width of the form field
* <code>true</code> to have each labelled item fill the width of the form field
* <code>false</code> to have each labelled item size to its displayed contents (defaults to <code>false</code>)
*/
stacked: false,
/**
* @cfg {Boolean} pinList
* When multiSelect is true, the pick list used for the combo will stay open after each selection is made. This
* config option has no effect if multiSelect is false.
* <code>true</code> to keep the pick list expanded after each multiSelect selection
* <code>false</code> to collapse the pick list after each multiSelect selection (defaults to <code>true</code>)
*/
pinList: true,
/**
* @cfg {Boolean} grow <tt>true</tt> if this field should automatically grow and shrink to its content
* (defaults to <tt>true</tt>)
*/
grow: true,
/**
* @cfg {Number} growMin The minimum height to allow when <tt>{@link Ext.form.field.Text#grow grow}=true</tt>
* (defaults to <tt>false</tt>, which allows for natural growth based on selections)
*/
growMin: false,
/**
* @cfg {Number} growMax The maximum height to allow when <tt>{@link Ext.form.field.Text#grow grow}=true</tt>
* (defaults to <tt>false</tt>, which allows for natural growth based on selections)
*/
growMax: false,
//private
componentLayout: 'boxselectfield',
/**
* Initialize additional settings and enable simultaneous typeAhead and multiSelect support
*/
initComponent: function() {
var me = this,
typeAhead = me.typeAhead;
if (typeAhead && !me.editable) {
Ext.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.');
}
Ext.apply(me, {
typeAhead: false
});
me.callParent(arguments);
me.typeAhead = typeAhead;
me.selectionModel = new Ext.selection.Model({
store: me.valueStore,
mode: 'MULTI',
onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
commitFn();
}
});
if (!Ext.isEmpty(me.delimiter) && me.multiSelect) {
me.delimiterEndingRegexp = new RegExp(String(me.delimiter).replace(/[$%()*+.?\[\\\]{|}]/g, "\\$&") + "$");
}
},
/**
* Register events for management controls of labelled items
*/
initEvents: function() {
var me = this;
me.callParent(arguments);
if (!me.enableKeyEvents) {
me.mon(me.inputEl, 'keydown', me.onKeyDown, me);
}
me.mon(me.itemList, 'click', me.onItemListClick, me);
me.mon(me.selectionModel, 'selectionchange', me.applyMultiselectItemMarkup, me);
},
/**
* Create a store for the records of our current value based on the main store's model
*/
bindStore: function(store, initial) {
var me = this,
oldStore = me.store;
if (oldStore) {
me.mun(oldStore, 'beforeload', me.onBeforeLoad, me);
if (me.valueStore) {
me.mun(me.valueStore, 'datachanged', me.applyMultiselectItemMarkup, me);
me.valueStore = null;
}
}
me.callParent(arguments);
if (me.store) {
me.valueStore = new Ext.data.Store({
model: me.store.model,
proxy: {
type: 'memory'
}
});
me.mon(me.valueStore, 'datachanged', me.applyMultiselectItemMarkup, me);
me.mon(me.store, 'beforeload', me.onBeforeLoad, me);
}
},
/**
* Add refresh tracking to the picker for selection management
*/
createPicker: function() {
var me = this,
picker = me.callParent(arguments);
me.mon(picker, {
'beforerefresh': me.onBeforeListRefresh,
'show': function(pick) {
/**
* Temporary fix for reapplying maxHeight after shorter list was previously shown
*/
var listEl = picker.listEl,
ch = listEl.getHeight();
if (ch > picker.maxHeight) {
listEl.setHeight(picker.maxHeight);
}
},
scope: me
});
return picker;
},
/**
* Clean up labelled items management controls
*/
onDestroy: function() {
var me = this;
Ext.destroyMembers(me, 'selectionModel', 'valueStore');
me.callParent(arguments);
},
/**
* Overridden to avoid use of placeholder, as our main input field is often empty
*/
afterRender: function() {
var me = this;
if (Ext.supports.Placeholder && me.inputEl && me.emptyText) {
delete me.inputEl.dom.placeholder;
}
if (me.stacked === true) {
me.itemList.addCls('x-boxselect-stacked');
}
if (me.grow) {
if (Ext.isNumber(me.growMin) && (me.growMin > 0)) {
me.itemList.applyStyles('min-height:'+me.growMin+'px');
}
if (Ext.isNumber(me.growMax) && (me.growMax > 0)) {
me.itemList.applyStyles('max-height:'+me.growMax+'px');
}
}
me.applyMultiselectItemMarkup();
me.callParent(arguments);
},
/**
* Overridden to search store snapshot instead of data (if available)
*/
findRecord: function(field, value) {
var ds = this.store,
rec = false,
idx;
if (ds.snapshot) {
idx = ds.snapshot.findIndexBy(function(rec) {
return rec.get(field) === value;
});
rec = (idx !== -1) ? ds.snapshot.getAt(idx) : false;
} else {
idx = ds.findExact(field, value);
rec = (idx !== -1) ? ds.getAt(idx) : false;
}
return rec;
},
/**
* When the picker is refreshing, we should ignore selection changes. Otherwise
* the value of our field will be changing just because our view of the choices is.
*/
onBeforeLoad: function() {
this.ignoreSelection++;
},
/**
* Overridden to map previously selected records to the "new" versions of the records
* based on value field, if they are part of the new store load
*/
onLoad: function() {
var me = this,
valueField = me.valueField,
valueStore = me.valueStore,
changed = false;
if (valueStore) {
if (!Ext.isEmpty(me.value) && (valueStore.getCount() == 0)) {
me.setValue(me.value, false, true);
}
valueStore.suspendEvents();
valueStore.each(function(rec) {
var r = me.findRecord(valueField, rec.get(valueField)),
i = r ? valueStore.indexOf(rec) : -1;
if (i >= 0) {
valueStore.removeAt(i);
valueStore.insert(i, r);
changed = true;
}
});
valueStore.resumeEvents();
if (changed) {
valueStore.fireEvent('datachanged', valueStore);
}
}
me.callParent(arguments);
me.ignoreSelection = Ext.Number.constrain(me.ignoreSelection - 1, 0);
me.alignPicker();
},
/**
* @private
* Used to determine if a record is filtered (for retaining as a multiSelect value)
*/
isFilteredRecord: function(record) {
var me = this,
store = me.store,
valueField = me.valueField,
storeRecord,
filtered = false;
storeRecord = store.findExact(valueField, record.get(valueField));
filtered = ((storeRecord === -1) && (!store.snapshot || (me.findRecord(valueField, record.get(valueField)) !== false)));
filtered = filtered || (!filtered && (storeRecord === -1) && (me.forceSelection !== true) &&
(me.valueStore.findExact(valueField, record.get(valueField)) >= 0));
return filtered;
},
/**
* Overridden to allow for continued querying with multiSelect selections already made
*/
doRawQuery: function() {
var me = this,
rawValue = me.inputEl.dom.value;
if (me.multiSelect) {
rawValue = rawValue.split(me.delimiter).pop();
}
this.doQuery(rawValue, false, true);
},
/**
* When the picker is refreshing, we should ignore selection changes. Otherwise
* the value of our field will be changing just because our view of the choices is.
*/
onBeforeListRefresh: function() {
this.ignoreSelection++;
},
/**
* When the picker is refreshing, we should ignore selection changes. Otherwise
* the value of our field will be changing just because our view of the choices is.
*/
onListRefresh: function() {
this.callParent(arguments);
this.ignoreSelection = Ext.Number.constrain(this.ignoreSelection - 1, 0);
},
/**
* Overridden to preserve current labelled items when list is filtered/paged/loaded
* and does not include our current value.
*/
onListSelectionChange: function(list, selectedRecords) {
var me = this,
valueStore = me.valueStore,
mergedRecords = [],
i;
// Only react to selection if it is not called from setValue, and if our list is
// expanded (ignores changes to the selection model triggered elsewhere)
if ((me.ignoreSelection <= 0) && me.isExpanded) {
// Pull forward records that were already selected or are now filtered out of the store
valueStore.each(function(rec) {
if (Ext.Array.contains(selectedRecords, rec) || me.isFilteredRecord(rec)) {
mergedRecords.push(rec);
}
});
mergedRecords = Ext.Array.merge(mergedRecords, selectedRecords);
i = Ext.Array.intersect(mergedRecords, valueStore.getRange()).length;
if ((i != mergedRecords.length) || (i != me.valueStore.getCount())) {
me.setValue(mergedRecords, false);
if (!me.multiSelect || !me.pinList) {
Ext.defer(me.collapse, 1, me);
}
if (valueStore.getCount() > 0) {
me.fireEvent('select', me, valueStore.getRange());
}
}
me.inputEl.focus();
if (!me.pinList) {
me.inputEl.dom.value = '';
}
if (me.selectOnFocus) {
me.inputEl.dom.select();
}
}
},
/**
* Overridden to use valueStore instead of valueModels, for inclusion of filtered records
*/
syncSelection: function() {
var me = this,
picker = me.picker,
valueField = me.valueField,
pickStore, selection, selModel;
if (picker) {
pickStore = picker.store;
// From the value, find the Models that are in the store's current data
selection = [];
if (me.valueStore) {
me.valueStore.each(function(rec) {
var i = pickStore.findExact(valueField, rec.get(valueField));
if (i >= 0) {
selection.push(pickStore.getAt(i));
}
});
}
// Update the selection to match
me.ignoreSelection++;
selModel = picker.getSelectionModel();
selModel.deselectAll();
if (selection.length > 0) {
selModel.select(selection);
}
me.ignoreSelection = Ext.Number.constrain(me.ignoreSelection - 1, 0);
}
},
/**
* Overridden to align to itemList size instead of inputEl
*/
alignPicker: function() {
var me = this,
picker, isAbove,
aboveSfx = '-above',
itemBox = me.itemList.getBox(false, true);
if (this.isExpanded) {
picker = me.getPicker();
var pickerScrollPos = picker.getTargetEl().dom.scrollTop;
if (me.matchFieldWidth) {
// Auto the height (it will be constrained by min and max width) unless there are no records to display.
picker.setSize(itemBox.width, picker.store && picker.store.getCount() ? null : 0);
}
if (picker.isFloating()) {
picker.alignTo(me.itemList, me.pickerAlign, me.pickerOffset);
// add the {openCls}-above class if the picker was aligned above
// the field due to hitting the bottom of the viewport
isAbove = picker.el.getY() < me.inputEl.getY();
me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx);
picker.el[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx);
}
}
},
/**
* @private
* Get the current cursor position in the input field
*/
getCursorPosition: function() {
var cursorPos;
if (Ext.isIE) {
cursorPos = document.selection.createRange();
cursorPos.collapse(true);
cursorPos.moveStart("character", -this.inputEl.dom.value.length);
cursorPos = cursorPos.text.length;
} else {
cursorPos = this.inputEl.dom.selectionStart;
}
return cursorPos;
},
/**
* @private
* Check to see if the input field has selected text
*/
hasSelectedText: function() {
var sel, range;
if (Ext.isIE) {
sel = document.selection;
range = sel.createRange();
return (range.parentElement() == this.inputEl.dom);
} else {
return this.inputEl.dom.selectionStart != this.inputEl.dom.selectionEnd;
}
},
/**
* Handles keyDown processing of key-based selection of labelled items
*/
onKeyDown: function(e, t) {
var me = this,
key = e.getKey(),
rawValue = me.inputEl.dom.value,
valueStore = me.valueStore,
selModel = me.selectionModel,
stopEvent = false,
rec, i;
if (me.readOnly || me.disabled || !me.editable) {
return;
}
// Handle keyboard based navigation of selected labelled items
if ((valueStore.getCount() > 0) &&
((rawValue == '') || ((me.getCursorPosition() === 0) && !me.hasSelectedText()))) {
if ((key == e.BACKSPACE) || (key == e.DELETE)) {
if (selModel.getCount() > 0) {
me.valueStore.remove(selModel.getSelection());
} else {
me.valueStore.remove(me.valueStore.last());
}
me.setValue(me.valueStore.getRange());
selModel.deselectAll();
stopEvent = true;
} else if ((key == e.RIGHT) || (key == e.LEFT)) {
if ((selModel.getCount() === 0) && (key == e.LEFT)) {
selModel.select(valueStore.last());
stopEvent = true;
} else if (selModel.getCount() > 0) {
rec = selModel.getLastFocused() || selModel.getLastSelected();
if (rec) {
i = valueStore.indexOf(rec);
if (key == e.RIGHT) {
if (i < (valueStore.getCount() - 1)) {
selModel.select(i + 1, e.shiftKey);
stopEvent = true;
} else if (!e.shiftKey) {
selModel.deselect(rec);
stopEvent = true;
}
} else if ((key == e.LEFT) && (i > 0)) {
selModel.select(i - 1, e.shiftKey);
stopEvent = true;
}
}
}
} else if (key == e.A && e.ctrlKey) {
selModel.selectAll();
stopEvent = e.A;
}
me.inputEl.focus();
}
if (stopEvent) {
me.preventKeyUpEvent = stopEvent;
e.stopEvent();
return;
}
// Prevent key up processing for enter if it is being handled by the picker
if (me.isExpanded && (key == e.ENTER) && me.picker.highlightedItem) {
me.preventKeyUpEvent = true;
}
if (me.enableKeyEvents) {
me.callParent(arguments);
}
if (!e.isSpecialKey() && !e.hasModifier()) {
me.selectionModel.deselectAll();
me.inputEl.focus();
}
},
/**
* Handles auto-selection of labelled items based on this field's delimiter, as well
* as the keyUp processing of key-based selection of labelled items.
*/
onKeyUp: function(e, t) {
var me = this,
rawValue = me.inputEl.dom.value,
rec;
if (me.preventKeyUpEvent) {
e.stopEvent();
if ((me.preventKeyUpEvent === true) || (e.getKey() === me.preventKeyUpEvent)) {
delete me.preventKeyUpEvent;
}
return;
}
if (me.multiSelect && (me.delimiterEndingRegexp && me.delimiterEndingRegexp.test(rawValue)) ||
((me.createNewOnEnter === true) && e.getKey() == e.ENTER)) {
rawValue = rawValue.replace(me.delimiterEndingRegexp, '');
if (!Ext.isEmpty(rawValue)) {
rec = me.valueStore.findExact(me.valueField, rawValue);
if (rec >= 0) {
rec = me.valueStore.getAt(rec);
} else {
rec = me.store.findExact(me.valueField, rawValue);
if (rec >= 0) {
rec = me.store.getAt(rec);
} else {
rec = false;
}
}
if (!rec && !me.forceSelection) {
rec = {};
rec[me.valueField] = rawValue;
rec[me.displayField] = rawValue;
rec = new me.valueStore.model(rec);
}
if (rec) {
me.collapse();
me.setValue(me.valueStore.getRange().concat(rec));
me.inputEl.dom.value = '';
me.inputEl.focus();
}
}
}
me.callParent([e,t]);
Ext.Function.defer(me.alignPicker, 10, me);
},
/**
* Overridden to get and set the dom value directly for type-ahead suggestion (bypassing get/setRawValue)
*/
onTypeAhead: function() {
var me = this,
displayField = me.displayField,
inputElDom = me.inputEl.dom,
record = me.store.findRecord(displayField, inputElDom.value),
boundList = me.getPicker(),
newValue, len, selStart;
if (record) {
newValue = record.get(displayField);
len = newValue.length;
selStart = inputElDom.value.length;
boundList.highlightItem(boundList.getNode(record));
if (selStart !== 0 && selStart !== len) {
inputElDom.value = newValue;
me.selectText(selStart, newValue.length);
}
}
},
/**
* Delegation control for selecting and removing labelled items or triggering list collapse/expansion
*/
onItemListClick: function(evt, el, o) {
var me = this,
itemEl = evt.getTarget('.x-boxselect-item'),
closeEl = itemEl ? evt.getTarget('.x-boxselect-item-close') : false;
if (me.readOnly || me.disabled) {
return;
}
evt.stopPropagation();
if (itemEl) {
if (closeEl) {
me.removeByListItemNode(itemEl);
} else {
me.toggleSelectionByListItemNode(itemEl, evt.shiftKey);
}
me.inputEl.focus();
} else if (me.triggerOnClick) {
me.onTriggerClick();
}
},
/**
* Build the markup for the labelled items. Template must be built on demand due to ComboBox initComponent
* lifecycle for the creation of on-demand stores (to account for automatic valueField/displayField setting)
*/
getMultiSelectItemMarkup: function() {
var me = this;
if (!me.multiSelectItemTpl) {
if (!me.labelTpl) {
me.labelTpl = Ext.create('Ext.XTemplate',
'{[values.' + me.displayField + ']}'
);
} else if (Ext.isString(me.labelTpl)) {
me.labelTpl = Ext.create('Ext.XTemplate', me.labelTpl);
}
me.multiSelectItemTpl = [
'<tpl for=".">',
'<li class="x-boxselect-item ',
'<tpl if="this.isSelected(values.'+ me.valueField + ')">',
' selected',
'</tpl>',
'" qtip="{[typeof values === "string" ? values : values.' + me.displayField + ']}">' ,
'<div class="x-boxselect-item-text">{[typeof values === "string" ? values : this.getItemLabel(values)]}</div>',
'<div class="x-tab-close-btn x-boxselect-item-close"></div>' ,
'</li>' ,
'</tpl>',
{
compile: true,
disableFormats: true,
isSelected: function(value) {
var i = me.valueStore.findExact(me.valueField, value);
if (i >= 0) {
return me.selectionModel.isSelected(me.valueStore.getAt(i));
}
},
getItemLabel: function(values) {
return me.getTpl('labelTpl').apply(values);
}
}
];
}
return this.getTpl('multiSelectItemTpl').apply(Ext.Array.pluck(this.valueStore.getRange(), 'data'));
},
/**
* Update the labelled items rendering
*/
applyMultiselectItemMarkup: function() {
var me = this,
itemList = me.itemList,
item;
if (itemList) {
while ((item = me.inputElCt.prev()) != null) {
item.remove();
}
me.inputElCt.insertHtml('beforeBegin', me.getMultiSelectItemMarkup());
}
},
/**
* Returns the record from valueStore for the labelled item node
*/
getRecordByListItemNode: function(itemEl) {
var me = this,
itemIdx = 0,
searchEl = me.itemList.dom.firstChild;
while (searchEl && searchEl.nextSibling) {
if (searchEl == itemEl) {
break;
}
itemIdx++;
searchEl = searchEl.nextSibling;
}
itemIdx = (searchEl == itemEl) ? itemIdx : false;
if (itemIdx === false) {
return false;
}
return me.valueStore.getAt(itemIdx);
},
/**
* Toggle of labelled item selection by node reference
*/
toggleSelectionByListItemNode: function(itemEl, keepExisting) {
var me = this,
rec = me.getRecordByListItemNode(itemEl);
if (rec) {
if (me.selectionModel.isSelected(rec)) {
me.selectionModel.deselect(rec);
} else {
me.selectionModel.select(rec, keepExisting);
}
}
},
/**
* Removal of labelled item by node reference
*/
removeByListItemNode: function(itemEl) {
var me = this,
rec = me.getRecordByListItemNode(itemEl);
if (rec) {
me.valueStore.remove(rec);
me.setValue(me.valueStore.getRange());
}
},
/**
* Intercept calls to getRawValue to pretend there is no inputEl for rawValue handling,
* so that we can use inputEl for just the user input.
*
* **Note that in general, raw values are the rendered value for the input field,
* and therefore should not be used for comboboxes or most programmatic logic.**
*/
getRawValue: function() {
var me = this,
inputEl = me.inputEl,
result;
me.inputEl = false;
result = me.callParent(arguments);
me.inputEl = inputEl;
return result;
},
/**
* Intercept calls to setRawValue to pretend there is no inputEl for rawValue handling,
* so that we can use inputEl for just the user input.
*
* **Note that in general, raw values are the rendered value for the input field,
* and therefore should not be used for comboboxes or most programmatic logic.**
*/
setRawValue: function(value) {
var me = this,
inputEl = me.inputEl,
result;
me.inputEl = false;
result = me.callParent([value]);
me.inputEl = inputEl;
return result;
},
/**
* Adds a value or values to the current value of the field
* @param {mixed} valueMixed The value or values to add to the current value
*/
addValue: function(valueMixed) {
var me = this;
if (valueMixed) {
me.setValue(Ext.Array.merge(me.value, Ext.Array.from(valueMixed)));
}
},
/**
* Removes a value or values from the current value of the field
* @param {mixed} valueMixed The value or values to remove from the current value
*/
removeValue: function(valueMixed) {
var me = this;
if (valueMixed) {
me.setValue(Ext.Array.difference(me.value, Ext.Array.from(valueMixed)));
}
},
/**
* Intercept calls to setValue to use records from the valueStore when available.
* Unknown values (if forceSelection is true) will trigger a call to store.load
* once to try to retrieve those records. The list of unknown values will be
* submitted as the name of the valueField with values separated by the configured
* delimiter. This process will cause setValue to asynchronously process.
*/
setValue: function(value, doSelect, skipLoad) {
var me = this,
valueStore = me.valueStore,
valueField = me.valueField,
record, len, i, valueRecord, h,
unknownValues = [];
if (Ext.isEmpty(value)) {
value = null;
}
if (Ext.isString(value) && me.multiSelect) {
value = value.split(me.delimiter);
}
value = Ext.Array.from(value);
for (i = 0, len = value.length; i < len; i++) {
record = value[i];
if (!record || !record.isModel) {
valueRecord = valueStore.findExact(valueField, record);
if (valueRecord >= 0) {
value[i] = valueStore.getAt(valueRecord);
} else {
valueRecord = me.findRecord(valueField, record);
if (!valueRecord) {
if (me.forceSelection) {
unknownValues.push(record);
} else {
valueRecord = {};
valueRecord[me.valueField] = record;
valueRecord[me.displayField] = record;
valueRecord = new me.valueStore.model(valueRecord);
}
}
if (valueRecord) {
value[i] = valueRecord;
}
}
}
}
if ((skipLoad !== true) && (unknownValues.length > 0) && (me.queryMode === 'remote')) {
var params = {};
params[me.valueField] = unknownValues.join(me.delimiter);
me.store.load({
params: params,
callback: function() {
me.itemList.unmask();
me.setValue(value, doSelect, true);
me.autoSize();
}
});
return false;
}
/**
* For single-select boxes, use the last value
*/
if (!me.multiSelect && (value.length > 0)) {
value = value[value.length - 1];
}
me.callParent([value, doSelect]);
},
/**
* Returns the records for the field's current value
* @return {Array} The records for the field's current value
*/
getValueRecords: function() {
return this.valueStore.getRange();
},
/**
* Overridden to optionally allow for submitting the field as a json encoded array.
*/
getSubmitData: function() {
var me = this,
val = me.callParent(arguments);
if (me.multiSelect && me.encodeSubmitValue && val && val[me.name]) {
val[me.name] = Ext.encode(val[me.name]);
}
return val;
},
/**
* Overridden to handle creation of new value for unforced selections
*/
beforeBlur: function() {
var me = this;
me.doQueryTask.cancel();
me.assertValue();
me.collapse();
},
/**
* Overridden to clear the input field if we are auto-setting a value as we blur.
*/
mimicBlur: function() {
var me = this;
if (me.selectOnTab && me.picker && me.picker.highlightedItem) {
me.inputEl.dom.value = '';
}
me.callParent(arguments);
},
/**
* Overridden to handle partial-input selections more directly
*/
assertValue: function() {
var me = this,
rawValue = me.inputEl.dom.value,
rec = !Ext.isEmpty(rawValue) ? me.findRecordByDisplay(rawValue) : false,
value = false;
if (!rec && !me.forceSelection && me.createNewOnBlur && !Ext.isEmpty(rawValue)) {
value = rawValue;
} else if (rec) {
value = rec;
}
if (value) {
me.addValue(value);
}
me.inputEl.dom.value = '';
me.collapse();
},
/**
* Update the valueStore from the new value and fire change events for UI to respond to
*/
checkChange: function() {
if (!this.suspendCheckChange && !this.isDestroyed) {
var me = this,
valueStore = me.valueStore,
lastValue = me.lastValue,
valueField = me.valueField,
newValue = Ext.Array.map(Ext.Array.from(me.value), function(val) {
if (val.isModel) {
return val.get(valueField);
}
return val;
}, this).join(this.delimiter);
if (!me.isEqual(newValue, lastValue)) {
valueStore.suspendEvents();
valueStore.removeAll();
if (Ext.isArray(me.valueModels)) {
valueStore.add(me.valueModels);
}
valueStore.resumeEvents();
valueStore.fireEvent('datachanged', valueStore);
me.lastValue = newValue;
me.fireEvent('change', me, newValue, lastValue);
me.onChange(newValue, lastValue)
}
}
},
/**
* Overridden to use value (selection) instead of raw value and to avoid the use of placeholder
*/
applyEmptyText : function() {
var me = this,
emptyText = me.emptyText,
inputEl, isEmpty;
if (me.rendered && emptyText) {
isEmpty = Ext.isEmpty(me.value) && !me.hasFocus;
inputEl = me.inputEl;
if (isEmpty) {
inputEl.dom.value = emptyText;
inputEl.addCls(me.emptyCls);
} else {
if (inputEl.dom.value === emptyText) {
inputEl.dom.value = '';
}
inputEl.removeCls(me.emptyCls);
}
}
},
/**
* Overridden to use inputEl instead of raw value and to avoid the use of placeholder
*/
preFocus : function(){
var me = this,
inputEl = me.inputEl,
emptyText = me.emptyText,
isEmpty;
if (emptyText && inputEl.dom.value === emptyText) {
inputEl.dom.value = '';
isEmpty = true;
inputEl.removeCls(me.emptyCls);
}
if (me.selectOnFocus || isEmpty) {
inputEl.dom.select();
}
},
/**
* Intercept calls to onFocus to add focusCls, because the base field classes assume this should be applied to inputEl
*/
onFocus: function() {
var me = this,
focusCls = me.focusCls,
itemList = me.itemList;
if (focusCls && itemList) {
itemList.addCls(focusCls);
}
me.callParent(arguments);
},
/**
* Intercept calls to onBlur to remove focusCls, because the base field classes assume this should be applied to inputEl
*/
onBlur: function() {
var me = this,
focusCls = me.focusCls,
itemList = me.itemList;
if (focusCls && itemList) {
itemList.removeCls(focusCls);
}
me.callParent(arguments);
},
/**
* Intercept calls to renderActiveError to add invalidCls, because the base field classes assume this should be applied to inputEl
*/
renderActiveError: function() {
var me = this,
invalidCls = me.invalidCls,
itemList = me.itemList,
hasError = me.hasActiveError();
if (invalidCls && itemList) {
itemList[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field');
}
me.callParent(arguments);
},
/**
* Ensure inputEl is sized well for user input using the remaining
* horizontal space available in the list element
*
* Automatically grows the field to accomodate the height of the selections up to the
* maximum field height allowed. This only takes effect if <tt>{@link #grow} = true</tt>,
* and fires the {@link #autosize} event if the height changes.
*/
autoSize: function() {
var me = this,
height;
if (me.rendered) {
me.doComponentLayout();
if (me.grow) {
height = me.getHeight();
if (height !== me.lastInputHeight) {
me.alignPicker();
me.fireEvent('autosize', height);
me.lastInputHeight = height;
}
}
}
return me;
}
}, function() {
/**
* ExtJS 4.0.5 introduced more optimized ways of referencing child elements. As this is
* currently a subscriber only release, these registrations are performed here for
* backwards compatibility with the currently available public version 4.0.2a
*/
var useNewSelectors = !Ext.getVersion('extjs').isLessThan('4.0.5'),
overrides = {};
if (useNewSelectors) {
Ext.apply(overrides, {
fieldSubTpl: [
'<div class="x-boxselect">',
'<ul id="{cmpId}-itemList" class="x-boxselect-list {fieldCls} {typeCls}">',
'<li id="{cmpId}-inputElCt" class="x-boxselect-input">',
'<input id="{cmpId}-inputEl" type="{type}" ',
'<tpl if="name">name="{name}" </tpl>',
'<tpl if="size">size="{size}" </tpl>',
'<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
'class="x-boxselect-input-field" autocomplete="off" />',
'</li>',
'</ul>',
'<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">',
'{triggerEl}',
'<div class="{clearCls}" role="presentation"></div>',
'</div>',
'<div class="{clearCls}" role="presentation"></div>',
'</div>',
{
compiled: true,
disableFormats: true
}
],
childEls: ['itemList', 'inputEl', 'inputElCt']
});
} else {
Ext.apply(overrides, {
fieldSubTpl: [
'<div class="x-boxselect">',
'<ul class="x-boxselect-list {fieldCls} {typeCls}">',
'<li class="x-boxselect-input">',
'<input id="{id}" type="{type}" ',
'<tpl if="name">name="{name}" </tpl>',
'<tpl if="size">size="{size}" </tpl>',
'<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
'class="x-boxselect-input-field" autocomplete="off" />',
'</li>',
'</ul>',
'<div class="{triggerWrapCls}" role="presentation">',
'{triggerEl}',
'<div class="{clearCls}" role="presentation"></div>',
'</div>',
'</div>',
{
compiled: true,
disableFormats: true
}
],
renderSelectors: {
itemList: 'ul.x-boxselect-list',
inputEl: 'input.x-boxselect-input-field',
inputElCt: 'li.x-boxselect-input'
}
});
}
Ext.override(this, overrides);
});
/**
* This is an amalgamation of the TextArea field layout and the Trigger field layout,
* with overrides to manage the layout of the field on the itemList wrap instead
* of the inputEl and to grow based on inputEl wrap positioning instead of
* raw text value.
*/
Ext.define('Ext.ux.layout.component.field.BoxSelectField', {
/* Begin Definitions */
alias: ['layout.boxselectfield'],
extend: 'Ext.layout.component.field.Field',
/* End Definitions */
type: 'boxselectfield',
/**
* Overridden to use an encoded value instead of raw value
*/
beforeLayout: function(width, height) {
var me = this,
owner = me.owner,
lastValue = this.lastValue,
value = Ext.encode(owner.value);
this.lastValue = value;
return me.callParent(arguments) || (owner.grow && value !== lastValue);
},
/**
* Overridden to use itemList instead of inputEl, and to merge trigger field
* sizing with text field growability.
*/
sizeBodyContents: function(width, height) {
var me = this,
owner = me.owner,
triggerWrap = owner.triggerWrap,
triggerWidth = owner.getTriggerWidth(),
itemList, inputEl, inputElCt, lastEntry,
listBox, listWidth, inputWidth;
// If we or our ancestor is hidden, we can get a triggerWidth calculation
// of 0. We don't want to resize in this case.
if (owner.hideTrigger || owner.readOnly || triggerWidth > 0) {
itemList = owner.itemList;
// Decrease the field's width by the width of the triggers. Both the field and the triggerWrap
// are floated left in CSS so they'll stack up side by side.
me.setElementSize(itemList, Ext.isNumber(width) ? width - triggerWidth : width, height);
// Explicitly set the triggerWrap's width, to prevent wrapping
triggerWrap.setWidth(triggerWidth);
// Size the input el to take up the maximum amount of remaining list width,
// or the entirety of list width to cause wrapping if too little space remains.
inputEl = owner.inputEl;
inputElCt = owner.inputElCt;
listBox = itemList.getBox(true, true);
listWidth = listBox.width;
if ((owner.grow && owner.growMax && (itemList.dom.scrollHeight > (owner.growMax - 25))) ||
(owner.isFixedHeight() && (itemList.dom.scrollHeight > itemList.dom.clientHeight))) {
listWidth = listWidth - Ext.getScrollbarSize().width;
}
inputWidth = listWidth - 10;
lastEntry = inputElCt.dom.previousSibling;
if (lastEntry) {
inputWidth = inputWidth - (lastEntry.offsetLeft + Ext.fly(lastEntry).getWidth() + Ext.fly(lastEntry).getPadding('lr'));
}
if (inputWidth < 35) {
inputWidth = listWidth - 10;
}
if (inputWidth >= 0) {
me.setElementSize(inputEl, inputWidth);
if (owner.hasFocus) {
inputElCt.scrollIntoView(itemList);
}
}
}
}
});