325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
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;
|
|
});
|