]>
Commit | Line | Data |
---|---|---|
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 | |
12 | Ext.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 |