]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/form/field/Tag.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / form / field / Tag.js
CommitLineData
6527f429
DM
1/**\r
2 * `tagfield` provides a combobox that removes the hassle of dealing with long and unruly select \r
3 * options. The selected list is visually maintained in the value display area instead of \r
4 * within the picker itself. Users may easily add or remove `tags` from the \r
5 * display value area.\r
6 *\r
7 * @example\r
8 * var shows = Ext.create('Ext.data.Store', {\r
9 * fields: ['id','show'],\r
10 * data: [\r
11 * {id: 0, show: 'Battlestar Galactica'},\r
12 * {id: 1, show: 'Doctor Who'},\r
13 * {id: 2, show: 'Farscape'},\r
14 * {id: 3, show: 'Firefly'},\r
15 * {id: 4, show: 'Star Trek'},\r
16 * {id: 5, show: 'Star Wars: Christmas Special'}\r
17 * ]\r
18 * });\r
19 *\r
20 * Ext.create('Ext.form.Panel', {\r
21 * renderTo: Ext.getBody(),\r
22 * title: 'Sci-Fi Television',\r
23 * height: 200,\r
24 * width: 500,\r
25 * items: [{\r
26 * xtype: 'tagfield',\r
27 * fieldLabel: 'Select a Show',\r
28 * store: shows,\r
29 * displayField: 'show',\r
30 * valueField: 'id',\r
31 * queryMode: 'local',\r
32 * filterPickList: true\r
33 * }]\r
34 * }); \r
35 * \r
36 * ### History\r
37 *\r
38 * Inspired by the [SuperBoxSelect component for ExtJS 3](http://technomedia.co.uk/SuperBoxSelect/examples3.html),\r
39 * which in turn was inspired by the [BoxSelect component for ExtJS 2](http://efattal.fr/en/extjs/extuxboxselect/).\r
40 *\r
41 * Various contributions and suggestions made by many members of the ExtJS community which can be seen\r
42 * in the [user extension forum post](http://www.sencha.com/forum/showthread.php?134751-Ext.ux.form.field.BoxSelect).\r
43 *\r
44 * By: kvee_iv http://www.sencha.com/forum/member.php?29437-kveeiv\r
45 */\r
46Ext.define('Ext.form.field.Tag', {\r
47 extend:'Ext.form.field.ComboBox',\r
48 requires: [\r
49 'Ext.selection.Model',\r
50 'Ext.data.Store',\r
51 'Ext.data.ChainedStore'\r
52 ],\r
53\r
54 xtype: 'tagfield',\r
55\r
56 noWrap: false,\r
57\r
58 /**\r
59 * @cfg allowOnlyWhitespace\r
60 * @hide\r
61 * Currently unsupported since the value of a tagfield is an array of values and shouldn't ever be a string.\r
62 */\r
63\r
64 /**\r
65 * @cfg {String} valueParam\r
66 * The name of the parameter used to load unknown records into the store. If left unspecified, {@link #valueField}\r
67 * will be used.\r
68 */\r
69\r
70 /**\r
71 * @cfg {Boolean} multiSelect\r
72 * If set to `true`, allows the combo field to hold more than one value at a time, and allows selecting multiple\r
73 * items from the dropdown list. The combo's text field will show all selected values using the template\r
74 * defined by {@link #labelTpl}.\r
75 *\r
76 */\r
77 multiSelect: true,\r
78\r
79 /**\r
80 * @cfg {String} delimiter\r
81 * The character(s) used to separate new values to be added when {@link #createNewOnEnter}\r
82 * or {@link #createNewOnBlur} are set.\r
83 * `{@link #multiSelect} = true`.\r
84 */\r
85 delimiter: ',',\r
86\r
87 /**\r
88 * @cfg {String/Ext.XTemplate} labelTpl\r
89 * The {@link Ext.XTemplate XTemplate} to use for the inner\r
90 * markup of the labeled items. Defaults to the configured {@link #displayField}\r
91 */\r
92 \r
93 /**\r
94 * @cfg {String/Ext.XTemplate} tipTpl\r
95 * The {@link Ext.XTemplate XTemplate} to use for the tip of the labeled items. \r
96 *\r
97 * @since 5.1.1\r
98 */\r
99 tipTpl: undefined,\r
100\r
101 /**\r
102 * @cfg\r
103 * @inheritdoc\r
104 *\r
105 * When {@link #forceSelection} is `false`, new records can be created by the user as they\r
106 * are typed. These records are **not** added to the combo's store. Multiple new values\r
107 * may be added by separating them with the {@link #delimiter}, and can be further configured using the\r
108 * {@link #createNewOnEnter} and {@link #createNewOnBlur} configuration options.\r
109 *\r
110 * This functionality is primarily useful for things such as an email address.\r
111 */\r
112 forceSelection: true,\r
113\r
114 /**\r
115 * @cfg {Boolean} createNewOnEnter\r
116 * Has no effect if {@link #forceSelection} is `true`.\r
117 *\r
118 * With this set to `true`, the creation described in\r
119 * {@link #forceSelection} will also be triggered by the 'enter' key.\r
120 */\r
121 createNewOnEnter: false,\r
122\r
123 /**\r
124 * @cfg {Boolean} createNewOnBlur\r
125 * Has no effect if {@link #forceSelection} is `true`.\r
126 *\r
127 * With this set to `true`, the creation described in\r
128 * {@link #forceSelection} will also be triggered when the field loses focus.\r
129 *\r
130 * Please note that this behavior is also affected by the configuration options\r
131 * {@link #autoSelect} and {@link #selectOnTab}. If those are true and an existing\r
132 * item would have been selected as a result, the partial text the user has entered will\r
133 * be discarded and the existing item will be added to the selection.\r
134 */\r
135 createNewOnBlur: false,\r
136\r
137 /**\r
138 * @cfg {Boolean} encodeSubmitValue\r
139 * Has no effect if {@link #multiSelect} is `false`.\r
140 *\r
141 * Controls the formatting of the form submit value of the field as returned by {@link #getSubmitValue}\r
142 *\r
143 * - `true` for the field value to submit as a json encoded array in a single GET/POST variable\r
144 * - `false` for the field to submit as an array of GET/POST variables\r
145 */\r
146 encodeSubmitValue: false,\r
147\r
148 /**\r
149 * @cfg {Boolean} triggerOnClick\r
150 * `true` to activate the trigger when clicking in empty space in the field. Note that the\r
151 * subsequent behavior of this is controlled by the field's {@link #triggerAction}.\r
152 * This behavior is similar to that of a basic ComboBox with {@link #editable} `false`.\r
153 */\r
154 triggerOnClick: true,\r
155\r
156 /**\r
157 * @cfg {Boolean} stacked\r
158 * - `true` to have each selected value fill to the width of the form field\r
159 * - `false to have each selected value size to its displayed contents\r
160 */\r
161 stacked: false,\r
162\r
163 /**\r
164 * @cfg {Boolean} filterPickList\r
165 * True to hide the currently selected values from the drop down list.\r
166 *\r
167 * - `true` to hide currently selected values from the drop down pick list\r
168 * - `false` to keep the item in the pick list as a selected item\r
169 */\r
170 filterPickList: false,\r
171\r
172 /**\r
173 * @cfg {Boolean}\r
174 *\r
175 * `true` if this field should automatically grow and shrink vertically to its content.\r
176 * Note that this overrides the natural trigger grow functionality, which is used to size\r
177 * the field horizontally.\r
178 */\r
179 grow: true,\r
180\r
181 /**\r
182 * @cfg {Number/Boolean}\r
183 * Has no effect if {@link #grow} is `false`\r
184 *\r
185 * The minimum height to allow when {@link #grow} is `true`, or `false` to allow for\r
186 * natural vertical growth based on the current selected values. See also {@link #growMax}.\r
187 */\r
188 growMin: false,\r
189\r
190 /**\r
191 * @cfg {Number/Boolean}\r
192 * Has no effect if {@link #grow} is `false`\r
193 *\r
194 * The maximum height to allow when {@link #grow} is `true`, or `false` to allow for\r
195 * natural vertical growth based on the current selected values. See also {@link #growMin}.\r
196 */\r
197 growMax: false,\r
198\r
199 /**\r
200 * @cfg\r
201 * @inheritdoc\r
202 */\r
203 selectOnFocus: true,\r
204\r
205 /**\r
206 * @cfg growAppend\r
207 * @hide\r
208 * Currently unsupported since this is used for horizontal growth and this component\r
209 * only supports vertical growth.\r
210 */\r
211\r
212 /**\r
213 * @cfg growToLongestValue\r
214 * @hide\r
215 * Currently unsupported since this is used for horizontal growth and this component\r
216 * only supports vertical growth.\r
217 */\r
218\r
219 /**\r
220 * @event autosize\r
221 * Fires when the **{@link #autoSize}** function is triggered and the field is resized according to the\r
222 * {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result. This event provides a hook for the\r
223 * developer to apply additional logic at runtime to resize the field if needed.\r
224 * @param {Ext.form.field.Tag} this This field\r
225 * @param {Number} height The new field height\r
226 */\r
227\r
228 /**\r
229 * @private\r
230 * @cfg\r
231 */\r
232 fieldSubTpl: [\r
233 '<div id="{cmpId}-listWrapper" data-ref="listWrapper" class="' + Ext.baseCSSPrefix + 'tagfield {fieldCls} {typeCls} {typeCls}-{ui}" style="{wrapperStyle}">',\r
234 '<ul id="{cmpId}-itemList" data-ref="itemList" class="' + Ext.baseCSSPrefix + 'tagfield-list{itemListCls}">',\r
235 '<li id="{cmpId}-inputElCt" data-ref="inputElCt" class="' + Ext.baseCSSPrefix + 'tagfield-input">',\r
236 '<div id="{cmpId}-emptyEl" data-ref="emptyEl" class="{emptyCls}">{emptyText}</div>',\r
237 '<input id="{cmpId}-inputEl" data-ref="inputEl" type="{type}" ',\r
238 '<tpl if="name">name="{name}" </tpl>',\r
239 '<tpl if="value"> value="{[Ext.util.Format.htmlEncode(values.value)]}"</tpl>',\r
240 '<tpl if="size">size="{size}" </tpl>',\r
241 '<tpl if="tabIdx != null">tabindex="{tabIdx}" </tpl>',\r
242 '<tpl if="disabled"> disabled="disabled"</tpl>',\r
243 'class="' + Ext.baseCSSPrefix + 'tagfield-input-field {inputElCls}" autocomplete="off">',\r
244 '</li>',\r
245 '</ul>',\r
246 '</div>',\r
247 {\r
248 disableFormats: true\r
249 }\r
250 ],\r
251\r
252 extraFieldBodyCls: Ext.baseCSSPrefix + 'tagfield-body',\r
253\r
254 /**\r
255 * @private\r
256 */\r
257 childEls: [ 'listWrapper', 'itemList', 'inputEl', 'inputElCt', 'emptyEl' ],\r
258\r
259 /**\r
260 * @private\r
261 */\r
262 emptyInputCls: Ext.baseCSSPrefix + 'tagfield-emptyinput',\r
263\r
264 /**\r
265 * @private\r
266 */\r
267 clearValueOnEmpty: false,\r
268\r
269 tagItemCls: Ext.baseCSSPrefix + 'tagfield-item',\r
270 tagItemTextCls: Ext.baseCSSPrefix + 'tagfield-item-text',\r
271 tagItemCloseCls: Ext.baseCSSPrefix + 'tagfield-item-close',\r
272\r
273 tagItemSelector: '.' + Ext.baseCSSPrefix + 'tagfield-item',\r
274 tagItemCloseSelector: '.' + Ext.baseCSSPrefix + 'tagfield-item-close',\r
275 tagSelectedCls: Ext.baseCSSPrefix + 'tagfield-item-selected',\r
276\r
277 initComponent: function() {\r
278 var me = this,\r
279 typeAhead = me.typeAhead,\r
280 delimiter = me.delimiter;\r
281\r
282 // <debug>\r
283 if (typeAhead && !me.editable) {\r
284 Ext.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.');\r
285 }\r
286 // </debug>\r
287\r
288 // Allow unmatched textual values to be converted into new value records.\r
289 if (me.createNewOnEnter || me.createNewOnBlur) {\r
290 me.forceSelection = false;\r
291 }\r
292\r
293 me.typeAhead = false;\r
294 if (me.value == null) {\r
295 me.value = [];\r
296 }\r
297\r
298 // This is the selection model for selecting tags in the tag list. NOT the dropdown BoundList.\r
299 // Create the selModel before calling parent, we need it to be available\r
300 // when we bind the store.\r
301 me.selectionModel = new Ext.selection.Model({\r
302 mode: 'MULTI',\r
303 onSelectChange: function(record, isSelected, suppressEvent, commitFn) {\r
304 commitFn();\r
305 },\r
306 // Relay these selection events passing the field instead of exposing the underlying selection model\r
307 listeners: {\r
308 scope: me,\r
309 selectionchange: me.onSelectionChange,\r
310 focuschange: me.onFocusChange\r
311 }\r
312 });\r
313\r
314 me.callParent();\r
315\r
316 me.typeAhead = typeAhead;\r
317\r
318 if (delimiter && me.multiSelect) {\r
319 me.delimiterRegexp = new RegExp(Ext.String.escapeRegex(delimiter));\r
320 }\r
321 },\r
322\r
323 initEvents: function() {\r
324 var me = this,\r
325 inputEl = me.inputEl;\r
326\r
327 me.callParent(arguments);\r
328\r
329 if (!me.enableKeyEvents) {\r
330 inputEl.on('keydown', me.onKeyDown, me);\r
331 inputEl.on('keyup', me.onKeyUp, me);\r
332 }\r
333\r
334 me.listWrapper.on({\r
335 scope: me,\r
336 click: me.onItemListClick,\r
337 mousedown: me.onItemMouseDown\r
338 });\r
339 },\r
340\r
341 isValid: function() {\r
342 var me = this,\r
343 disabled = me.disabled,\r
344 validate = me.forceValidation || !disabled;\r
345\r
346 return validate ? me.validateValue(me.getValue()) : disabled;\r
347 },\r
348\r
349 onBindStore: function(store) {\r
350 var me = this;\r
351\r
352 me.callParent([store]);\r
353 if (store) {\r
354 // We collect picked records in a value store so that a selection model can track selection\r
355 me.valueStore = new Ext.data.Store({\r
356 model: store.getModel(),\r
357 // We may have the empty store here, so just ignore empty models\r
358 useModelWarning: false\r
359 });\r
360 me.selectionModel.bindStore(me.valueStore);\r
361\r
362 // Picked records disappear from the BoundList\r
363 if (me.filterPickList) {\r
364 me.listFilter = new Ext.util.Filter({\r
365 scope: me,\r
366 filterFn: me.filterPicked\r
367 });\r
368 me.changingFilters = true;\r
369 store.filter(me.listFilter);\r
370 me.changingFilters = false;\r
371 }\r
372 }\r
373 },\r
374\r
375 filterPicked: function(rec) {\r
376 return !this.valueCollection.contains(rec);\r
377 },\r
378\r
379 onUnbindStore: function(store) {\r
380 var me = this,\r
381 valueStore = me.valueStore,\r
382 picker = me.picker;\r
383\r
384 if (picker) {\r
385 picker.bindStore(null);\r
386 }\r
387\r
388 if (valueStore) {\r
389 valueStore.destroy();\r
390 me.valueStore = null;\r
391 }\r
392\r
393 if (me.filterPickList && !store.destroyed) {\r
394 me.changingFilters = true;\r
395 store.removeFilter(me.listFilter);\r
396 me.changingFilters = false;\r
397 }\r
398 me.callParent(arguments);\r
399 },\r
400\r
401 onValueCollectionEndUpdate: function() {\r
402 var me = this,\r
403 pickedRecords = me.valueCollection.items,\r
404 valueStore = me.valueStore;\r
405\r
406 if (me.isSelectionUpdating()) {\r
407 return;\r
408 }\r
409\r
410 // Ensure the source store is filtered down\r
411 if (me.filterPickList) {\r
412 me.changingFilters = true;\r
413 me.store.filter(me.listFilter);\r
414 me.changingFilters = false;\r
415 }\r
416 me.callParent();\r
417\r
418 Ext.suspendLayouts();\r
419 if (valueStore) {\r
420 valueStore.suspendEvents();\r
421 valueStore.loadRecords(pickedRecords);\r
422 valueStore.resumeEvents();\r
423 }\r
424 Ext.resumeLayouts(true);\r
425 me.alignPicker();\r
426 },\r
427\r
428 checkValueOnDataChange: Ext.emptyFn,\r
429\r
430 onSelectionChange: function(selModel, selectedRecs) {\r
431 this.applyMultiselectItemMarkup();\r
432 this.fireEvent('valueselectionchange', this, selectedRecs);\r
433 },\r
434\r
435 onFocusChange: function(selectionModel, oldFocused, newFocused) {\r
436 this.fireEvent('valuefocuschange', this, oldFocused, newFocused);\r
437 },\r
438\r
439 onDestroy: function() {\r
440 this.selectionModel = Ext.destroy(this.selectionModel);\r
441\r
442 // This will unbind the store, which will destroy the valueStore\r
443 this.callParent(arguments);\r
444 },\r
445\r
446 getSubTplData: function(fieldData) {\r
447 var me = this,\r
448 data = me.callParent(arguments),\r
449 emptyText = me.emptyText,\r
450 emptyInputCls = me.emptyInputCls,\r
451 isEmpty = emptyText && data.value.length < 1,\r
452 growMin = me.growMin,\r
453 growMax = me.growMax,\r
454 wrapperStyle = '';\r
455\r
456 data.value = '';\r
457 data.emptyText = isEmpty ? emptyText : '';\r
458 data.emptyCls = isEmpty ? me.emptyCls : emptyInputCls;\r
459 data.inputElCls = isEmpty ? emptyInputCls : '';\r
460 data.itemListCls = '';\r
461\r
462 if (me.grow) {\r
463 if (Ext.isNumber(growMin) && growMin > 0) {\r
464 wrapperStyle += 'min-height:' + growMin + 'px;';\r
465 }\r
466 if (Ext.isNumber(growMax) && growMax > 0) {\r
467 wrapperStyle += 'max-height:' + growMax + 'px;';\r
468 }\r
469 }\r
470\r
471 data.wrapperStyle = wrapperStyle;\r
472\r
473 if (me.stacked === true) {\r
474 data.itemListCls += ' ' + Ext.baseCSSPrefix + 'tagfield-stacked';\r
475 }\r
476\r
477 if (!me.multiSelect) {\r
478 data.itemListCls += ' ' + Ext.baseCSSPrefix + 'tagfield-singleselect';\r
479 }\r
480\r
481 return data;\r
482 },\r
483\r
484 afterRender: function() {\r
485 var me = this,\r
486 inputEl = me.inputEl,\r
487 emptyText = me.emptyText;\r
488\r
489 if (emptyText) {\r
490 // We remove HTML5 placeholder here because we use the emptyEl instead.\r
491 if (Ext.supports.Placeholder && inputEl) {\r
492 inputEl.dom.removeAttribute('placeholder');\r
493 } else {\r
494 me.applyEmptyText();\r
495 }\r
496 }\r
497\r
498 me.applyMultiselectItemMarkup();\r
499\r
500 me.callParent(arguments);\r
501 },\r
502\r
503 findRecord: function(field, value) {\r
504 var matches = this.getStore().queryRecords(field, value);\r
505 return matches.length ? matches[0] : false;\r
506 },\r
507\r
508 /**\r
509 * Get the current cursor position in the input field, for key-based navigation\r
510 * @private\r
511 */\r
512 getCursorPosition: function() {\r
513 var cursorPos;\r
514\r
515 if (document.selection) {\r
516 cursorPos = document.selection.createRange();\r
517 cursorPos.collapse(true);\r
518 cursorPos.moveStart('character', -this.inputEl.dom.value.length);\r
519 cursorPos = cursorPos.text.length;\r
520 } else {\r
521 cursorPos = this.inputEl.dom.selectionStart;\r
522 }\r
523 return cursorPos;\r
524 },\r
525\r
526 /**\r
527 * Check to see if the input field has selected text, for key-based navigation\r
528 * @private\r
529 */\r
530 hasSelectedText: function() {\r
531 var inputEl = this.inputEl.dom,\r
532 sel, range;\r
533\r
534 if (document.selection) {\r
535 sel = document.selection;\r
536 range = sel.createRange();\r
537 return (range.parentElement() === inputEl);\r
538 } else {\r
539 return inputEl.selectionStart !== inputEl.selectionEnd;\r
540 }\r
541 },\r
542\r
543 /**\r
544 * Handles keyDown processing of key-based selection of labeled items.\r
545 * Supported keyboard controls:\r
546 *\r
547 * - If pick list is expanded\r
548 *\r
549 * - `CTRL-A` will select all the items in the pick list\r
550 *\r
551 * - If the cursor is at the beginning of the input field and there are values present\r
552 *\r
553 * - `CTRL-A` will highlight all the currently selected values\r
554 * - `BACKSPACE` and `DELETE` will remove any currently highlighted selected values\r
555 * - `RIGHT` and `LEFT` will move the current highlight in the appropriate direction\r
556 * - `SHIFT-RIGHT` and `SHIFT-LEFT` will add to the current highlight in the appropriate direction\r
557 *\r
558 * @protected\r
559 */\r
560 onKeyDown: function(e) {\r
561 var me = this,\r
562 key = e.getKey(),\r
563 inputEl = me.inputEl,\r
564 rawValue = inputEl.dom.value,\r
565 valueCollection = me.valueCollection,\r
566 selModel = me.selectionModel,\r
567 stopEvent = false,\r
568 lastSelectionIndex;\r
569\r
570 if (me.readOnly || me.disabled || !me.editable) {\r
571 return;\r
572 }\r
573\r
574 if (valueCollection.getCount() > 0 && (rawValue === '' || (me.getCursorPosition() === 0 && !me.hasSelectedText()))) {\r
575 // Keyboard navigation of current values\r
576 lastSelectionIndex = (selModel.getCount() > 0) ? valueCollection.indexOf(selModel.getLastSelected()) : -1;\r
577\r
578 if (key === e.BACKSPACE || key === e.DELETE) {\r
579 // Delete token\r
580 if (lastSelectionIndex > -1) {\r
581 if (selModel.getCount() > 1) {\r
582 lastSelectionIndex = -1;\r
583 }\r
584 valueCollection.remove(selModel.getSelection());\r
585 } else {\r
586 valueCollection.remove(valueCollection.last());\r
587 }\r
588 selModel.clearSelections();\r
589 if (lastSelectionIndex > 0) {\r
590 selModel.select(lastSelectionIndex - 1);\r
591 } else if (valueCollection.getCount()) {\r
592 selModel.select(valueCollection.last());\r
593 }\r
594 stopEvent = true;\r
595 } else if (key === e.RIGHT || key === e.LEFT) {\r
596 // Navigate and select tokens\r
597 if (lastSelectionIndex === -1 && key === e.LEFT) {\r
598 selModel.select(valueCollection.last());\r
599 stopEvent = true;\r
600 } else if (lastSelectionIndex > -1) {\r
601 if (key === e.RIGHT) {\r
602 if (lastSelectionIndex < (valueCollection.getCount() - 1)) {\r
603 selModel.select(lastSelectionIndex + 1, e.shiftKey);\r
604 stopEvent = true;\r
605 } else if (!e.shiftKey) {\r
606 selModel.deselectAll();\r
607 stopEvent = true;\r
608 }\r
609 } else if (key === e.LEFT && (lastSelectionIndex > 0)) {\r
610 selModel.select(lastSelectionIndex - 1, e.shiftKey);\r
611 stopEvent = true;\r
612 }\r
613 }\r
614 } else if (key === e.A && e.ctrlKey) {\r
615 // Select all tokens\r
616 selModel.selectAll();\r
617 stopEvent = e.A;\r
618 }\r
619 }\r
620\r
621 if (stopEvent) {\r
622 me.preventKeyUpEvent = stopEvent;\r
623 e.stopEvent();\r
624 return;\r
625 }\r
626\r
627 // Prevent key up processing for enter if it is being handled by the picker\r
628 if (me.isExpanded && key === e.ENTER && me.picker.highlightedItem) {\r
629 me.preventKeyUpEvent = true;\r
630 }\r
631\r
632 if (me.enableKeyEvents) {\r
633 me.callParent(arguments);\r
634 }\r
635\r
636 if (!e.isSpecialKey() && !e.hasModifier()) {\r
637 selModel.deselectAll();\r
638 }\r
639 },\r
640\r
641 /**\r
642 * Handles auto-selection and creation of labeled items based on this field's\r
643 * delimiter, as well as the keyUp processing of key-based selection of labeled items.\r
644 * @protected\r
645 */\r
646 onKeyUp: function(e, t) {\r
647 var me = this,\r
648 inputEl = me.inputEl,\r
649 rawValue = inputEl.dom.value,\r
650 preventKeyUpEvent = me.preventKeyUpEvent;\r
651\r
652 if (me.preventKeyUpEvent) {\r
653 e.stopEvent();\r
654 if (preventKeyUpEvent === true || e.getKey() === preventKeyUpEvent) {\r
655 delete me.preventKeyUpEvent;\r
656 }\r
657 return;\r
658 }\r
659\r
660 if (me.multiSelect && me.delimiterRegexp && me.delimiterRegexp.test(rawValue) ||\r
661 (me.createNewOnEnter && e.getKey() === e.ENTER)) {\r
662 rawValue = Ext.Array.clean(rawValue.split(me.delimiterRegexp));\r
663 inputEl.dom.value = '';\r
664 me.setValue(me.valueStore.getRange().concat(rawValue));\r
665 inputEl.focus();\r
666 }\r
667\r
668 me.callParent([e,t]);\r
669 },\r
670\r
671 /**\r
672 * Overridden to get and set the DOM value directly for type-ahead suggestion (bypassing get/setRawValue)\r
673 * @protected\r
674 */\r
675 onTypeAhead: function() {\r
676 var me = this,\r
677 displayField = me.displayField,\r
678 inputElDom = me.inputEl.dom,\r
679 boundList = me.getPicker(),\r
680 record = me.getStore().findRecord(displayField, inputElDom.value),\r
681 newValue, len, selStart;\r
682\r
683 if (record) {\r
684 newValue = record.get(displayField);\r
685 len = newValue.length;\r
686 selStart = inputElDom.value.length;\r
687 boundList.highlightItem(boundList.getNode(record));\r
688 if (selStart !== 0 && selStart !== len) {\r
689 inputElDom.value = newValue;\r
690 me.selectText(selStart, newValue.length);\r
691 }\r
692 }\r
693 },\r
694\r
695 /**\r
696 * Delegation control for selecting and removing labeled items or triggering list collapse/expansion\r
697 * @protected\r
698 */\r
699 onItemListClick: function(e) {\r
700 var me = this,\r
701 selectionModel = me.selectionModel,\r
702 itemEl = e.getTarget(me.tagItemSelector),\r
703 closeEl = itemEl ? e.getTarget(me.tagItemCloseSelector) : false;\r
704\r
705 if (me.readOnly || me.disabled) {\r
706 return;\r
707 }\r
708\r
709 e.stopPropagation();\r
710\r
711 if (itemEl) {\r
712 if (closeEl) {\r
713 me.removeByListItemNode(itemEl);\r
714 if (me.valueStore.getCount() > 0) {\r
715 me.fireEvent('select', me, me.valueStore.getRange());\r
716 }\r
717 } else {\r
718 me.toggleSelectionByListItemNode(itemEl, e.shiftKey);\r
719 }\r
720 // If not using touch interactions, focus the input\r
721 if (!Ext.supports.TouchEvents) {\r
722 me.inputEl.focus();\r
723 }\r
724 } else {\r
725 if (selectionModel.getCount() > 0) {\r
726 selectionModel.deselectAll();\r
727 }\r
728 me.inputEl.focus();\r
729 if (me.triggerOnClick) {\r
730 me.onTriggerClick();\r
731 }\r
732 }\r
733 },\r
734\r
735 // Prevent item from receiving focus.\r
736 // See EXTJS-17686.\r
737 onItemMouseDown: function(e) {\r
738 e.preventDefault();\r
739 },\r
740\r
741 /**\r
742 * Build the markup for the labeled items. Template must be built on demand due to ComboBox initComponent\r
743 * life cycle for the creation of on-demand stores (to account for automatic valueField/displayField setting)\r
744 * @private\r
745 */\r
746 getMultiSelectItemMarkup: function() {\r
747 var me = this,\r
748 childElCls = (me._getChildElCls && me._getChildElCls()) || ''; // hook for rtl cls\r
749\r
750 if (!me.multiSelectItemTpl) {\r
751 if (!me.labelTpl) {\r
752 me.labelTpl = '{' + me.displayField + '}';\r
753 }\r
754 me.labelTpl = me.getTpl('labelTpl');\r
755\r
756 if (me.tipTpl) {\r
757 me.tipTpl = me.getTpl('tipTpl');\r
758 }\r
759\r
760 me.multiSelectItemTpl = new Ext.XTemplate([\r
761 '<tpl for=".">',\r
762 '<li data-selectionIndex="{[xindex - 1]}" data-recordId="{internalId}" class="' + me.tagItemCls + childElCls,\r
763 '<tpl if="this.isSelected(values)">',\r
764 ' ' + me.tagSelectedCls,\r
765 '</tpl>',\r
766 '{%',\r
767 'values = values.data;',\r
768 '%}',\r
769 me.tipTpl ? '" data-qtip="{[this.getTip(values)]}">' : '">',\r
770 '<div class="' + me.tagItemTextCls + '">{[this.getItemLabel(values)]}</div>',\r
771 '<div class="' + me.tagItemCloseCls + childElCls + '"></div>' ,\r
772 '</li>' ,\r
773 '</tpl>',\r
774 {\r
775 isSelected: function(rec) {\r
776 return me.selectionModel.isSelected(rec);\r
777 },\r
778 getItemLabel: function(values) {\r
779 return Ext.String.htmlEncode(me.labelTpl.apply(values));\r
780 },\r
781 getTip: function(values) {\r
782 return Ext.String.htmlEncode(me.tipTpl.apply(values));\r
783 },\r
784 strict: true\r
785 }\r
786 ]);\r
787 }\r
788 if (!me.multiSelectItemTpl.isTemplate) {\r
789 me.multiSelectItemTpl = this.getTpl('multiSelectItemTpl');\r
790 }\r
791\r
792 return me.multiSelectItemTpl.apply(me.valueCollection.getRange());\r
793 },\r
794\r
795 /**\r
796 * Update the labeled items rendering\r
797 * @private\r
798 */\r
799 applyMultiselectItemMarkup: function() {\r
800 var me = this,\r
801 itemList = me.itemList;\r
802\r
803 if (itemList) {\r
804 itemList.select('.' + Ext.baseCSSPrefix + 'tagfield-item').destroy();\r
805 me.inputElCt.insertHtml('beforeBegin', me.getMultiSelectItemMarkup());\r
806 me.autoSize();\r
807 }\r
808 },\r
809\r
810 /**\r
811 * Returns the record from valueStore for the labeled item node\r
812 */\r
813 getRecordByListItemNode: function(itemEl) {\r
814 return this.valueCollection.items[Number(itemEl.getAttribute('data-selectionIndex'))];\r
815 },\r
816\r
817 /**\r
818 * Toggle of labeled item selection by node reference\r
819 */\r
820 toggleSelectionByListItemNode: function(itemEl, keepExisting) {\r
821 var me = this,\r
822 rec = me.getRecordByListItemNode(itemEl),\r
823 selModel = me.selectionModel;\r
824\r
825 if (rec) {\r
826 if (selModel.isSelected(rec)) {\r
827 selModel.deselect(rec);\r
828 } else {\r
829 selModel.select(rec, keepExisting);\r
830 }\r
831 }\r
832 },\r
833\r
834 /**\r
835 * Removal of labelled item by node reference\r
836 */\r
837 removeByListItemNode: function(itemEl) {\r
838 var me = this,\r
839 rec = me.getRecordByListItemNode(itemEl);\r
840\r
841 if (rec) {\r
842 me.pickerSelectionModel.deselect(rec);\r
843 }\r
844 },\r
845\r
846 // Private implementation.\r
847 // The display value is always the raw value.\r
848 // Picked values are displayed by the tag template.\r
849 getDisplayValue: function() {\r
850 return this.getRawValue();\r
851 },\r
852\r
853 /**\r
854 * @inheritdoc\r
855 * Intercept calls to getRawValue to pretend there is no inputEl for rawValue handling,\r
856 * so that we can use inputEl for user input of just the current value.\r
857 */\r
858 getRawValue: function() {\r
859 var me = this,\r
860 records = me.getValueRecords(),\r
861 values = [],\r
862 i, len;\r
863\r
864 for (i = 0, len = records.length; i < len; i++) {\r
865 values.push(records[i].data[me.displayField]);\r
866 }\r
867\r
868 return values.join(',');\r
869 },\r
870\r
871 setRawValue: function(value) {\r
872 // setRawValue is not supported for tagfield.\r
873 return;\r
874 },\r
875\r
876 /**\r
877 * Removes a value or values from the current value of the field\r
878 * @param {Mixed} value The value or values to remove from the current value, see {@link #setValue}\r
879 */\r
880 removeValue: function(value) {\r
881 var me = this,\r
882 valueCollection = me.valueCollection,\r
883 len, i, item,\r
884 toRemove = [];\r
885\r
886 if (value) {\r
887 value = Ext.Array.from(value);\r
888\r
889 // Ensure that the remove values are records\r
890 for (i = 0, len = value.length; i < len; ++i) {\r
891 item = value[i];\r
892\r
893 // If a key is supplied, find the matching value record from our value collection\r
894 if (!item.isModel) {\r
895 item = valueCollection.byValue.get(item);\r
896 }\r
897 if (item) {\r
898 toRemove.push(item);\r
899 }\r
900 }\r
901 me.valueCollection.beginUpdate();\r
902 me.pickerSelectionModel.deselect(toRemove);\r
903 me.valueCollection.endUpdate();\r
904 }\r
905 },\r
906\r
907 /**\r
908 * Sets the specified value(s) into the field. The following value formats are recognized:\r
909 *\r
910 * - Single Values\r
911 *\r
912 * - A string associated to this field's configured {@link #valueField}\r
913 * - A record containing at least this field's configured {@link #valueField} and {@link #displayField}\r
914 *\r
915 * - Multiple Values\r
916 *\r
917 * - If {@link #multiSelect} is `true`, a string containing multiple strings as\r
918 * specified in the Single Values section above, concatenated in to one string\r
919 * with each entry separated by this field's configured {@link #delimiter}\r
920 * - An array of strings as specified in the Single Values section above\r
921 * - An array of records as specified in the Single Values section above\r
922 *\r
923 * In any of the string formats above, the following occurs if an associated record cannot be found:\r
924 *\r
925 * 1. If {@link #forceSelection} is `false`, a new record of the {@link #store}'s configured model type\r
926 * will be created using the given value as the {@link #displayField} and {@link #valueField}.\r
927 * This record will be added to the current value, but it will **not** be added to the store.\r
928 * 2. If {@link #forceSelection} is `true` and {@link #queryMode} is `remote`, the list of unknown\r
929 * values will be submitted as a call to the {@link #store}'s load as a parameter named by\r
930 * the {@link #valueParam} with values separated by the configured {@link #delimiter}.\r
931 * ** This process will cause setValue to asynchronously process. ** This will only be attempted\r
932 * once. Any unknown values that the server does not return records for will be removed.\r
933 * 3. Otherwise, unknown values will be removed.\r
934 *\r
935 * @param {Mixed} value The value(s) to be set, see method documentation for details\r
936 * @return {Ext.form.field.Field/Boolean} this, or `false` if asynchronously querying for unknown values\r
937 */\r
938 setValue: function(value, /* private */ add, skipLoad) {\r
939 var me = this,\r
940 valueStore = me.valueStore,\r
941 valueField = me.valueField,\r
942 unknownValues = [],\r
943 store = me.store,\r
944 autoLoadOnValue = me.autoLoadOnValue,\r
945 isLoaded = store.getCount() > 0 || store.isLoaded(),\r
946 pendingLoad = store.hasPendingLoad(),\r
947 unloaded = autoLoadOnValue && !isLoaded && !pendingLoad,\r
948 record, len, i, valueRecord, cls, params;\r
949\r
950 if (Ext.isEmpty(value)) {\r
951 value = null;\r
952 } else if (Ext.isString(value) && me.multiSelect) {\r
953 value = value.split(me.delimiter);\r
954 } else {\r
955 value = Ext.Array.from(value, true);\r
956 }\r
957\r
958 if (value && me.queryMode === 'remote' && !store.isEmptyStore && skipLoad !== true && unloaded) {\r
959 for (i = 0, len = value.length; i < len; i++) {\r
960 record = value[i];\r
961 if (!record || !record.isModel) {\r
962 valueRecord = valueStore.findExact(valueField, record);\r
963 if (valueRecord > -1) {\r
964 value[i] = valueStore.getAt(valueRecord);\r
965 } else {\r
966 valueRecord = me.findRecord(valueField, record);\r
967 if (!valueRecord) {\r
968 if (me.forceSelection) {\r
969 unknownValues.push(record);\r
970 } else {\r
971 valueRecord = {};\r
972 valueRecord[me.valueField] = record;\r
973 valueRecord[me.displayField] = record;\r
974\r
975 cls = me.valueStore.getModel();\r
976 valueRecord = new cls(valueRecord);\r
977 }\r
978 }\r
979 if (valueRecord) {\r
980 value[i] = valueRecord;\r
981 }\r
982 }\r
983 }\r
984 }\r
985\r
986 if (unknownValues.length) {\r
987 params = {};\r
988 params[me.valueParam || me.valueField] = unknownValues.join(me.delimiter);\r
989 store.load({\r
990 params: params,\r
991 callback: function() {\r
992 me.setValue(value, add, true);\r
993 me.autoSize();\r
994 me.lastQuery = false;\r
995 }\r
996 });\r
997 return false;\r
998 }\r
999 }\r
1000\r
1001 // For single-select boxes, use the last good (formal record) value if possible\r
1002 if (!me.multiSelect && value.length > 0) {\r
1003 for (i = value.length - 1; i >= 0; i--) {\r
1004 if (value[i].isModel) {\r
1005 value = value[i];\r
1006 break;\r
1007 }\r
1008 }\r
1009 if (Ext.isArray(value)) {\r
1010 value = value[value.length - 1];\r
1011 }\r
1012 }\r
1013\r
1014 return me.callParent([value, add]);\r
1015 },\r
1016\r
1017 // Private internal setting of value when records are added to the valueCollection\r
1018 // setValue itself adds to the valueCollection.\r
1019 updateValue: function() {\r
1020 var me = this,\r
1021 valueArray = me.valueCollection.getRange(),\r
1022 len = valueArray.length,\r
1023 i;\r
1024\r
1025 for (i = 0; i < len; i++) {\r
1026 valueArray[i] = valueArray[i].get(me.valueField);\r
1027 }\r
1028\r
1029 // Set the value of this field. If we are multi-selecting, then that is an array.\r
1030 me.setHiddenValue(valueArray);\r
1031 me.value = me.multiSelect ? valueArray : valueArray[0];\r
1032 if (!Ext.isDefined(me.value)) {\r
1033 me.value = undefined;\r
1034 }\r
1035\r
1036 me.applyMultiselectItemMarkup();\r
1037 me.checkChange();\r
1038 me.applyEmptyText();\r
1039 },\r
1040\r
1041 /**\r
1042 * Returns the records for the field's current value\r
1043 * @return {Array} The records for the field's current value\r
1044 */\r
1045 getValueRecords: function() {\r
1046 return this.valueCollection.getRange();\r
1047 },\r
1048\r
1049 /**\r
1050 * @inheritdoc\r
1051 * Overridden to optionally allow for submitting the field as a json encoded array.\r
1052 */\r
1053 getSubmitData: function() {\r
1054 var me = this,\r
1055 val = me.callParent(arguments);\r
1056\r
1057 if (me.multiSelect && me.encodeSubmitValue && val && val[me.name]) {\r
1058 val[me.name] = Ext.encode(val[me.name]);\r
1059 }\r
1060\r
1061 return val;\r
1062 },\r
1063\r
1064 /**\r
1065 * Overridden to handle partial-input selections more directly\r
1066 */\r
1067 assertValue: function() {\r
1068 var me = this,\r
1069 rawValue = me.inputEl.dom.value,\r
1070 rec = !Ext.isEmpty(rawValue) ? me.findRecordByDisplay(rawValue) : false,\r
1071 value = false;\r
1072\r
1073 if (!rec && !me.forceSelection && me.createNewOnBlur && !Ext.isEmpty(rawValue)) {\r
1074 value = rawValue;\r
1075 } else if (rec) {\r
1076 value = rec;\r
1077 }\r
1078\r
1079 if (value) {\r
1080 me.addValue(value);\r
1081 }\r
1082\r
1083 me.inputEl.dom.value = '';\r
1084\r
1085 me.collapse();\r
1086 },\r
1087\r
1088 /**\r
1089 * Overridden to be more accepting of varied value types\r
1090 */\r
1091 isEqual: function(v1, v2) {\r
1092 var fromArray = Ext.Array.from,\r
1093 valueField = this.valueField,\r
1094 i, len, t1, t2;\r
1095\r
1096 v1 = fromArray(v1);\r
1097 v2 = fromArray(v2);\r
1098 len = v1.length;\r
1099\r
1100 if (len !== v2.length) {\r
1101 return false;\r
1102 }\r
1103\r
1104 for(i = 0; i < len; i++) {\r
1105 t1 = v1[i].isModel ? v1[i].get(valueField) : v1[i];\r
1106 t2 = v2[i].isModel ? v2[i].get(valueField) : v2[i];\r
1107 if (t1 !== t2) {\r
1108 return false;\r
1109 }\r
1110 }\r
1111\r
1112 return true;\r
1113 },\r
1114\r
1115 /**\r
1116 * Overridden to use value (selection) instead of raw value and to avoid the use of placeholder\r
1117 */\r
1118 applyEmptyText : function() {\r
1119 var me = this,\r
1120 emptyText = me.emptyText,\r
1121 emptyEl = me.emptyEl,\r
1122 inputEl = me.inputEl,\r
1123 listWrapper = me.listWrapper,\r
1124 emptyCls = me.emptyCls,\r
1125 emptyInputCls = me.emptyInputCls,\r
1126 isEmpty;\r
1127\r
1128 if (me.rendered && emptyText) {\r
1129 isEmpty = Ext.isEmpty(me.value) && !me.hasFocus;\r
1130\r
1131 if (isEmpty) {\r
1132 inputEl.dom.value = '';\r
1133 emptyEl.setHtml(emptyText);\r
1134 emptyEl.addCls(emptyCls);\r
1135 emptyEl.removeCls(emptyInputCls);\r
1136 listWrapper.addCls(emptyCls);\r
1137 inputEl.addCls(emptyInputCls);\r
1138 } else {\r
1139 emptyEl.addCls(emptyInputCls);\r
1140 emptyEl.removeCls(emptyCls);\r
1141 listWrapper.removeCls(emptyCls);\r
1142 inputEl.removeCls(emptyInputCls);\r
1143 }\r
1144 me.autoSize();\r
1145 }\r
1146 },\r
1147\r
1148 /**\r
1149 * Overridden to use inputEl instead of raw value and to avoid the use of placeholder\r
1150 */\r
1151 preFocus : function(){\r
1152 var me = this,\r
1153 inputEl = me.inputEl,\r
1154 isEmpty = inputEl.dom.value === '';\r
1155\r
1156 me.emptyEl.addCls(me.emptyInputCls);\r
1157 me.emptyEl.removeCls(me.emptyCls);\r
1158 me.listWrapper.removeCls(me.emptyCls);\r
1159 me.inputEl.removeCls(me.emptyInputCls);\r
1160\r
1161 if (me.selectOnFocus || isEmpty) {\r
1162 inputEl.dom.select();\r
1163 }\r
1164 },\r
1165\r
1166 /**\r
1167 * Intercept calls to onFocus to add focusCls, because the base field\r
1168 * classes assume this should be applied to inputEl\r
1169 */\r
1170 onFocus: function() {\r
1171 var me = this,\r
1172 focusCls = me.focusCls,\r
1173 itemList = me.itemList;\r
1174\r
1175 if (focusCls && itemList) {\r
1176 itemList.addCls(focusCls);\r
1177 }\r
1178\r
1179 me.callParent(arguments);\r
1180 },\r
1181\r
1182 /**\r
1183 * Intercept calls to onBlur to remove focusCls, because the base field\r
1184 * classes assume this should be applied to inputEl\r
1185 */\r
1186 onBlur: function() {\r
1187 var me = this,\r
1188 focusCls = me.focusCls,\r
1189 itemList = me.itemList;\r
1190\r
1191 if (focusCls && itemList) {\r
1192 itemList.removeCls(focusCls);\r
1193 }\r
1194\r
1195 me.callParent(arguments);\r
1196 },\r
1197\r
1198 /**\r
1199 * Intercept calls to renderActiveError to add invalidCls, because the base\r
1200 * field classes assume this should be applied to inputEl\r
1201 */\r
1202 renderActiveError: function() {\r
1203 var me = this,\r
1204 invalidCls = me.invalidCls,\r
1205 itemList = me.itemList,\r
1206 hasError = me.hasActiveError();\r
1207\r
1208 if (invalidCls && itemList) {\r
1209 itemList[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field');\r
1210 }\r
1211\r
1212 me.callParent(arguments);\r
1213 },\r
1214\r
1215 /**\r
1216 * Initiate auto-sizing for height based on {@link #grow}, if applicable.\r
1217 */\r
1218 autoSize: function() {\r
1219 var me = this;\r
1220\r
1221 if (me.grow && me.rendered) {\r
1222 me.autoSizing = true;\r
1223 me.updateLayout();\r
1224 }\r
1225\r
1226 return me;\r
1227 },\r
1228\r
1229 /**\r
1230 * Track height change to fire {@link #event-autosize} event, when applicable.\r
1231 */\r
1232 afterComponentLayout: function() {\r
1233 var me = this,\r
1234 height;\r
1235\r
1236 if (me.autoSizing) {\r
1237 height = me.getHeight();\r
1238 if (height !== me.lastInputHeight) {\r
1239 if (me.isExpanded) {\r
1240 me.alignPicker();\r
1241 }\r
1242 me.fireEvent('autosize', me, height);\r
1243 me.lastInputHeight = height;\r
1244 me.autoSizing = false;\r
1245 }\r
1246 }\r
1247 }\r
1248});\r