1 | define([ |
---|
2 | "dojo/_base/kernel", |
---|
3 | "dojo/_base/declare", |
---|
4 | "dojo/_base/lang", |
---|
5 | "dojo/_base/window", |
---|
6 | "dojo/dom-geometry", |
---|
7 | "dojo/dom-style", |
---|
8 | "dojo/dom-attr", |
---|
9 | "dojo/window", |
---|
10 | "dojo/touch", |
---|
11 | "dijit/form/_AutoCompleterMixin", |
---|
12 | "dijit/popup", |
---|
13 | "./_ComboBoxMenu", |
---|
14 | "./TextBox", |
---|
15 | "./sniff" |
---|
16 | ], function(kernel, declare, lang, win, domGeometry, domStyle, domAttr, windowUtils, touch, AutoCompleterMixin, popup, ComboBoxMenu, TextBox, has){ |
---|
17 | kernel.experimental("dojox.mobile.ComboBox"); // should be using a more native search-type UI |
---|
18 | |
---|
19 | return declare("dojox.mobile.ComboBox", [TextBox, AutoCompleterMixin], { |
---|
20 | // summary: |
---|
21 | // A non-templated auto-completing text box widget. |
---|
22 | |
---|
23 | // dropDownClass: [protected extension] String |
---|
24 | // Name of the drop-down widget class used to select a date/time. |
---|
25 | // Should be specified by subclasses. |
---|
26 | dropDownClass: "dojox.mobile._ComboBoxMenu", |
---|
27 | |
---|
28 | // initially disable selection since iphone displays selection handles |
---|
29 | // that makes it hard to pick from the list |
---|
30 | |
---|
31 | // selectOnClick: Boolean |
---|
32 | // Flag which enables the selection on click. |
---|
33 | selectOnClick: false, |
---|
34 | |
---|
35 | // autoComplete: Boolean |
---|
36 | // Flag which enables the auto-completion. |
---|
37 | autoComplete: false, |
---|
38 | |
---|
39 | // dropDown: [protected] Widget |
---|
40 | // The widget to display as a popup. This widget *must* be |
---|
41 | // defined before the startup function is called. |
---|
42 | dropDown: null, |
---|
43 | |
---|
44 | // maxHeight: [protected] int |
---|
45 | // The maximum height for the drop-down. |
---|
46 | // Any drop-down taller than this value will have scrollbars. |
---|
47 | // Set to -1 to limit the height to the available space in the viewport. |
---|
48 | maxHeight: -1, |
---|
49 | |
---|
50 | // dropDownPosition: [const] String[] |
---|
51 | // This variable controls the position of the drop-down. |
---|
52 | // It is an array of strings with the following values: |
---|
53 | // |
---|
54 | // - before: places drop down to the left of the target node/widget, or to the right in |
---|
55 | // the case of RTL scripts like Hebrew and Arabic |
---|
56 | // - after: places drop down to the right of the target node/widget, or to the left in |
---|
57 | // the case of RTL scripts like Hebrew and Arabic |
---|
58 | // - above: drop down goes above target node |
---|
59 | // - below: drop down goes below target node |
---|
60 | // |
---|
61 | // The list is positions is tried, in order, until a position is found where the drop down fits |
---|
62 | // within the viewport. |
---|
63 | dropDownPosition: ["below","above"], |
---|
64 | |
---|
65 | _throttleOpenClose: function(){ |
---|
66 | // summary: |
---|
67 | // Prevents the open/close in rapid succession. |
---|
68 | // tags: |
---|
69 | // private |
---|
70 | if(this._throttleHandler){ |
---|
71 | this._throttleHandler.remove(); |
---|
72 | } |
---|
73 | this._throttleHandler = this.defer(function(){ this._throttleHandler = null; }, 500); |
---|
74 | }, |
---|
75 | |
---|
76 | _onFocus: function(){ |
---|
77 | // summary: |
---|
78 | // Shows drop-down if the user is selecting Next/Previous from the virtual keyboard. |
---|
79 | // tags: |
---|
80 | // private |
---|
81 | this.inherited(arguments); |
---|
82 | if(!this._opened && !this._throttleHandler){ |
---|
83 | this._startSearchAll(); |
---|
84 | } |
---|
85 | |
---|
86 | if(has("windows-theme")) { |
---|
87 | this.domNode.blur(); |
---|
88 | } |
---|
89 | }, |
---|
90 | |
---|
91 | onInput: function(e){ |
---|
92 | this._onKey(e); |
---|
93 | this.inherited(arguments); |
---|
94 | }, |
---|
95 | |
---|
96 | _setListAttr: function(v){ |
---|
97 | // tags: |
---|
98 | // private |
---|
99 | this._set('list', v); // needed for Firefox 4+ to prevent HTML5 mode |
---|
100 | }, |
---|
101 | |
---|
102 | closeDropDown: function(){ |
---|
103 | // summary: |
---|
104 | // Closes the drop down on this widget |
---|
105 | // tags: |
---|
106 | // protected |
---|
107 | |
---|
108 | this._throttleOpenClose(); |
---|
109 | if(this.endHandler){ |
---|
110 | this.disconnect(this.startHandler); |
---|
111 | this.disconnect(this.endHandler); |
---|
112 | this.disconnect(this.moveHandler); |
---|
113 | clearInterval(this.repositionTimer); |
---|
114 | this.repositionTimer = this.endHandler = null; |
---|
115 | } |
---|
116 | this.inherited(arguments); |
---|
117 | domAttr.remove(this.domNode, "aria-owns"); |
---|
118 | domAttr.set(this.domNode, "aria-expanded", "false"); |
---|
119 | popup.close(this.dropDown); |
---|
120 | this._opened = false; |
---|
121 | |
---|
122 | // Remove disable attribute to make input element clickable after context menu closed |
---|
123 | if(has("windows-theme") && this.domNode.disabled){ |
---|
124 | this.defer(function(){ |
---|
125 | this.domNode.removeAttribute("disabled"); |
---|
126 | }, 300); |
---|
127 | } |
---|
128 | |
---|
129 | }, |
---|
130 | |
---|
131 | openDropDown: function(){ |
---|
132 | // summary: |
---|
133 | // Opens the dropdown for this widget. To be called only when this.dropDown |
---|
134 | // has been created and is ready to display (that is, its data is loaded). |
---|
135 | // returns: |
---|
136 | // Returns the value of popup.open(). |
---|
137 | // tags: |
---|
138 | // protected |
---|
139 | |
---|
140 | var wasClosed = !this._opened; |
---|
141 | var dropDown = this.dropDown, |
---|
142 | ddNode = dropDown.domNode, |
---|
143 | aroundNode = this.domNode, |
---|
144 | self = this; |
---|
145 | |
---|
146 | domAttr.set(dropDown.domNode, "role", "listbox"); |
---|
147 | domAttr.set(this.domNode, "aria-expanded", "true"); |
---|
148 | if(dropDown.id){ |
---|
149 | domAttr.set(this.domNode, "aria-owns", dropDown.id); |
---|
150 | } |
---|
151 | |
---|
152 | if(has('touch')){ |
---|
153 | win.global.scrollBy(0, domGeometry.position(aroundNode, false).y); // don't call scrollIntoView since it messes up ScrollableView |
---|
154 | } |
---|
155 | |
---|
156 | // TODO: isn't maxHeight dependent on the return value from popup.open(), |
---|
157 | // i.e., dependent on how much space is available (BK) |
---|
158 | |
---|
159 | if(!this._preparedNode){ |
---|
160 | this._preparedNode = true; |
---|
161 | // Check if we have explicitly set width and height on the dropdown widget dom node |
---|
162 | if(ddNode.style.width){ |
---|
163 | this._explicitDDWidth = true; |
---|
164 | } |
---|
165 | if(ddNode.style.height){ |
---|
166 | this._explicitDDHeight = true; |
---|
167 | } |
---|
168 | } |
---|
169 | |
---|
170 | // Code for resizing dropdown (height limitation, or increasing width to match my width) |
---|
171 | var myStyle = { |
---|
172 | display: "", |
---|
173 | overflow: "hidden", |
---|
174 | visibility: "hidden" |
---|
175 | }; |
---|
176 | if(!this._explicitDDWidth){ |
---|
177 | myStyle.width = ""; |
---|
178 | } |
---|
179 | if(!this._explicitDDHeight){ |
---|
180 | myStyle.height = ""; |
---|
181 | } |
---|
182 | domStyle.set(ddNode, myStyle); |
---|
183 | |
---|
184 | // Figure out maximum height allowed (if there is a height restriction) |
---|
185 | var maxHeight = this.maxHeight; |
---|
186 | if(maxHeight == -1){ |
---|
187 | // limit height to space available in viewport either above or below my domNode |
---|
188 | // (whichever side has more room) |
---|
189 | var viewport = windowUtils.getBox(), |
---|
190 | position = domGeometry.position(aroundNode, false); |
---|
191 | maxHeight = Math.floor(Math.max(position.y, viewport.h - (position.y + position.h))); |
---|
192 | } |
---|
193 | |
---|
194 | // Attach dropDown to DOM and make make visibility:hidden rather than display:none |
---|
195 | // so we call startup() and also get the size |
---|
196 | popup.moveOffScreen(dropDown); |
---|
197 | |
---|
198 | if(dropDown.startup && !dropDown._started){ |
---|
199 | dropDown.startup(); // this has to be done after being added to the DOM |
---|
200 | } |
---|
201 | // Get size of drop down, and determine if vertical scroll bar needed |
---|
202 | var mb = domGeometry.position(this.dropDown.containerNode, false); |
---|
203 | var overHeight = (maxHeight && mb.h > maxHeight); |
---|
204 | if(overHeight){ |
---|
205 | mb.h = maxHeight; |
---|
206 | } |
---|
207 | |
---|
208 | // Adjust dropdown width to match or be larger than my width |
---|
209 | mb.w = Math.max(mb.w, aroundNode.offsetWidth); |
---|
210 | domGeometry.setMarginBox(ddNode, mb); |
---|
211 | |
---|
212 | var retVal = popup.open({ |
---|
213 | parent: this, |
---|
214 | popup: dropDown, |
---|
215 | around: aroundNode, |
---|
216 | orient: has("windows-theme") ? ["above"] : this.dropDownPosition, |
---|
217 | onExecute: function(){ |
---|
218 | self.closeDropDown(); |
---|
219 | }, |
---|
220 | onCancel: function(){ |
---|
221 | self.closeDropDown(); |
---|
222 | }, |
---|
223 | onClose: function(){ |
---|
224 | self._opened = false; |
---|
225 | } |
---|
226 | }); |
---|
227 | this._opened=true; |
---|
228 | |
---|
229 | if(wasClosed){ |
---|
230 | var isGesture = false, |
---|
231 | skipReposition = false, |
---|
232 | active = false, |
---|
233 | wrapper = dropDown.domNode.parentNode, |
---|
234 | aroundNodePos = domGeometry.position(aroundNode, false), |
---|
235 | popupPos = domGeometry.position(wrapper, false), |
---|
236 | deltaX = popupPos.x - aroundNodePos.x, |
---|
237 | deltaY = popupPos.y - aroundNodePos.y, |
---|
238 | startX = -1, startY = -1; |
---|
239 | |
---|
240 | // touchstart isn't really needed since touchmove implies touchstart, but |
---|
241 | // mousedown is needed since mousemove doesn't know if the left button is down or not |
---|
242 | this.startHandler = this.connect(win.doc.documentElement, touch.press, |
---|
243 | function(e){ |
---|
244 | skipReposition = true; |
---|
245 | active = true; |
---|
246 | isGesture = false; |
---|
247 | startX = e.clientX; |
---|
248 | startY = e.clientY; |
---|
249 | } |
---|
250 | ); |
---|
251 | this.moveHandler = this.connect(win.doc.documentElement, touch.move, |
---|
252 | function(e){ |
---|
253 | skipReposition = true; |
---|
254 | if(e.touches){ |
---|
255 | active = isGesture = true; // touchmove implies touchstart |
---|
256 | }else if(active && (e.clientX != startX || e.clientY != startY)){ |
---|
257 | isGesture = true; |
---|
258 | } |
---|
259 | } |
---|
260 | ); |
---|
261 | this.clickHandler = this.connect(dropDown.domNode, "onclick", |
---|
262 | function(){ |
---|
263 | skipReposition = true; |
---|
264 | active = isGesture = false; // click implies no gesture movement |
---|
265 | } |
---|
266 | ); |
---|
267 | this.endHandler = this.connect(win.doc.documentElement, "onmouseup",//touch.release, |
---|
268 | function(){ |
---|
269 | this.defer(function(){ // allow onclick to go first |
---|
270 | skipReposition = true; |
---|
271 | if(!isGesture && active){ // if click without move, then close dropdown |
---|
272 | this.closeDropDown(); |
---|
273 | } |
---|
274 | active = false; |
---|
275 | }); |
---|
276 | } |
---|
277 | ); |
---|
278 | this.repositionTimer = setInterval(lang.hitch(this, function(){ |
---|
279 | if(skipReposition){ // don't reposition if busy |
---|
280 | skipReposition = false; |
---|
281 | return; |
---|
282 | } |
---|
283 | var currentAroundNodePos = domGeometry.position(aroundNode, false), |
---|
284 | currentPopupPos = domGeometry.position(wrapper, false), |
---|
285 | currentDeltaX = currentPopupPos.x - currentAroundNodePos.x, |
---|
286 | currentDeltaY = currentPopupPos.y - currentAroundNodePos.y; |
---|
287 | // if the popup is no longer placed correctly, relocate it |
---|
288 | if(Math.abs(currentDeltaX - deltaX) >= 1 || Math.abs(currentDeltaY - deltaY) >= 1){ // Firefox plays with partial pixels |
---|
289 | domStyle.set(wrapper, { left: parseInt(domStyle.get(wrapper, "left")) + deltaX - currentDeltaX + 'px', top: parseInt(domStyle.get(wrapper, "top")) + deltaY - currentDeltaY + 'px' }); |
---|
290 | } |
---|
291 | }), 50); // yield a short time to allow for consolidation for better CPU throughput |
---|
292 | } |
---|
293 | |
---|
294 | // We need to disable input control in order to prevent opening the soft keyboard in IE |
---|
295 | if(has("windows-theme")){ |
---|
296 | this.domNode.setAttribute("disabled", true); |
---|
297 | } |
---|
298 | |
---|
299 | return retVal; |
---|
300 | }, |
---|
301 | |
---|
302 | postCreate: function(){ |
---|
303 | this.inherited(arguments); |
---|
304 | this.connect(this.domNode, "onclick", "_onClick"); |
---|
305 | domAttr.set(this.domNode, "role", "combobox"); |
---|
306 | domAttr.set(this.domNode, "aria-expanded", "false"); |
---|
307 | }, |
---|
308 | |
---|
309 | destroy: function(){ |
---|
310 | if(this.repositionTimer){ |
---|
311 | clearInterval(this.repositionTimer); |
---|
312 | } |
---|
313 | this.inherited(arguments); |
---|
314 | }, |
---|
315 | |
---|
316 | _onClick: function(/*Event*/ e){ |
---|
317 | // tags: |
---|
318 | // private |
---|
319 | |
---|
320 | // throttle clicks to prevent double click from doing double actions |
---|
321 | if(!this._throttleHandler){ |
---|
322 | if(this.opened){ |
---|
323 | this.closeDropDown(); |
---|
324 | }else{ |
---|
325 | this._startSearchAll(); |
---|
326 | } |
---|
327 | } |
---|
328 | } |
---|
329 | }); |
---|
330 | }); |
---|