[483] | 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 | }); |
---|