define("dijit/_CssStateMixin", [ "dojo/_base/array", // array.forEach array.map "dojo/_base/declare", // declare "dojo/dom", // dom.isDescendant() "dojo/dom-class", // domClass.toggle "dojo/has", "dojo/_base/lang", // lang.hitch "dojo/on", "dojo/ready", "dojo/_base/window", // win.body "./registry" ], function(array, declare, dom, domClass, has, lang, on, ready, win, registry){ // module: // dijit/_CssStateMixin var CssStateMixin = declare("dijit._CssStateMixin", [], { // summary: // Mixin for widgets to set CSS classes on the widget DOM nodes depending on hover/mouse press/focus // state changes, and also higher-level state changes such becoming disabled or selected. // // description: // By mixing this class into your widget, and setting the this.baseClass attribute, it will automatically // maintain CSS classes on the widget root node (this.domNode) depending on hover, // active, focus, etc. state. Ex: with a baseClass of dijitButton, it will apply the classes // dijitButtonHovered and dijitButtonActive, as the user moves the mouse over the widget and clicks it. // // It also sets CSS like dijitButtonDisabled based on widget semantic state. // // By setting the cssStateNodes attribute, a widget can also track events on subnodes (like buttons // within the widget). // cssStateNodes: [protected] Object // List of sub-nodes within the widget that need CSS classes applied on mouse hover/press and focus // // Each entry in the hash is a an attachpoint names (like "upArrowButton") mapped to a CSS class names // (like "dijitUpArrowButton"). Example: // | { // | "upArrowButton": "dijitUpArrowButton", // | "downArrowButton": "dijitDownArrowButton" // | } // The above will set the CSS class dijitUpArrowButton to the this.upArrowButton DOMNode when it // is hovered, etc. cssStateNodes: {}, // hovering: [readonly] Boolean // True if cursor is over this widget hovering: false, // active: [readonly] Boolean // True if mouse was pressed while over this widget, and hasn't been released yet active: false, _applyAttributes: function(){ // This code would typically be in postCreate(), but putting in _applyAttributes() for // performance: so the class changes happen before DOM is inserted into the document. // Change back to postCreate() in 2.0. See #11635. this.inherited(arguments); // Monitoring changes to disabled, readonly, etc. state, and update CSS class of root node array.forEach(["disabled", "readOnly", "checked", "selected", "focused", "state", "hovering", "active", "_opened"], function(attr){ this.watch(attr, lang.hitch(this, "_setStateClass")); }, this); // Track hover and active mouse events on widget root node, plus possibly on subnodes for(var ap in this.cssStateNodes){ this._trackMouseState(this[ap], this.cssStateNodes[ap]); } this._trackMouseState(this.domNode, this.baseClass); // Set state initially; there's probably no hover/active/focus state but widget might be // disabled/readonly/checked/selected so we want to set CSS classes for those conditions. this._setStateClass(); }, _cssMouseEvent: function(/*Event*/ event){ // summary: // Handler for CSS event on this.domNode. Sets hovering and active properties depending on mouse state, // which triggers _setStateClass() to set appropriate CSS classes for this.domNode. if(!this.disabled){ switch(event.type){ case "mouseover": this._set("hovering", true); this._set("active", this._mouseDown); break; case "mouseout": this._set("hovering", false); this._set("active", false); break; case "mousedown": case "touchstart": this._set("active", true); break; case "mouseup": case "touchend": this._set("active", false); break; } } }, _setStateClass: function(){ // summary: // Update the visual state of the widget by setting the css classes on this.domNode // (or this.stateNode if defined) by combining this.baseClass with // various suffixes that represent the current widget state(s). // // description: // In the case where a widget has multiple // states, it sets the class based on all possible // combinations. For example, an invalid form widget that is being hovered // will be "dijitInput dijitInputInvalid dijitInputHover dijitInputInvalidHover". // // The widget may have one or more of the following states, determined // by this.state, this.checked, this.valid, and this.selected: // // - Error - ValidationTextBox sets this.state to "Error" if the current input value is invalid // - Incomplete - ValidationTextBox sets this.state to "Incomplete" if the current input value is not finished yet // - Checked - ex: a checkmark or a ToggleButton in a checked state, will have this.checked==true // - Selected - ex: currently selected tab will have this.selected==true // // In addition, it may have one or more of the following states, // based on this.disabled and flags set in _onMouse (this.active, this.hovering) and from focus manager (this.focused): // // - Disabled - if the widget is disabled // - Active - if the mouse (or space/enter key?) is being pressed down // - Focused - if the widget has focus // - Hover - if the mouse is over the widget // Compute new set of classes var newStateClasses = this.baseClass.split(" "); function multiply(modifier){ newStateClasses = newStateClasses.concat(array.map(newStateClasses, function(c){ return c+modifier; }), "dijit"+modifier); } if(!this.isLeftToRight()){ // For RTL mode we need to set an addition class like dijitTextBoxRtl. multiply("Rtl"); } var checkedState = this.checked == "mixed" ? "Mixed" : (this.checked ? "Checked" : ""); if(this.checked){ multiply(checkedState); } if(this.state){ multiply(this.state); } if(this.selected){ multiply("Selected"); } if(this._opened){ multiply("Opened"); } if(this.disabled){ multiply("Disabled"); }else if(this.readOnly){ multiply("ReadOnly"); }else{ if(this.active){ multiply("Active"); }else if(this.hovering){ multiply("Hover"); } } if(this.focused){ multiply("Focused"); } // Remove old state classes and add new ones. // For performance concerns we only write into domNode.className once. var tn = this.stateNode || this.domNode, classHash = {}; // set of all classes (state and otherwise) for node array.forEach(tn.className.split(" "), function(c){ classHash[c] = true; }); if("_stateClasses" in this){ array.forEach(this._stateClasses, function(c){ delete classHash[c]; }); } array.forEach(newStateClasses, function(c){ classHash[c] = true; }); var newClasses = []; for(var c in classHash){ newClasses.push(c); } tn.className = newClasses.join(" "); this._stateClasses = newStateClasses; }, _subnodeCssMouseEvent: function(node, clazz, evt){ // summary: // Handler for hover/active mouse event on widget's subnode if(this.disabled || this.readOnly){ return; } function hover(isHovering){ domClass.toggle(node, clazz+"Hover", isHovering); } function active(isActive){ domClass.toggle(node, clazz+"Active", isActive); } function focused(isFocused){ domClass.toggle(node, clazz+"Focused", isFocused); } switch(evt.type){ case "mouseover": hover(true); break; case "mouseout": hover(false); active(false); break; case "mousedown": case "touchstart": active(true); break; case "mouseup": case "touchend": active(false); break; case "focus": case "focusin": focused(true); break; case "blur": case "focusout": focused(false); break; } }, _trackMouseState: function(/*DomNode*/ node, /*String*/ clazz){ // summary: // Track mouse/focus events on specified node and set CSS class on that node to indicate // current state. Usually not called directly, but via cssStateNodes attribute. // description: // Given class=foo, will set the following CSS class on the node // // - fooActive: if the user is currently pressing down the mouse button while over the node // - fooHover: if the user is hovering the mouse over the node, but not pressing down a button // - fooFocus: if the node is focused // // Note that it won't set any classes if the widget is disabled. // node: DomNode // Should be a sub-node of the widget, not the top node (this.domNode), since the top node // is handled specially and automatically just by mixing in this class. // clazz: String // CSS class name (ex: dijitSliderUpArrow) // Flag for listener code below to call this._cssMouseEvent() or this._subnodeCssMouseEvent() // when node is hovered/active node._cssState = clazz; } }); ready(function(){ // Document level listener to catch hover etc. events on widget root nodes and subnodes. // Note that when the mouse is moved quickly, a single onmouseenter event could signal that multiple widgets // have been hovered or unhovered (try test_Accordion.html) function handler(evt){ // Poor man's event propagation. Don't propagate event to ancestors of evt.relatedTarget, // to avoid processing mouseout events moving from a widget's domNode to a descendant node; // such events shouldn't be interpreted as a mouseleave on the widget. if(!dom.isDescendant(evt.relatedTarget, evt.target)){ for(var node = evt.target; node && node != evt.relatedTarget; node = node.parentNode){ // Process any nodes with _cssState property. They are generally widget root nodes, // but could also be sub-nodes within a widget if(node._cssState){ var widget = registry.getEnclosingWidget(node); if(widget){ if(node == widget.domNode){ // event on the widget's root node widget._cssMouseEvent(evt); }else{ // event on widget's sub-node widget._subnodeCssMouseEvent(node, node._cssState, evt); } } } } } } function ieHandler(evt){ evt.target = evt.srcElement; handler(evt); } // Use addEventListener() (and attachEvent() on IE) to catch the relevant events even if other handlers // (on individual nodes) call evt.stopPropagation() or event.stopEvent(). // Currently typematic.js is doing that, not sure why. // Don't monitor mouseover/mouseout on mobile because iOS generates "phantom" mouseover/mouseout events when // drag-scrolling, at the point in the viewport where the drag originated. Test the Tree in api viewer. var body = win.body(), types = (has("touch") ? [] : ["mouseover", "mouseout"]).concat(["mousedown", "touchstart", "mouseup", "touchend"]); array.forEach(types, function(type){ if(body.addEventListener){ body.addEventListener(type, handler, true); // W3C }else{ body.attachEvent("on"+type, ieHandler); // IE } }); // Track focus events on widget sub-nodes that have been registered via _trackMouseState(). // However, don't track focus events on the widget root nodes, because focus is tracked via the // focus manager (and it's not really tracking focus, but rather tracking that focus is on one of the widget's // nodes or a subwidget's node or a popup node, etc.) // Remove for 2.0 (if focus CSS needed, just use :focus pseudo-selector). on(body, "focusin, focusout", function(evt){ var node = evt.target; if(node._cssState && !node.getAttribute("widgetId")){ var widget = registry.getEnclosingWidget(node); widget._subnodeCssMouseEvent(node, node._cssState, evt); } }); }); return CssStateMixin; });