source: Dev/trunk/src/client/dijit/_CssStateMixin.js @ 525

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

Added Dojo 1.9.3 release.

File size: 12.5 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.map
3        "dojo/_base/declare", // declare
4        "dojo/dom", // dom.isDescendant()
5        "dojo/dom-class", // domClass.toggle
6        "dojo/has",
7        "dojo/_base/lang", // lang.hitch
8        "dojo/on",
9        "dojo/domReady",
10        "dojo/touch",
11        "dojo/_base/window", // win.body
12        "./a11yclick",
13        "./registry"
14], function(array, declare, dom, domClass, has, lang, on, domReady, touch, win, a11yclick, registry){
15
16        // module:
17        //              dijit/_CssStateMixin
18
19        var CssStateMixin = declare("dijit._CssStateMixin", [], {
20                // summary:
21                //              Mixin for widgets to set CSS classes on the widget DOM nodes depending on hover/mouse press/focus
22                //              state changes, and also higher-level state changes such becoming disabled or selected.
23                //
24                // description:
25                //              By mixing this class into your widget, and setting the this.baseClass attribute, it will automatically
26                //              maintain CSS classes on the widget root node (this.domNode) depending on hover,
27                //              active, focus, etc. state.   Ex: with a baseClass of dijitButton, it will apply the classes
28                //              dijitButtonHovered and dijitButtonActive, as the user moves the mouse over the widget and clicks it.
29                //
30                //              It also sets CSS like dijitButtonDisabled based on widget semantic state.
31                //
32                //              By setting the cssStateNodes attribute, a widget can also track events on subnodes (like buttons
33                //              within the widget).
34
35                /*=====
36                 // cssStateNodes: [protected] Object
37                 //             Subclasses may define a cssStateNodes property that lists sub-nodes within the widget that
38                 //             need CSS classes applied on mouse hover/press and focus.
39                 //
40                 //             Each entry in this optional hash is a an attach-point name (like "upArrowButton") mapped to a CSS class name
41                 //             (like "dijitUpArrowButton"). Example:
42                 //     |               {
43                 //     |                       "upArrowButton": "dijitUpArrowButton",
44                 //     |                       "downArrowButton": "dijitDownArrowButton"
45                 //     |               }
46                 //             The above will set the CSS class dijitUpArrowButton to the this.upArrowButton DOMNode when it
47                 //             is hovered, etc.
48                 cssStateNodes: {},
49                 =====*/
50
51                // hovering: [readonly] Boolean
52                //              True if cursor is over this widget
53                hovering: false,
54
55                // active: [readonly] Boolean
56                //              True if mouse was pressed while over this widget, and hasn't been released yet
57                active: false,
58
59                _applyAttributes: function(){
60                        // This code would typically be in postCreate(), but putting in _applyAttributes() for
61                        // performance: so the class changes happen before DOM is inserted into the document.
62                        // Change back to postCreate() in 2.0.  See #11635.
63
64                        this.inherited(arguments);
65
66                        // Monitoring changes to disabled, readonly, etc. state, and update CSS class of root node
67                        array.forEach(["disabled", "readOnly", "checked", "selected", "focused", "state", "hovering", "active", "_opened"], function(attr){
68                                this.watch(attr, lang.hitch(this, "_setStateClass"));
69                        }, this);
70
71                        // Track hover and active mouse events on widget root node, plus possibly on subnodes
72                        for(var ap in this.cssStateNodes || {}){
73                                this._trackMouseState(this[ap], this.cssStateNodes[ap]);
74                        }
75                        this._trackMouseState(this.domNode, this.baseClass);
76
77                        // Set state initially; there's probably no hover/active/focus state but widget might be
78                        // disabled/readonly/checked/selected so we want to set CSS classes for those conditions.
79                        this._setStateClass();
80                },
81
82                _cssMouseEvent: function(/*Event*/ event){
83                        // summary:
84                        //              Handler for CSS event on this.domNode. Sets hovering and active properties depending on mouse state,
85                        //              which triggers _setStateClass() to set appropriate CSS classes for this.domNode.
86
87                        if(!this.disabled){
88                                switch(event.type){
89                                        case "mouseover":
90                                        case "MSPointerOver":
91                                        case "pointerover":
92                                                this._set("hovering", true);
93                                                this._set("active", this._mouseDown);
94                                                break;
95                                        case "mouseout":
96                                        case "MSPointerOut":
97                                        case "pointerout":
98                                                this._set("hovering", false);
99                                                this._set("active", false);
100                                                break;
101                                        case "mousedown":
102                                        case "touchstart":
103                                        case "MSPointerDown":
104                                        case "pointerdown":
105                                        case "keydown":
106                                                this._set("active", true);
107                                                break;
108                                        case "mouseup":
109                                        case "dojotouchend":
110                                        case "MSPointerUp":
111                                        case "pointerup":
112                                        case "keyup":
113                                                this._set("active", false);
114                                                break;
115                                }
116                        }
117                },
118
119                _setStateClass: function(){
120                        // summary:
121                        //              Update the visual state of the widget by setting the css classes on this.domNode
122                        //              (or this.stateNode if defined) by combining this.baseClass with
123                        //              various suffixes that represent the current widget state(s).
124                        //
125                        // description:
126                        //              In the case where a widget has multiple
127                        //              states, it sets the class based on all possible
128                        //              combinations.  For example, an invalid form widget that is being hovered
129                        //              will be "dijitInput dijitInputInvalid dijitInputHover dijitInputInvalidHover".
130                        //
131                        //              The widget may have one or more of the following states, determined
132                        //              by this.state, this.checked, this.valid, and this.selected:
133                        //
134                        //              - Error - ValidationTextBox sets this.state to "Error" if the current input value is invalid
135                        //              - Incomplete - ValidationTextBox sets this.state to "Incomplete" if the current input value is not finished yet
136                        //              - Checked - ex: a checkmark or a ToggleButton in a checked state, will have this.checked==true
137                        //              - Selected - ex: currently selected tab will have this.selected==true
138                        //
139                        //              In addition, it may have one or more of the following states,
140                        //              based on this.disabled and flags set in _onMouse (this.active, this.hovering) and from focus manager (this.focused):
141                        //
142                        //              - Disabled      - if the widget is disabled
143                        //              - Active                - if the mouse (or space/enter key?) is being pressed down
144                        //              - Focused               - if the widget has focus
145                        //              - Hover         - if the mouse is over the widget
146
147                        // Compute new set of classes
148                        var newStateClasses = this.baseClass.split(" ");
149
150                        function multiply(modifier){
151                                newStateClasses = newStateClasses.concat(array.map(newStateClasses, function(c){
152                                        return c + modifier;
153                                }), "dijit" + modifier);
154                        }
155
156                        if(!this.isLeftToRight()){
157                                // For RTL mode we need to set an addition class like dijitTextBoxRtl.
158                                multiply("Rtl");
159                        }
160
161                        var checkedState = this.checked == "mixed" ? "Mixed" : (this.checked ? "Checked" : "");
162                        if(this.checked){
163                                multiply(checkedState);
164                        }
165                        if(this.state){
166                                multiply(this.state);
167                        }
168                        if(this.selected){
169                                multiply("Selected");
170                        }
171                        if(this._opened){
172                                multiply("Opened");
173                        }
174
175                        if(this.disabled){
176                                multiply("Disabled");
177                        }else if(this.readOnly){
178                                multiply("ReadOnly");
179                        }else{
180                                if(this.active){
181                                        multiply("Active");
182                                }else if(this.hovering){
183                                        multiply("Hover");
184                                }
185                        }
186
187                        if(this.focused){
188                                multiply("Focused");
189                        }
190
191                        // Remove old state classes and add new ones.
192                        // For performance concerns we only write into domNode.className once.
193                        var tn = this.stateNode || this.domNode,
194                                classHash = {}; // set of all classes (state and otherwise) for node
195
196                        array.forEach(tn.className.split(" "), function(c){
197                                classHash[c] = true;
198                        });
199
200                        if("_stateClasses" in this){
201                                array.forEach(this._stateClasses, function(c){
202                                        delete classHash[c];
203                                });
204                        }
205
206                        array.forEach(newStateClasses, function(c){
207                                classHash[c] = true;
208                        });
209
210                        var newClasses = [];
211                        for(var c in classHash){
212                                newClasses.push(c);
213                        }
214                        tn.className = newClasses.join(" ");
215
216                        this._stateClasses = newStateClasses;
217                },
218
219                _subnodeCssMouseEvent: function(node, clazz, evt){
220                        // summary:
221                        //              Handler for hover/active mouse event on widget's subnode
222                        if(this.disabled || this.readOnly){
223                                return;
224                        }
225
226                        function hover(isHovering){
227                                domClass.toggle(node, clazz + "Hover", isHovering);
228                        }
229
230                        function active(isActive){
231                                domClass.toggle(node, clazz + "Active", isActive);
232                        }
233
234                        function focused(isFocused){
235                                domClass.toggle(node, clazz + "Focused", isFocused);
236                        }
237
238                        switch(evt.type){
239                                case "mouseover":
240                                case "MSPointerOver":
241                                case "pointerover":
242                                        hover(true);
243                                        break;
244                                case "mouseout":
245                                case "MSPointerOut":
246                                case "pointerout":
247                                        hover(false);
248                                        active(false);
249                                        break;
250                                case "mousedown":
251                                case "touchstart":
252                                case "MSPointerDown":
253                                case "pointerdown":
254                                case "keydown":
255                                        active(true);
256                                        break;
257                                case "mouseup":
258                                case "MSPointerUp":
259                                case "pointerup":
260                                case "dojotouchend":
261                                case "keyup":
262                                        active(false);
263                                        break;
264                                case "focus":
265                                case "focusin":
266                                        focused(true);
267                                        break;
268                                case "blur":
269                                case "focusout":
270                                        focused(false);
271                                        break;
272                        }
273                },
274
275                _trackMouseState: function(/*DomNode*/ node, /*String*/ clazz){
276                        // summary:
277                        //              Track mouse/focus events on specified node and set CSS class on that node to indicate
278                        //              current state.   Usually not called directly, but via cssStateNodes attribute.
279                        // description:
280                        //              Given class=foo, will set the following CSS class on the node
281                        //
282                        //              - fooActive: if the user is currently pressing down the mouse button while over the node
283                        //              - fooHover: if the user is hovering the mouse over the node, but not pressing down a button
284                        //              - fooFocus: if the node is focused
285                        //
286                        //              Note that it won't set any classes if the widget is disabled.
287                        // node: DomNode
288                        //              Should be a sub-node of the widget, not the top node (this.domNode), since the top node
289                        //              is handled specially and automatically just by mixing in this class.
290                        // clazz: String
291                        //              CSS class name (ex: dijitSliderUpArrow)
292
293                        // Flag for listener code below to call this._cssMouseEvent() or this._subnodeCssMouseEvent()
294                        // when node is hovered/active
295                        node._cssState = clazz;
296                }
297        });
298
299        domReady(function(){
300                // Document level listener to catch hover etc. events on widget root nodes and subnodes.
301                // Note that when the mouse is moved quickly, a single onmouseenter event could signal that multiple widgets
302                // have been hovered or unhovered (try test_Accordion.html)
303
304                function pointerHandler(evt, target, relatedTarget){
305                        // Handler for mouseover, mouseout, a11yclick.press and a11click.release events
306
307                        // Poor man's event propagation.  Don't propagate event to ancestors of evt.relatedTarget,
308                        // to avoid processing mouseout events moving from a widget's domNode to a descendant node;
309                        // such events shouldn't be interpreted as a mouseleave on the widget.
310                        if(relatedTarget && dom.isDescendant(relatedTarget, target)){
311                                return;
312                        }
313
314                        for(var node = target; node && node != relatedTarget; node = node.parentNode){
315                                // Process any nodes with _cssState property.   They are generally widget root nodes,
316                                // but could also be sub-nodes within a widget
317                                if(node._cssState){
318                                        var widget = registry.getEnclosingWidget(node);
319                                        if(widget){
320                                                if(node == widget.domNode){
321                                                        // event on the widget's root node
322                                                        widget._cssMouseEvent(evt);
323                                                }else{
324                                                        // event on widget's sub-node
325                                                        widget._subnodeCssMouseEvent(node, node._cssState, evt);
326                                                }
327                                        }
328                                }
329                        }
330                }
331
332                var body = win.body(), activeNode;
333
334                // Handle pointer related events (i.e. mouse or touch)
335                on(body, touch.over, function(evt){
336                        // Using touch.over rather than mouseover mainly to ignore phantom mouse events on iOS.
337                        pointerHandler(evt, evt.target, evt.relatedTarget);
338                });
339                on(body, touch.out, function(evt){
340                        // Using touch.out rather than mouseout mainly to ignore phantom mouse events on iOS.
341                        pointerHandler(evt, evt.target, evt.relatedTarget);
342                });
343                on(body, a11yclick.press, function(evt){
344                        // Save the a11yclick.press target to reference when the a11yclick.release comes.
345                        activeNode = evt.target;
346                        pointerHandler(evt, activeNode)
347                });
348                on(body, a11yclick.release, function(evt){
349                        // The release event could come on a separate node than the press event, if for example user slid finger.
350                        // Reference activeNode to reset the state of the node that got state set in the a11yclick.press handler.
351                        pointerHandler(evt, activeNode);
352                        activeNode = null;
353                });
354
355                // Track focus events on widget sub-nodes that have been registered via _trackMouseState().
356                // However, don't track focus events on the widget root nodes, because focus is tracked via the
357                // focus manager (and it's not really tracking focus, but rather tracking that focus is on one of the widget's
358                // nodes or a subwidget's node or a popup node, etc.)
359                // Remove for 2.0 (if focus CSS needed, just use :focus pseudo-selector).
360                on(body, "focusin, focusout", function(evt){
361                        var node = evt.target;
362                        if(node._cssState && !node.getAttribute("widgetId")){
363                                var widget = registry.getEnclosingWidget(node);
364                                if(widget){
365                                        widget._subnodeCssMouseEvent(node, node._cssState, evt);
366                                }
367                        }
368                });
369        });
370
371        return CssStateMixin;
372});
Note: See TracBrowser for help on using the repository browser.