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