]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/form/field/Picker.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / form / field / Picker.js
CommitLineData
6527f429
DM
1/**\r
2 * An abstract class for fields that have a single trigger which opens a "picker" popup below the field, e.g. a combobox\r
3 * menu list or a date picker. It provides a base implementation for toggling the picker's visibility when the trigger\r
4 * is clicked, as well as keyboard navigation and some basic events. Sizing and alignment of the picker can be\r
5 * controlled via the {@link #matchFieldWidth} and {@link #pickerAlign}/{@link #pickerOffset} config properties\r
6 * respectively.\r
7 *\r
8 * You would not normally use this class directly, but instead use it as the parent class for a specific picker field\r
9 * implementation. Subclasses must implement the {@link #createPicker} method to create a picker component appropriate\r
10 * for the field.\r
11 */\r
12Ext.define('Ext.form.field.Picker', {\r
13 extend: 'Ext.form.field.Text',\r
14 alias: 'widget.pickerfield',\r
15 alternateClassName: 'Ext.form.Picker',\r
16 requires: ['Ext.util.KeyNav'],\r
17\r
18 config: {\r
19 triggers: {\r
20 picker: {\r
21 handler: 'onTriggerClick',\r
22 scope: 'this'\r
23 }\r
24 }\r
25 },\r
26 \r
27 renderConfig: {\r
28 /**\r
29 * @cfg {Boolean} editable\r
30 * False to prevent the user from typing text directly into the field; the field can only have its value set via\r
31 * selecting a value from the picker. In this state, the picker can also be opened by clicking directly on the input\r
32 * field itself.\r
33 */\r
34 editable: true \r
35 },\r
36\r
37 /**\r
38 * @property {Boolean} isPickerField\r
39 * `true` in this class to identify an object as an instantiated Picker Field, or subclass thereof.\r
40 */\r
41 isPickerField: true,\r
42\r
43 /**\r
44 * @cfg {Boolean} matchFieldWidth\r
45 * Whether the picker dropdown's width should be explicitly set to match the width of the field. Defaults to true.\r
46 */\r
47 matchFieldWidth: true,\r
48\r
49 /**\r
50 * @cfg {String} pickerAlign\r
51 * The {@link Ext.util.Positionable#alignTo alignment position} with which to align the picker. Defaults to "tl-bl?"\r
52 */\r
53 pickerAlign: 'tl-bl?',\r
54\r
55 /**\r
56 * @cfg {Number[]} pickerOffset\r
57 * An offset [x,y] to use in addition to the {@link #pickerAlign} when positioning the picker.\r
58 * Defaults to undefined.\r
59 */\r
60\r
61 /**\r
62 * @cfg {String} [openCls='x-pickerfield-open']\r
63 * A class to be added to the field's {@link #bodyEl} element when the picker is opened.\r
64 */\r
65 openCls: Ext.baseCSSPrefix + 'pickerfield-open',\r
66\r
67 /**\r
68 * @property {Boolean} isExpanded\r
69 * True if the picker is currently expanded, false if not.\r
70 */\r
71 isExpanded: false,\r
72\r
73 /**\r
74 * @cfg {String} triggerCls\r
75 * An additional CSS class used to style the trigger button. The trigger will always\r
76 * get the class 'x-form-trigger' and triggerCls will be appended if specified.\r
77 */\r
78\r
79 /**\r
80 * @event expand\r
81 * Fires when the field's picker is expanded.\r
82 * @param {Ext.form.field.Picker} field This field instance\r
83 */\r
84\r
85 /**\r
86 * @event collapse\r
87 * Fires when the field's picker is collapsed.\r
88 * @param {Ext.form.field.Picker} field This field instance\r
89 */\r
90\r
91 /**\r
92 * @event select\r
93 * Fires when a value is selected via the picker.\r
94 * @param {Ext.form.field.Picker} field This field instance\r
95 * @param {Object} value The value that was selected. The exact type of this value is dependent on\r
96 * the individual field and picker implementations.\r
97 */\r
98\r
99 applyTriggers: function(triggers) {\r
100 var me = this,\r
101 picker = triggers.picker;\r
102\r
103 if (!picker.cls) {\r
104 picker.cls = me.triggerCls;\r
105 }\r
106\r
107 return me.callParent([triggers]);\r
108 },\r
109 \r
110 getSubTplData: function(fieldData) {\r
111 var me = this,\r
112 data, inputElAttr;\r
113 \r
114 data = me.callParent([fieldData]);\r
115 \r
116 if (me.ariaRole) {\r
117 inputElAttr = data.inputElAriaAttributes;\r
118 \r
119 if (inputElAttr) {\r
120 inputElAttr['aria-haspopup'] = true;\r
121 \r
122 // Picker fields start as collapsed\r
123 inputElAttr['aria-expanded'] = false;\r
124 }\r
125 }\r
126 \r
127 return data;\r
128 },\r
129\r
130 initEvents: function() {\r
131 var me = this;\r
132 me.callParent();\r
133\r
134 // Add handlers for keys to expand/collapse the picker\r
135 me.keyNav = new Ext.util.KeyNav(me.inputEl, {\r
136 down: me.onDownArrow,\r
137 esc: {\r
138 handler: me.onEsc,\r
139 scope: me,\r
140 defaultEventAction: false\r
141 },\r
142 scope: me,\r
143 forceKeyDown: true\r
144 });\r
145\r
146 // Disable native browser autocomplete\r
147 if (Ext.isGecko) {\r
148 me.inputEl.dom.setAttribute('autocomplete', 'off');\r
149 }\r
150 },\r
151\r
152 updateEditable: function(editable, oldEditable) {\r
153 var me = this;\r
154\r
155 // Non-editable allows opening the picker by clicking the field\r
156 if (!editable) {\r
157 me.inputEl.on('click', me.onTriggerClick, me);\r
158 } else {\r
159 me.inputEl.un('click', me.onTriggerClick, me);\r
160 }\r
161 me.callParent([editable, oldEditable]);\r
162 },\r
163\r
164 /**\r
165 * @private\r
166 */\r
167 onEsc: function(e) {\r
168 if (Ext.isIE) {\r
169 // Stop the esc key from "restoring" the previous value in IE\r
170 // For example, type "foo". Highlight all the text, hit backspace.\r
171 // Hit esc, "foo" will be restored. This behaviour doesn't occur\r
172 // in any other browsers\r
173 e.preventDefault();\r
174 }\r
175\r
176 if (this.isExpanded) {\r
177 this.collapse();\r
178 e.stopEvent();\r
179 }\r
180 },\r
181\r
182 onDownArrow: function(e) {\r
183 var me = this;\r
184 \r
185 if ((e.time - me.lastDownArrow) > 150) {\r
186 delete me.lastDownArrow;\r
187 }\r
188 \r
189 if (!me.isExpanded) {\r
190 // Do not let the down arrow event propagate into the picker\r
191 e.stopEvent();\r
192\r
193 // Don't call expand() directly as there may be additional processing involved before\r
194 // expanding, e.g. in the case of a ComboBox query.\r
195 me.onTriggerClick();\r
196 \r
197 me.lastDownArrow = e.time;\r
198 }\r
199 else if (!e.isStopped && (e.time - me.lastDownArrow) < 150) {\r
200 delete me.lastDownArrow;\r
201 }\r
202 },\r
203\r
204 /**\r
205 * Expands this field's picker dropdown.\r
206 */\r
207 expand: function() {\r
208 var me = this,\r
209 bodyEl, ariaDom, picker, doc;\r
210\r
211 if (me.rendered && !me.isExpanded && !me.destroyed) {\r
212 bodyEl = me.bodyEl;\r
213 picker = me.getPicker();\r
214 doc = Ext.getDoc();\r
215 picker.setMaxHeight(picker.initialConfig.maxHeight);\r
216 \r
217 if (me.matchFieldWidth) {\r
218 picker.setWidth(me.bodyEl.getWidth());\r
219 }\r
220\r
221 // Show the picker and set isExpanded flag. alignPicker only works if isExpanded.\r
222 picker.show();\r
223 me.isExpanded = true;\r
224 me.alignPicker();\r
225 bodyEl.addCls(me.openCls);\r
226 \r
227 if (me.ariaRole) {\r
228 ariaDom = me.ariaEl.dom;\r
229 \r
230 ariaDom.setAttribute('aria-owns', picker.listEl ? picker.listEl.id : picker.el.id);\r
231 ariaDom.setAttribute('aria-expanded', true);\r
232 }\r
233\r
234 // Collapse on touch outside this component tree.\r
235 // Because touch platforms do not focus document.body on touch\r
236 // so no focusleave would occur to trigger a collapse.\r
237 me.touchListeners = doc.on({\r
238 // Do not translate on non-touch platforms.\r
239 // mousedown will blur the field.\r
240 translate:false,\r
241 touchstart: me.collapseIf,\r
242 scope: me,\r
243 delegated: false,\r
244 destroyable: true\r
245 });\r
246\r
247 // Scrolling of anything which causes this field to move should collapse\r
248 me.scrollListeners = Ext.on({\r
249 scroll: me.onGlobalScroll,\r
250 scope: me,\r
251 destroyable: true\r
252 });\r
253 \r
254 // Buffer is used to allow any layouts to complete before we align\r
255 Ext.on('resize', me.alignPicker, me, {buffer: 1});\r
256 me.fireEvent('expand', me);\r
257 me.onExpand();\r
258 }\r
259 },\r
260\r
261 onExpand: Ext.emptyFn,\r
262\r
263 /**\r
264 * Aligns the picker to the input element\r
265 * @protected\r
266 */\r
267 alignPicker: function() {\r
268 if (!this.destroyed) {\r
269 var picker = this.getPicker();\r
270\r
271 if (picker.isVisible() && picker.isFloating()) {\r
272 this.doAlign();\r
273 }\r
274 }\r
275 },\r
276\r
277 /**\r
278 * Performs the alignment on the picker using the class defaults\r
279 * @private\r
280 */\r
281 doAlign: function(){\r
282 var me = this,\r
283 picker = me.picker,\r
284 aboveSfx = '-above',\r
285 isAbove;\r
286\r
287 // Align to the trigger wrap because the border isn't always on the input element, which\r
288 // can cause the offset to be off\r
289 me.picker.alignTo(me.triggerWrap, me.pickerAlign, me.pickerOffset);\r
290 // add the {openCls}-above class if the picker was aligned above\r
291 // the field due to hitting the bottom of the viewport\r
292 isAbove = picker.el.getY() < me.inputEl.getY();\r
293 me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx);\r
294 picker[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx);\r
295 },\r
296\r
297 /**\r
298 * Collapses this field's picker dropdown.\r
299 */\r
300 collapse: function() {\r
301 var me = this;\r
302 \r
303 if (me.isExpanded && !me.destroyed && !me.destroying) {\r
304 var openCls = me.openCls,\r
305 picker = me.picker,\r
306 aboveSfx = '-above';\r
307\r
308 // hide the picker and set isExpanded flag\r
309 picker.hide();\r
310 me.isExpanded = false;\r
311\r
312 // remove the openCls\r
313 me.bodyEl.removeCls([openCls, openCls + aboveSfx]);\r
314 picker.el.removeCls(picker.baseCls + aboveSfx);\r
315 \r
316 if (me.ariaRole) {\r
317 me.ariaEl.dom.setAttribute('aria-expanded', false);\r
318 }\r
319\r
320 // remove event listeners\r
321 me.touchListeners.destroy();\r
322 me.scrollListeners.destroy();\r
323 Ext.un('resize', me.alignPicker, me);\r
324 me.fireEvent('collapse', me);\r
325 me.onCollapse();\r
326 }\r
327 },\r
328\r
329 onCollapse: Ext.emptyFn,\r
330\r
331 /**\r
332 * @private\r
333 * Runs on touchstart of doc to check to see if we should collapse the picker.\r
334 */\r
335 collapseIf: function(e) {\r
336 var me = this;\r
337\r
338 // If what was mousedowned on is outside of this Field, and is not focusable, then collapse.\r
339 // If it is focusable, this Field will blur and collapse anyway.\r
340 if (!me.destroyed && !e.within(me.bodyEl, false, true) && !me.owns(e.target) && !Ext.fly(e.target).isFocusable()) {\r
341 me.collapse();\r
342 }\r
343 },\r
344\r
345 /**\r
346 * Returns a reference to the picker component for this field, creating it if necessary by\r
347 * calling {@link #createPicker}.\r
348 * @return {Ext.Component} The picker component\r
349 */\r
350 getPicker: function() {\r
351 var me = this,\r
352 picker = me.picker;\r
353\r
354 if (!picker) {\r
355 me.creatingPicker = true;\r
356 me.picker = picker = me.createPicker();\r
357 // For upward component searches.\r
358 picker.ownerCmp = me;\r
359 delete me.creatingPicker;\r
360 }\r
361\r
362 return me.picker;\r
363 },\r
364\r
365 // When focus leaves the picker component, if it's to outside of this\r
366 // Component's hierarchy\r
367 onFocusLeave: function(e) {\r
368 this.collapse();\r
369 this.callParent([e]);\r
370 },\r
371\r
372 /**\r
373 * @private\r
374 * The CQ interface. Allow drilling down into the picker when it exists.\r
375 * Important for determining whether an event took place in the bounds of some\r
376 * higher level containing component. See AbstractComponent#owns\r
377 */\r
378 getRefItems: function() {\r
379 var result = [];\r
380 if (this.picker) {\r
381 result[0] = this.picker;\r
382 }\r
383 return result;\r
384 },\r
385\r
386 /**\r
387 * @method\r
388 * Creates and returns the component to be used as this field's picker. Must be implemented by subclasses of Picker.\r
389 */\r
390 createPicker: Ext.emptyFn,\r
391\r
392 /**\r
393 * Handles the trigger click; by default toggles between expanding and collapsing the picker component.\r
394 * @protected\r
395 */\r
396 onTriggerClick: function(e) {\r
397 var me = this;\r
398 if (!me.readOnly && !me.disabled) {\r
399 if (me.isExpanded) {\r
400 me.collapse();\r
401 } else {\r
402 me.expand();\r
403 }\r
404 }\r
405 },\r
406\r
407 beforeDestroy : function(){\r
408 var me = this,\r
409 picker = me.picker;\r
410\r
411 me.callParent();\r
412 Ext.un('resize', me.alignPicker, me);\r
413 Ext.destroy(me.keyNav, picker);\r
414 if (picker) {\r
415 me.picker = picker.pickerField = null;\r
416 }\r
417 },\r
418\r
419 privates: {\r
420 onGlobalScroll: function (scroller) {\r
421 var scrollPosition,\r
422 newScrollPosition,\r
423 targetEl = this.el;\r
424\r
425 // Collapse if this field being moved by the scroll the scroll\r
426 if (scroller.getElement().contains(targetEl)) {\r
427 scrollPosition = scroller.getPosition();\r
428 newScrollPosition = targetEl.getScrollIntoViewXY(scroller.getElement(), scrollPosition.x, scrollPosition.y);\r
429\r
430 // If this field is part of a fixed position component, or\r
431 // this field is out of the scroller element's view in any way, collapse\r
432 if (this.up('[fixed]') || newScrollPosition.y !== scrollPosition.y || newScrollPosition.x !== scrollPosition.x) {\r
433 this.collapse();\r
434 }\r
435 }\r
436 }\r
437 }\r
438});\r