1 | define([ |
---|
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 | }); |
---|