/**
* The OverlayContext Utility
*
* @module aui-overlay
* @submodule aui-overlay-context
*/
var L = A.Lang,
isString = L.isString,
isNumber = L.isNumber,
isObject = L.isObject,
isBoolean = L.isBoolean,
isNodeList = function(v) {
return (v instanceof A.NodeList);
},
ALIGN = 'align',
BL = 'bl',
BOUNDING_BOX = 'boundingBox',
CANCELLABLE_HIDE = 'cancellableHide',
OVERLAY_CONTEXT = 'overlaycontext',
CURRENT_NODE = 'currentNode',
FOCUSED = 'focused',
HIDE = 'hide',
HIDE_DELAY = 'hideDelay',
HIDE_ON = 'hideOn',
HIDE_ON_DOCUMENT_CLICK = 'hideOnDocumentClick',
MOUSEDOWN = 'mousedown',
SHOW = 'show',
SHOW_DELAY = 'showDelay',
SHOW_ON = 'showOn',
TL = 'tl',
TRIGGER = 'trigger',
USE_ARIA = 'useARIA',
VISIBLE = 'visible';
/**
* <p><img src="assets/images/aui-overlay-context/main.png"/></p>
*
* A base class for OverlayContext, providing:
* <ul>
* <li>Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)</li>
* <li>Able to display an <a href="Overlay.html">Overlay</a> at a specified corner of an element <a href="OverlayContext.html#config_trigger">trigger</a></li>
* </ul>
*
* Quick Example:<br/>
*
* <pre><code>var instance = new A.OverlayContext({
* boundingBox: '#OverlayBoundingBox',
* hideOn: 'mouseleave',
* showOn: 'mouseenter',
* trigger: '.menu-trigger'
* }).render();
* </code></pre>
*
* Check the list of <a href="OverlayContext.html#configattributes">Configuration Attributes</a> available for
* OverlayContext.
*
* @class OverlayContext
* @constructor
* @extends OverlayBase
* @param config {Object} Object literal specifying widget configuration properties.
*/
var OverlayContext = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property OverlayContext.NAME
* @type String
* @static
*/
NAME: OVERLAY_CONTEXT,
/**
* Static property used to define the default attribute
* configuration for the OverlayContext.
*
* @property OverlayContext.ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Inherited from <a href="Overlay.html#config_align">Overlay</a>.
*
* @attribute align
* @default { node: null, points: [ TL, BL ] }
* @type Object
*/
align: {
value: {
node: null,
points: [TL, BL]
}
},
/**
* Cancel auto hide delay if the user interact with the Overlay
* (focus, click, mouseover)
*
* @attribute cancellableHide
* @default true
* @type boolean
*/
cancellableHide: {
value: true,
validator: isBoolean
},
/**
* OverlayContext allow multiple elements to be the
* <a href="OverlayContext.html#config_trigger">trigger</a>, the
* currentNode stores the current active one.
*
* @attribute currentNode
* @default First item of the
* <a href="OverlayContext.html#config_trigger">trigger</a> NodeList.
* @type Node
*/
currentNode: {
valueFn: function() {
// define default currentNode as the first item from trigger
return this.get(TRIGGER).item(0);
}
},
delay: {
value: null,
validator: isObject
},
/**
* The event which is responsible to hide the OverlayContext.
*
* @attribute hideOn
* @default mouseout
* @type String
*/
hideOn: {
lazyAdd: false,
value: 'mouseout',
setter: function(v) {
return this._setHideOn(v);
}
},
/**
* If true the instance is registered on the
* <a href="OverlayContextManager.html">OverlayContextManager</a> static
* class and will be hide when the user click on document.
*
* @attribute hideOnDocumentClick
* @default true
* @type boolean
*/
hideOnDocumentClick: {
lazyAdd: false,
setter: function(v) {
return this._setHideOnDocumentClick(v);
},
value: true,
validator: isBoolean
},
/**
* Number of milliseconds after the hide method is invoked to hide the
* OverlayContext.
*
* @attribute hideDelay
* @default 0
* @type Number
*/
hideDelay: {
lazyAdd: false,
setter: '_setHideDelay',
value: 0,
validator: isNumber
},
/**
* The event which is responsible to show the OverlayContext.
*
* @attribute showOn
* @default mouseover
* @type String
*/
showOn: {
lazyAdd: false,
value: 'mouseover',
setter: function(v) {
return this._setShowOn(v);
}
},
/**
* Number of milliseconds after the show method is invoked to show the
* OverlayContext.
*
* @attribute showDelay
* @default 0
* @type Number
*/
showDelay: {
lazyAdd: false,
setter: '_setShowDelay',
value: 0,
validator: isNumber
},
/**
* Node, NodeList or Selector which will be used as trigger elements
* to show or hide the OverlayContext.
*
* @attribute trigger
* @default null
* @type {Node | NodeList | String}
*/
trigger: {
lazyAdd: false,
setter: function(v) {
if (isNodeList(v)) {
return v;
}
else if (isString(v)) {
return A.all(v);
}
return new A.NodeList([v]);
}
},
/**
* True if Overlay should use ARIA plugin
*
* @attribute useARIA
* @default true
* @type Boolean
*/
useARIA: {
value: true
},
/**
* If true the OverlayContext is visible by default after the render phase.
* Inherited from <a href="Overlay.html">Overlay</a>.
*
* @attribute visible
* @default false
* @type boolean
*/
visible: {
value: false
}
},
EXTENDS: A.OverlayBase,
constructor: function(config) {
var instance = this;
instance._showCallback = null;
instance._hideCallback = null;
OverlayContext.superclass.constructor.apply(this, arguments);
},
prototype: {
/**
* Construction logic executed during OverlayContext instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this;
var trigger = instance.get(TRIGGER);
if (trigger && trigger.size()) {
instance.set('align.node', trigger.item(0));
}
},
/**
* Bind the events on the OverlayContext UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var instance = this;
var boundingBox = instance.get(BOUNDING_BOX);
boundingBox.on(MOUSEDOWN, instance._stopTriggerEventPropagation);
instance.before('triggerChange', instance._beforeTriggerChange);
instance.before('showOnChange', instance._beforeShowOnChange);
instance.before('hideOnChange', instance._beforeHideOnChange);
instance.after('triggerChange', instance._afterTriggerChange);
instance.after('showOnChange', instance._afterShowOnChange);
instance.after('hideOnChange', instance._afterHideOnChange);
boundingBox.on('click', A.bind(instance._cancelAutoHide, instance));
boundingBox.on('mouseenter', A.bind(instance._cancelAutoHide, instance));
boundingBox.on('mouseleave', A.bind(instance._invokeHideTaskOnInteraction, instance));
instance.after('focusedChange', A.bind(instance._invokeHideTaskOnInteraction, instance));
instance.on('visibleChange', instance._onVisibleChangeOverlayContext);
},
/**
* Hides the OverlayContext.
*
* @method hide
*/
hide: function() {
var instance = this;
instance.clearIntervals();
instance.fire('hide');
OverlayContext.superclass.hide.apply(instance, arguments);
},
/**
* Shows the OverlayContext.
*
* @method hide
*/
show: function(event) {
var instance = this;
instance.clearIntervals();
instance.updateCurrentNode(event);
instance.fire('show');
OverlayContext.superclass.show.apply(instance, arguments);
instance.refreshAlign();
},
/**
* Refreshes the rendered UI, based on Widget State
*
* @method syncUI
* @protected
*
*/
syncUI: function() {
var instance = this;
if (instance.get(USE_ARIA)) {
instance.plug(A.Plugin.Aria, {
attributes: {
trigger: {
ariaName: 'controls',
format: function(value) {
var id = instance.get(BOUNDING_BOX).generateID();
return id;
},
node: function() {
return instance.get(TRIGGER);
}
},
visible: {
ariaName: 'hidden',
format: function(value) {
return !value;
}
}
},
roleName: 'dialog'
});
}
},
/**
* Toggles visibility of the OverlayContext.
*
* @method toggle
* @param {EventFacade} event
*/
toggle: function(event) {
var instance = this;
if (instance.get(VISIBLE)) {
instance._hideTask(event);
}
else {
instance._showTask(event);
}
},
/**
* Clear the intervals to show or hide the OverlayContext. See
* <a href="OverlayContext.html#config_hideDelay">hideDelay</a> and
* <a href="OverlayContext.html#config_showDelay">showDelay</a>.
*
* @method clearIntervals
*/
clearIntervals: function() {
this._hideTask.cancel();
this._showTask.cancel();
},
/**
* Refreshes the alignment of the OverlayContext with the
* <a href="OverlayContext.html#config_currentNode">currentNode</a>. See
* also <a href="OverlayContext.html#config_align">align</a>.
*
* @method refreshAlign
*/
refreshAlign: function() {
var instance = this;
var align = instance.get(ALIGN);
var currentNode = instance.get(CURRENT_NODE);
if (currentNode) {
instance._uiSetAlign(currentNode, align.points);
}
},
/**
* Update the
* <a href="OverlayContext.html#config_currentNode">currentNode</a> with the
* <a href="OverlayContext.html#config_align">align</a> node or the
* event.currentTarget and in last case with the first item of the
* <a href="OverlayContext.html#config_trigger">trigger</a>.
*
* @method updateCurrentNode
* @param {EventFacade} event
*/
updateCurrentNode: function(event) {
var instance = this;
var align = instance.get(ALIGN);
var trigger = instance.get(TRIGGER);
var currentTarget = null;
if (event) {
currentTarget = event.currentTarget;
}
var node = currentTarget || trigger.item(0) || align.node;
if (node) {
instance.set(CURRENT_NODE, node);
}
},
/**
* Handles the logic for the
* <a href="OverlayContext.html#method_toggle">toggle</a>.
*
* @method _toggle
* @param {EventFacade} event
* @protected
*/
_toggle: function(event) {
var instance = this;
if (instance.get('disabled')) {
return;
}
var currentTarget = event.currentTarget;
// check if the target is different and simulate a .hide() before toggle
if (instance._lastTarget != currentTarget) {
instance.hide();
}
instance.toggle(event);
event.stopPropagation();
instance._lastTarget = currentTarget;
},
/**
* Fires after the <a href="OverlayContext.html#config_showOn">showOn</a>
* attribute change.
*
* @method _afterShowOnChange
* @param {EventFacade} event
* @protected
*/
_afterShowOnChange: function(event) {
var instance = this;
var wasToggle = event.prevVal == instance.get(HIDE_ON);
if (wasToggle) {
var trigger = instance.get(TRIGGER);
// if wasToggle remove the toggle callback
trigger.detach(event.prevVal, instance._hideCallback);
// and re attach the hide event
instance._setHideOn(instance.get(HIDE_ON));
}
},
/**
* Fires after the <a href="OverlayContext.html#config_hideOn">hideOn</a>
* attribute change.
*
* @method _afterHideOnChange
* @param {EventFacade} event
* @protected
*/
_afterHideOnChange: function(event) {
var instance = this;
var wasToggle = event.prevVal == instance.get(SHOW_ON);
if (wasToggle) {
var trigger = instance.get(TRIGGER);
// if wasToggle remove the toggle callback
trigger.detach(event.prevVal, instance._showCallback);
// and re attach the show event
instance._setShowOn(instance.get(SHOW_ON));
}
},
/**
* Fires after the <a href="OverlayContext.html#config_trigger">trigger</a>
* attribute change.
*
* @method _afterTriggerChange
* @param {EventFacade} event
* @protected
*/
_afterTriggerChange: function(event) {
var instance = this;
instance._setShowOn(instance.get(SHOW_ON));
instance._setHideOn(instance.get(HIDE_ON));
},
/**
* Fires before the <a href="OverlayContext.html#config_showOn">showOn</a>
* attribute change.
*
* @method _beforeShowOnChange
* @param {EventFacade} event
* @protected
*/
_beforeShowOnChange: function(event) {
var instance = this;
var trigger = instance.get(TRIGGER);
// detach the old callback
trigger.detach(event.prevVal, instance._showCallback);
},
/**
* Fires before the <a href="OverlayContext.html#config_hideOn">hideOn</a>
* attribute change.
*
* @method _beforeHideOnChange
* @param {EventFacade} event
* @protected
*/
_beforeHideOnChange: function(event) {
var instance = this;
var trigger = instance.get(TRIGGER);
// detach the old callback
trigger.detach(event.prevVal, instance._hideCallback);
},
/**
* Fires before the <a href="OverlayContext.html#config_trigger">trigger</a>
* attribute change.
*
* @method _beforeTriggerChange
* @param {EventFacade} event
* @protected
*/
_beforeTriggerChange: function(event) {
var instance = this;
var trigger = instance.get(TRIGGER);
var showOn = instance.get(SHOW_ON);
var hideOn = instance.get(HIDE_ON);
trigger.detach(showOn, instance._showCallback);
trigger.detach(hideOn, instance._hideCallback);
trigger.detach(MOUSEDOWN, instance._stopTriggerEventPropagation);
},
/**
* Cancel hide event if the user does some interaction with the
* OverlayContext (focus, click or mouseover).
*
* @method _cancelAutoHide
* @param {EventFacade} event
* @protected
*/
_cancelAutoHide: function(event) {
var instance = this;
if (instance.get(CANCELLABLE_HIDE)) {
instance.clearIntervals();
}
event.stopPropagation();
},
/**
* Invoke the hide event when the OverlayContext looses the focus.
*
* @method _invokeHideTaskOnInteraction
* @param {EventFacade} event
* @protected
*/
_invokeHideTaskOnInteraction: function(event) {
var instance = this;
var cancellableHide = instance.get(CANCELLABLE_HIDE);
var focused = instance.get(FOCUSED);
if (!focused && !cancellableHide) {
instance._hideTask();
}
},
/**
* Fires when the <a href="OverlayContext.html#config_visible">visible</a>
* attribute changes.
*
* @method _onVisibleChangeOverlayContext
* @param {EventFacade} event
* @protected
*/
_onVisibleChangeOverlayContext: function(event) {
var instance = this;
if (event.newVal && instance.get('disabled')) {
event.preventDefault();
}
},
/**
* Helper method to invoke event.stopPropagation().
*
* @method _stopTriggerEventPropagation
* @param {EventFacade} event
* @protected
*/
_stopTriggerEventPropagation: function(event) {
event.stopPropagation();
},
/**
* Setter for the
* <a href="OverlayContext.html#config_hideDelay">hideDelay</a>
* attribute.
*
* @method _setHideDelay
* @param {number} val
* @protected
* @return {number}
*/
_setHideDelay: function(val) {
var instance = this;
instance._hideTask = A.debounce(instance.hide, val, instance);
return val;
},
/**
* Setter for the <a href="OverlayContext.html#config_hideOn">hideOn</a>
* attribute.
*
* @method _setHideOn
* @param {String} eventType Event type
* @protected
* @return {String}
*/
_setHideOn: function(eventType) {
var instance = this;
var trigger = instance.get(TRIGGER);
var toggle = eventType == instance.get(SHOW_ON);
if (toggle) {
instance._hideCallback = A.bind(instance._toggle, instance);
// only one attached event is enough for toggle
trigger.detach(eventType, instance._showCallback);
}
else {
var delay = instance.get(HIDE_DELAY);
instance._hideCallback = function(event) {
instance._hideTask(event);
event.stopPropagation();
};
}
trigger.on(eventType, instance._hideCallback);
return eventType;
},
/**
* Setter for the
* <a href="OverlayContext.html#config_hideOnDocumentClick">hideOnDocumentClick</a>
* attribute.
*
* @method _setHideOn
* @param {boolean} value
* @protected
* @return {boolean}
*/
_setHideOnDocumentClick: function(value) {
var instance = this;
if (value) {
A.OverlayContextManager.register(instance);
}
else {
A.OverlayContextManager.remove(instance);
}
return value;
},
/**
* Setter for the
* <a href="OverlayContext.html#config_showDelay">showDelay</a>
* attribute.
*
* @method _setShowDelay
* @param {number} val
* @protected
* @return {number}
*/
_setShowDelay: function(val) {
var instance = this;
instance._showTask = A.debounce(instance.show, val, instance);
return val;
},
/**
* Setter for the <a href="OverlayContext.html#config_showOn">showOn</a>
* attribute.
*
* @method _setShowOn
* @param {String} eventType Event type
* @protected
* @return {String}
*/
_setShowOn: function(eventType) {
var instance = this;
var trigger = instance.get(TRIGGER);
var toggle = eventType == instance.get(HIDE_ON);
if (toggle) {
instance._showCallback = A.bind(instance._toggle, instance);
// only one attached event is enough for toggle
trigger.detach(eventType, instance._hideCallback);
}
else {
var delay = instance.get(SHOW_DELAY);
instance._showCallback = function(event) {
instance._showTask(event);
event.stopPropagation();
};
}
if (eventType != MOUSEDOWN) {
trigger.on(MOUSEDOWN, instance._stopTriggerEventPropagation);
}
else {
trigger.detach(MOUSEDOWN, instance._stopTriggerEventPropagation);
}
trigger.on(eventType, instance._showCallback);
return eventType;
}
}
});
A.OverlayContext = OverlayContext;
/**
* A base class for OverlayContextManager:
*
* @param config {Object} Object literal specifying widget configuration properties.
*
* @class OverlayContextManager
* @constructor
* @extends OverlayManager
* @static
*/
A.OverlayContextManager = new A.OverlayManager({});
A.on(MOUSEDOWN, function() {
A.OverlayContextManager.hideAll();
}, A.getDoc());