[483] | 1 | define([ |
---|
| 2 | "dojo/_base/array", // array.forEach |
---|
| 3 | "dojo/_base/declare", // declare |
---|
| 4 | "dojo/dom-attr", // domAttr.set |
---|
| 5 | "dojo/dom-class", // domClass.add domClass.remove domClass.toggle |
---|
| 6 | "dojo/dom-geometry", // domGeometry.setMarginBox |
---|
| 7 | "dojo/i18n", // i18n.getLocalization |
---|
| 8 | "dojo/_base/lang", // lang.hitch |
---|
| 9 | "dojo/on", |
---|
| 10 | "dojo/sniff", // has("ie") |
---|
| 11 | "./_FormSelectWidget", |
---|
| 12 | "../_HasDropDown", |
---|
| 13 | "../DropDownMenu", |
---|
| 14 | "../MenuItem", |
---|
| 15 | "../MenuSeparator", |
---|
| 16 | "../Tooltip", |
---|
| 17 | "../_KeyNavMixin", |
---|
| 18 | "../registry", // registry.byNode |
---|
| 19 | "dojo/text!./templates/Select.html", |
---|
| 20 | "dojo/i18n!./nls/validate" |
---|
| 21 | ], function(array, declare, domAttr, domClass, domGeometry, i18n, lang, on, has, |
---|
| 22 | _FormSelectWidget, _HasDropDown, DropDownMenu, MenuItem, MenuSeparator, Tooltip, _KeyNavMixin, registry, template){ |
---|
| 23 | |
---|
| 24 | // module: |
---|
| 25 | // dijit/form/Select |
---|
| 26 | |
---|
| 27 | var _SelectMenu = declare("dijit.form._SelectMenu", DropDownMenu, { |
---|
| 28 | // summary: |
---|
| 29 | // An internally-used menu for dropdown that allows us a vertical scrollbar |
---|
| 30 | |
---|
| 31 | // Override Menu.autoFocus setting so that opening a Select highlights the current value. |
---|
| 32 | autoFocus: true, |
---|
| 33 | |
---|
| 34 | buildRendering: function(){ |
---|
| 35 | this.inherited(arguments); |
---|
| 36 | |
---|
| 37 | this.domNode.setAttribute("role", "listbox"); |
---|
| 38 | }, |
---|
| 39 | |
---|
| 40 | postCreate: function(){ |
---|
| 41 | // summary: |
---|
| 42 | // stop mousemove from selecting text on IE to be consistent with other browsers |
---|
| 43 | |
---|
| 44 | this.inherited(arguments); |
---|
| 45 | |
---|
| 46 | this.own(on(this.domNode, "selectstart", function(evt){ |
---|
| 47 | evt.preventDefault(); |
---|
| 48 | evt.stopPropagation(); |
---|
| 49 | })); |
---|
| 50 | }, |
---|
| 51 | |
---|
| 52 | focus: function(){ |
---|
| 53 | // summary: |
---|
| 54 | // Overridden so that the previously selected value will be focused instead of only the first item |
---|
| 55 | var found = false, |
---|
| 56 | val = this.parentWidget.value; |
---|
| 57 | if(lang.isArray(val)){ |
---|
| 58 | val = val[val.length - 1]; |
---|
| 59 | } |
---|
| 60 | if(val){ // if focus selected |
---|
| 61 | array.forEach(this.parentWidget._getChildren(), function(child){ |
---|
| 62 | if(child.option && (val === child.option.value)){ // find menu item widget with this value |
---|
| 63 | found = true; |
---|
| 64 | this.focusChild(child, false); // focus previous selection |
---|
| 65 | } |
---|
| 66 | }, this); |
---|
| 67 | } |
---|
| 68 | if(!found){ |
---|
| 69 | this.inherited(arguments); // focus first item by default |
---|
| 70 | } |
---|
| 71 | } |
---|
| 72 | }); |
---|
| 73 | |
---|
| 74 | var Select = declare("dijit.form.Select" + (has("dojo-bidi") ? "_NoBidi" : ""), [_FormSelectWidget, _HasDropDown, _KeyNavMixin], { |
---|
| 75 | // summary: |
---|
| 76 | // This is a "styleable" select box - it is basically a DropDownButton which |
---|
| 77 | // can take a `<select>` as its input. |
---|
| 78 | |
---|
| 79 | baseClass: "dijitSelect dijitValidationTextBox", |
---|
| 80 | |
---|
| 81 | templateString: template, |
---|
| 82 | |
---|
| 83 | _buttonInputDisabled: has("ie") ? "disabled" : "", // allows IE to disallow focus, but Firefox cannot be disabled for mousedown events |
---|
| 84 | |
---|
| 85 | // required: Boolean |
---|
| 86 | // Can be true or false, default is false. |
---|
| 87 | required: false, |
---|
| 88 | |
---|
| 89 | // state: [readonly] String |
---|
| 90 | // "Incomplete" if this select is required but unset (i.e. blank value), "" otherwise |
---|
| 91 | state: "", |
---|
| 92 | |
---|
| 93 | // message: String |
---|
| 94 | // Currently displayed error/prompt message |
---|
| 95 | message: "", |
---|
| 96 | |
---|
| 97 | // tooltipPosition: String[] |
---|
| 98 | // See description of `dijit/Tooltip.defaultPosition` for details on this parameter. |
---|
| 99 | tooltipPosition: [], |
---|
| 100 | |
---|
| 101 | // emptyLabel: string |
---|
| 102 | // What to display in an "empty" dropdown |
---|
| 103 | emptyLabel: " ", // |
---|
| 104 | |
---|
| 105 | // _isLoaded: Boolean |
---|
| 106 | // Whether or not we have been loaded |
---|
| 107 | _isLoaded: false, |
---|
| 108 | |
---|
| 109 | // _childrenLoaded: Boolean |
---|
| 110 | // Whether or not our children have been loaded |
---|
| 111 | _childrenLoaded: false, |
---|
| 112 | |
---|
| 113 | _fillContent: function(){ |
---|
| 114 | // summary: |
---|
| 115 | // Set the value to be the first, or the selected index |
---|
| 116 | this.inherited(arguments); |
---|
| 117 | // set value from selected option |
---|
| 118 | if(this.options.length && !this.value && this.srcNodeRef){ |
---|
| 119 | var si = this.srcNodeRef.selectedIndex || 0; // || 0 needed for when srcNodeRef is not a SELECT |
---|
| 120 | this._set("value", this.options[si >= 0 ? si : 0].value); |
---|
| 121 | } |
---|
| 122 | // Create the dropDown widget |
---|
| 123 | this.dropDown = new _SelectMenu({ id: this.id + "_menu", parentWidget: this }); |
---|
| 124 | domClass.add(this.dropDown.domNode, this.baseClass.replace(/\s+|$/g, "Menu ")); |
---|
| 125 | }, |
---|
| 126 | |
---|
| 127 | _getMenuItemForOption: function(/*_FormSelectWidget.__SelectOption*/ option){ |
---|
| 128 | // summary: |
---|
| 129 | // For the given option, return the menu item that should be |
---|
| 130 | // used to display it. This can be overridden as needed |
---|
| 131 | if(!option.value && !option.label){ |
---|
| 132 | // We are a separator (no label set for it) |
---|
| 133 | return new MenuSeparator({ownerDocument: this.ownerDocument}); |
---|
| 134 | }else{ |
---|
| 135 | // Just a regular menu option |
---|
| 136 | var click = lang.hitch(this, "_setValueAttr", option); |
---|
| 137 | var item = new MenuItem({ |
---|
| 138 | option: option, |
---|
| 139 | label: option.label || this.emptyLabel, |
---|
| 140 | onClick: click, |
---|
| 141 | ownerDocument: this.ownerDocument, |
---|
| 142 | dir: this.dir, |
---|
| 143 | textDir: this.textDir, |
---|
| 144 | disabled: option.disabled || false |
---|
| 145 | }); |
---|
| 146 | item.focusNode.setAttribute("role", "option"); |
---|
| 147 | return item; |
---|
| 148 | } |
---|
| 149 | }, |
---|
| 150 | |
---|
| 151 | _addOptionItem: function(/*_FormSelectWidget.__SelectOption*/ option){ |
---|
| 152 | // summary: |
---|
| 153 | // For the given option, add an option to our dropdown. |
---|
| 154 | // If the option doesn't have a value, then a separator is added |
---|
| 155 | // in that place. |
---|
| 156 | if(this.dropDown){ |
---|
| 157 | this.dropDown.addChild(this._getMenuItemForOption(option)); |
---|
| 158 | } |
---|
| 159 | }, |
---|
| 160 | |
---|
| 161 | _getChildren: function(){ |
---|
| 162 | if(!this.dropDown){ |
---|
| 163 | return []; |
---|
| 164 | } |
---|
| 165 | return this.dropDown.getChildren(); |
---|
| 166 | }, |
---|
| 167 | |
---|
| 168 | focus: function(){ |
---|
| 169 | // Override _KeyNavMixin::focus(), which calls focusFirstChild(). |
---|
| 170 | // We just want the standard form widget behavior. |
---|
| 171 | if(!this.disabled && this.focusNode.focus){ |
---|
| 172 | try{ |
---|
| 173 | this.focusNode.focus(); |
---|
| 174 | }catch(e){ |
---|
| 175 | /*squelch errors from hidden nodes*/ |
---|
| 176 | } |
---|
| 177 | } |
---|
| 178 | }, |
---|
| 179 | |
---|
| 180 | focusChild: function(/*dijit/_WidgetBase*/ widget){ |
---|
| 181 | // summary: |
---|
| 182 | // Sets the value to the given option, used during search by letter. |
---|
| 183 | // widget: |
---|
| 184 | // Reference to option's widget |
---|
| 185 | // tags: |
---|
| 186 | // protected |
---|
| 187 | if(widget){ |
---|
| 188 | this.set('value', widget.option); |
---|
| 189 | } |
---|
| 190 | }, |
---|
| 191 | |
---|
| 192 | _getFirst: function(){ |
---|
| 193 | // summary: |
---|
| 194 | // Returns the first child widget. |
---|
| 195 | // tags: |
---|
| 196 | // abstract extension |
---|
| 197 | var children = this._getChildren(); |
---|
| 198 | return children.length ? children[0] : null; |
---|
| 199 | }, |
---|
| 200 | |
---|
| 201 | _getLast: function(){ |
---|
| 202 | // summary: |
---|
| 203 | // Returns the last child widget. |
---|
| 204 | // tags: |
---|
| 205 | // abstract extension |
---|
| 206 | var children = this._getChildren(); |
---|
| 207 | return children.length ? children[children.length-1] : null; |
---|
| 208 | }, |
---|
| 209 | |
---|
| 210 | childSelector: function(/*DOMNode*/ node){ |
---|
| 211 | // Implement _KeyNavMixin.childSelector, to identify focusable child nodes. |
---|
| 212 | // If we allowed a dojo/query dependency from this module this could more simply be a string "> *" |
---|
| 213 | // instead of this function. |
---|
| 214 | |
---|
| 215 | var node = registry.byNode(node); |
---|
| 216 | return node && node.getParent() == this.dropDown; |
---|
| 217 | }, |
---|
| 218 | |
---|
| 219 | onKeyboardSearch: function(/*dijit/_WidgetBase*/ item, /*Event*/ evt, /*String*/ searchString, /*Number*/ numMatches){ |
---|
| 220 | // summary: |
---|
| 221 | // When a key is pressed that matches a child item, |
---|
| 222 | // this method is called so that a widget can take appropriate action is necessary. |
---|
| 223 | // tags: |
---|
| 224 | // protected |
---|
| 225 | if(item){ |
---|
| 226 | this.focusChild(item); |
---|
| 227 | } |
---|
| 228 | }, |
---|
| 229 | |
---|
| 230 | _loadChildren: function(/*Boolean*/ loadMenuItems){ |
---|
| 231 | // summary: |
---|
| 232 | // Resets the menu and the length attribute of the button - and |
---|
| 233 | // ensures that the label is appropriately set. |
---|
| 234 | // loadMenuItems: Boolean |
---|
| 235 | // actually loads the child menu items - we only do this when we are |
---|
| 236 | // populating for showing the dropdown. |
---|
| 237 | |
---|
| 238 | if(loadMenuItems === true){ |
---|
| 239 | // this.inherited destroys this.dropDown's child widgets (MenuItems). |
---|
| 240 | // Avoid this.dropDown (Menu widget) having a pointer to a destroyed widget (which will cause |
---|
| 241 | // issues later in _setSelected). (see #10296) |
---|
| 242 | if(this.dropDown){ |
---|
| 243 | delete this.dropDown.focusedChild; |
---|
| 244 | this.focusedChild = null; |
---|
| 245 | } |
---|
| 246 | if(this.options.length){ |
---|
| 247 | this.inherited(arguments); |
---|
| 248 | }else{ |
---|
| 249 | // Drop down menu is blank but add one blank entry just so something appears on the screen |
---|
| 250 | // to let users know that they are no choices (mimicing native select behavior) |
---|
| 251 | array.forEach(this._getChildren(), function(child){ |
---|
| 252 | child.destroyRecursive(); |
---|
| 253 | }); |
---|
| 254 | var item = new MenuItem({ |
---|
| 255 | ownerDocument: this.ownerDocument, |
---|
| 256 | label: this.emptyLabel |
---|
| 257 | }); |
---|
| 258 | this.dropDown.addChild(item); |
---|
| 259 | } |
---|
| 260 | }else{ |
---|
| 261 | this._updateSelection(); |
---|
| 262 | } |
---|
| 263 | |
---|
| 264 | this._isLoaded = false; |
---|
| 265 | this._childrenLoaded = true; |
---|
| 266 | |
---|
| 267 | if(!this._loadingStore){ |
---|
| 268 | // Don't call this if we are loading - since we will handle it later |
---|
| 269 | this._setValueAttr(this.value, false); |
---|
| 270 | } |
---|
| 271 | }, |
---|
| 272 | |
---|
| 273 | _refreshState: function(){ |
---|
| 274 | if(this._started){ |
---|
| 275 | this.validate(this.focused); |
---|
| 276 | } |
---|
| 277 | }, |
---|
| 278 | |
---|
| 279 | startup: function(){ |
---|
| 280 | this.inherited(arguments); |
---|
| 281 | this._refreshState(); // after all _set* methods have run |
---|
| 282 | }, |
---|
| 283 | |
---|
| 284 | _setValueAttr: function(value){ |
---|
| 285 | this.inherited(arguments); |
---|
| 286 | domAttr.set(this.valueNode, "value", this.get("value")); |
---|
| 287 | this._refreshState(); // to update this.state |
---|
| 288 | }, |
---|
| 289 | |
---|
| 290 | _setNameAttr: "valueNode", |
---|
| 291 | |
---|
| 292 | _setDisabledAttr: function(/*Boolean*/ value){ |
---|
| 293 | this.inherited(arguments); |
---|
| 294 | this._refreshState(); // to update this.state |
---|
| 295 | }, |
---|
| 296 | |
---|
| 297 | _setRequiredAttr: function(/*Boolean*/ value){ |
---|
| 298 | this._set("required", value); |
---|
| 299 | this.focusNode.setAttribute("aria-required", value); |
---|
| 300 | this._refreshState(); // to update this.state |
---|
| 301 | }, |
---|
| 302 | |
---|
| 303 | _setOptionsAttr: function(/*Array*/ options){ |
---|
| 304 | this._isLoaded = false; |
---|
| 305 | this._set('options', options); |
---|
| 306 | }, |
---|
| 307 | |
---|
| 308 | _setDisplay: function(/*String*/ newDisplay){ |
---|
| 309 | // summary: |
---|
| 310 | // sets the display for the given value (or values) |
---|
| 311 | var lbl = newDisplay || this.emptyLabel; |
---|
| 312 | this.containerNode.innerHTML = '<span role="option" class="dijitReset dijitInline ' + this.baseClass.replace(/\s+|$/g, "Label ") + '">' + lbl + '</span>'; |
---|
| 313 | }, |
---|
| 314 | |
---|
| 315 | validate: function(/*Boolean*/ isFocused){ |
---|
| 316 | // summary: |
---|
| 317 | // Called by oninit, onblur, and onkeypress, and whenever required/disabled state changes |
---|
| 318 | // description: |
---|
| 319 | // Show missing or invalid messages if appropriate, and highlight textbox field. |
---|
| 320 | // Used when a select is initially set to no value and the user is required to |
---|
| 321 | // set the value. |
---|
| 322 | |
---|
| 323 | var isValid = this.disabled || this.isValid(isFocused); |
---|
| 324 | this._set("state", isValid ? "" : (this._hasBeenBlurred ? "Error" : "Incomplete")); |
---|
| 325 | this.focusNode.setAttribute("aria-invalid", isValid ? "false" : "true"); |
---|
| 326 | var message = isValid ? "" : this._missingMsg; |
---|
| 327 | if(message && this.focused && this._hasBeenBlurred){ |
---|
| 328 | Tooltip.show(message, this.domNode, this.tooltipPosition, !this.isLeftToRight()); |
---|
| 329 | }else{ |
---|
| 330 | Tooltip.hide(this.domNode); |
---|
| 331 | } |
---|
| 332 | this._set("message", message); |
---|
| 333 | return isValid; |
---|
| 334 | }, |
---|
| 335 | |
---|
| 336 | isValid: function(/*Boolean*/ /*===== isFocused =====*/){ |
---|
| 337 | // summary: |
---|
| 338 | // Whether or not this is a valid value. The only way a Select |
---|
| 339 | // can be invalid is when it's required but nothing is selected. |
---|
| 340 | return (!this.required || this.value === 0 || !(/^\s*$/.test(this.value || ""))); // handle value is null or undefined |
---|
| 341 | }, |
---|
| 342 | |
---|
| 343 | reset: function(){ |
---|
| 344 | // summary: |
---|
| 345 | // Overridden so that the state will be cleared. |
---|
| 346 | this.inherited(arguments); |
---|
| 347 | Tooltip.hide(this.domNode); |
---|
| 348 | this._refreshState(); // to update this.state |
---|
| 349 | }, |
---|
| 350 | |
---|
| 351 | postMixInProperties: function(){ |
---|
| 352 | // summary: |
---|
| 353 | // set the missing message |
---|
| 354 | this.inherited(arguments); |
---|
| 355 | this._missingMsg = i18n.getLocalization("dijit.form", "validate", this.lang).missingMessage; |
---|
| 356 | }, |
---|
| 357 | |
---|
| 358 | postCreate: function(){ |
---|
| 359 | // summary: |
---|
| 360 | // stop mousemove from selecting text on IE to be consistent with other browsers |
---|
| 361 | |
---|
| 362 | this.inherited(arguments); |
---|
| 363 | |
---|
| 364 | this.own(on(this.domNode, "selectstart", function(evt){ |
---|
| 365 | evt.preventDefault(); |
---|
| 366 | evt.stopPropagation(); |
---|
| 367 | })); |
---|
| 368 | |
---|
| 369 | this.domNode.setAttribute("aria-expanded", "false"); |
---|
| 370 | |
---|
| 371 | if(has("ie") < 9){ |
---|
| 372 | // IE INPUT tag fontFamily has to be set directly using STYLE |
---|
| 373 | // the defer gives IE a chance to render the TextBox and to deal with font inheritance |
---|
| 374 | this.defer(function(){ |
---|
| 375 | try{ |
---|
| 376 | var s = domStyle.getComputedStyle(this.domNode); // can throw an exception if widget is immediately destroyed |
---|
| 377 | if(s){ |
---|
| 378 | var ff = s.fontFamily; |
---|
| 379 | if(ff){ |
---|
| 380 | var inputs = this.domNode.getElementsByTagName("INPUT"); |
---|
| 381 | if(inputs){ |
---|
| 382 | for(var i = 0; i < inputs.length; i++){ |
---|
| 383 | inputs[i].style.fontFamily = ff; |
---|
| 384 | } |
---|
| 385 | } |
---|
| 386 | } |
---|
| 387 | } |
---|
| 388 | }catch(e){ |
---|
| 389 | // when used in a Dialog, and this is called before the dialog is |
---|
| 390 | // shown, s.fontFamily would trigger "Invalid Argument" error. |
---|
| 391 | } |
---|
| 392 | }); |
---|
| 393 | } |
---|
| 394 | }, |
---|
| 395 | |
---|
| 396 | _setStyleAttr: function(/*String||Object*/ value){ |
---|
| 397 | this.inherited(arguments); |
---|
| 398 | domClass.toggle(this.domNode, this.baseClass.replace(/\s+|$/g, "FixedWidth "), !!this.domNode.style.width); |
---|
| 399 | }, |
---|
| 400 | |
---|
| 401 | isLoaded: function(){ |
---|
| 402 | return this._isLoaded; |
---|
| 403 | }, |
---|
| 404 | |
---|
| 405 | loadDropDown: function(/*Function*/ loadCallback){ |
---|
| 406 | // summary: |
---|
| 407 | // populates the menu |
---|
| 408 | this._loadChildren(true); |
---|
| 409 | this._isLoaded = true; |
---|
| 410 | loadCallback(); |
---|
| 411 | }, |
---|
| 412 | |
---|
| 413 | destroy: function(preserveDom){ |
---|
| 414 | if(this.dropDown && !this.dropDown._destroyed){ |
---|
| 415 | this.dropDown.destroyRecursive(preserveDom); |
---|
| 416 | delete this.dropDown; |
---|
| 417 | } |
---|
| 418 | this.inherited(arguments); |
---|
| 419 | }, |
---|
| 420 | |
---|
| 421 | _onFocus: function(){ |
---|
| 422 | this.validate(true); // show tooltip if second focus of required tooltip, but no selection |
---|
| 423 | }, |
---|
| 424 | |
---|
| 425 | _onBlur: function(){ |
---|
| 426 | Tooltip.hide(this.domNode); |
---|
| 427 | this.inherited(arguments); |
---|
| 428 | this.validate(false); |
---|
| 429 | } |
---|
| 430 | }); |
---|
| 431 | |
---|
| 432 | if(has("dojo-bidi")){ |
---|
| 433 | Select = declare("dijit.form.Select", Select, { |
---|
| 434 | _setDisplay: function(/*String*/ newDisplay){ |
---|
| 435 | this.inherited(arguments); |
---|
| 436 | this.applyTextDir(this.containerNode); |
---|
| 437 | } |
---|
| 438 | }); |
---|
| 439 | } |
---|
| 440 | |
---|
| 441 | Select._Menu = _SelectMenu; // for monkey patching |
---|
| 442 | |
---|
| 443 | // generic event helper to ensure the dropdown items are loaded before the real event handler is called |
---|
| 444 | function _onEventAfterLoad(method){ |
---|
| 445 | return function(evt){ |
---|
| 446 | if(!this._isLoaded){ |
---|
| 447 | this.loadDropDown(lang.hitch(this, method, evt)); |
---|
| 448 | }else{ |
---|
| 449 | this.inherited(method, arguments); |
---|
| 450 | } |
---|
| 451 | }; |
---|
| 452 | } |
---|
| 453 | Select.prototype._onContainerKeydown = _onEventAfterLoad("_onContainerKeydown"); |
---|
| 454 | Select.prototype._onContainerKeypress = _onEventAfterLoad("_onContainerKeypress"); |
---|
| 455 | |
---|
| 456 | return Select; |
---|
| 457 | }); |
---|