source: Dev/trunk/src/client/dijit/form/Select.js @ 483

Last change on this file since 483 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 13.7 KB
Line 
1define([
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: "&#160;", // &nbsp;
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});
Note: See TracBrowser for help on using the repository browser.