[483] | 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 | }); |
---|