/*
* Copyright 2012 Yahoo! Inc. All rights reserved.
* Licensed under the BSD License.
* http://yuilibrary.com/license/
*
* Portions Copyright 2012 Jens Elkner.
*/
// The tooltip in the YUI gallery makes nice effects but is not flexible enough.
// So we use a slightly modified version of the YUI examples.
// @see http://stage.yuilibrary.com/yui/docs/widget/widget-tooltip.html
YUI.add('tooltip', function(Y) {
var Lang = Y.Lang, Node = Y.Node;
/* Tooltip constructor */
function Tooltip(config) {
Tooltip.superclass.constructor.apply(this, arguments);
};
/*
* Required NAME static field, used to identify the Widget class and used
* as an event prefix, to generate class names etc. (set to the class name
* in camel case).
*/
Tooltip.NAME = 'tooltip';
/* Static constants */
Tooltip.OFFSET_X = 15;
Tooltip.OFFSET_Y = 15;
Tooltip.OFFSCREEN_X = -10000;
Tooltip.OFFSCREEN_Y = -10000;
/* Default Tooltip Attributes */
Tooltip.ATTRS = {
/*
* The tooltip content. This can either be a String, Node, or a simple
* map of id-to-values, designed to be used when a single tooltip is
* mapped to multiple trigger elements.
*/
content : {
value : null
},
/*
* The set of nodes to bind to the tooltip instance. Can be a string,
* or a node instance.
*/
triggerNodes : {
value : null,
setter : function(val) {
if (val && Lang.isString(val)) {
val = Node.all(val);
}
return val;
}
},
/*
* The delegate node to which event listeners should be attached. This
* node should be an ancestor of all trigger nodes bound to the
* instance. By default the document is used.
*/
delegate : {
value : null,
setter : function(val) {
return Y.one(val) || Y.one('document');
}
},
/*
* The time to wait, after the mouse enters the trigger node, to display
* the tooltip
*/
showDelay : {
value : 250
},
/*
* The time to wait, after the mouse leaves the trigger node, to hide
* the tooltip
*/
hideDelay : {
value : 10
},
/*
* The time to wait, after the tooltip is first displayed for a trigger
* node, to hide it, if the mouse has not left the trigger node
*/
autoHideDelay : {
value : 2000
},
/*
* Override the default visibility set by the widget base class
*/
visible : {
value : false
},
/*
* Override the default XY value set by the widget base class, to
* position the tooltip offscreen
*/
xy : {
value : [ Tooltip.OFFSCREEN_X, Tooltip.OFFSCREEN_Y ]
}
};
/* Extend the base Widget class */
Y.extend(Tooltip, Y.Widget, {
/*
* Initialization Code: Sets up privately used state properties, and
* publishes the events Tooltip introduces
*/
initializer : function(config) {
this._triggerClassName = this.getClassName('trigger');
// Currently bound trigger node information
this._currTrigger = {
node : null,
title : null,
mouseX : Tooltip.OFFSCREEN_X,
mouseY : Tooltip.OFFSCREEN_Y
};
// Event handles - mouse over is set on the delegate element,
// mousemove and mouseout are set on the trigger node
this._eventHandles = {
delegate : null,
trigger : {
mouseMove : null,
mouseOut : null
}
};
// Show/hide timers
this._timers = {
show : null,
hide : null
};
// Publish events introduced by Tooltip. Note the triggerEnter event
// is preventable, with the default behavior defined in the
// _defTriggerEnterFn method
this.publish('triggerEnter', {
defaultFn : this._defTriggerEnterFn,
preventable : true
});
this.publish('triggerLeave', {
preventable : false
});
// we assume, horizontal scrollbar has ~ same height as vert. and
// doesn't change
this.sbWidth = Y.DOM.getScrollbarWidth();
},
/*
* Destruction Code: Clears event handles, timers, and current trigger
* information
*/
destructor : function() {
this._clearCurrentTrigger();
this._clearTimers();
this._clearHandles();
},
/*
* bindUI is used to bind attribute change and dom event listeners
*/
bindUI : function() {
this.after('delegateChange', this._afterSetDelegate);
this.after('nodesChange', this._afterSetNodes);
this._bindDelegate();
},
/*
* syncUI is used to update the rendered DOM, based on the current
* Tooltip state
*/
syncUI : function() {
this._uiSetNodes(this.get('triggerNodes'));
},
/*
* Helper method to extract the content for the tooltip of the given
* node, based on (in order of precedence):
*
* a). The given node's 'content' attribute value, if set:
* 0) If its value denotes a function, the result of executing as
* function(node, this), whereby this is the Tooltip instance
* itself.
* 1) If the value is a map, the maps value for the key equal to
* the node's Id.
* 2) If the value is a String or Node, the value itself.
* 3) otherwise, fallback to b)
* b) The value of the node's 'title' attribute.
*
* @return either an innerHTML acceptable String or a Node
*/
getTooltipContent : function(node) {
var content = this.get('content');
if (content) {
if (Lang.isFunction(content)) {
content = content(node, this);
} else if (Y.Lang.isArray(content)) {
content = content[node.get('id')];
} else if (!(content instanceof Node || Y.Lang.isString(content))) {
content = null;
}
}
if (!content) {
content = node.getAttribute('title');
}
return content;
},
/*
* Public method, which can be used by triggerEvent event listeners to
* set the content of the tooltip for the current trigger node. This
* implementation uses {#getTooltipContent} to extract the content to
* show.
*/
setTriggerContent : function(node) {
var l, content = this.getTooltipContent(node),
contentBox = this.get('contentBox');
contentBox.set('innerHTML', '');
if (content) {
if (content instanceof Node) {
contentBox.appendChild(content);
} else if (Lang.isString(content)) {
contentBox.set('innerHTML',content);
}
}
},
/*
* Gets the closest ancestor of the given node, which is a tooltip
* trigger node
*/
getParentTrigger : function(node) {
var cn = this._triggerClassName;
return (node.hasClass(cn))
? node
: node.ancestor(function(node) { return node.hasClass(cn); });
},
/*
* Default attribute change listener for the triggerNodes attribute
*/
_afterSetNodes : function(e) {
this._uiSetNodes(e.newVal);
},
/*
* Default attribute change listener for the delegate attribute
*/
_afterSetDelegate : function(e) {
this._bindDelegate(e.newVal);
},
/*
* Updates the rendered DOM to reflect the set of trigger nodes passed in
*/
_uiSetNodes : function(nodes) {
if (this._triggerNodes) {
this._triggerNodes.removeClass(this._triggerClassName);
}
if (nodes) {
this._triggerNodes = nodes;
this._triggerNodes.addClass(this._triggerClassName);
}
},
/*
* Attaches the default mouseover DOM listener to the current delegate node
*/
_bindDelegate : function() {
var eventHandles = this._eventHandles;
if (eventHandles.delegate) {
eventHandles.delegate.detach();
eventHandles.delegate = null;
}
eventHandles.delegate = Y.on('mouseover',
Y.bind(this._onDelegateMouseOver, this), this.get('delegate'));
},
/*
* Default mouse over DOM event listener.
*
* Delegates to the _enterTrigger method, if the mouseover enters a
* trigger node.
*/
_onDelegateMouseOver : function(e) {
var node = this.getParentTrigger(e.target);
if (node && (!this._currTrigger.node
|| !node.compareTo(this._currTrigger.node)))
{
this._enterTrigger(node, e.pageX, e.pageY);
}
},
/*
* Default mouse out DOM event listener
*
* Delegates to _leaveTrigger if the mouseout leaves the current trigger
* node
*/
_onNodeMouseOut : function(e) {
var to = e.relatedTarget;
var trigger = e.currentTarget;
if (!trigger.contains(to)) {
this._leaveTrigger(trigger);
}
},
/*
* Default mouse move DOM event listener
*/
_onNodeMouseMove : function(e) {
this._overTrigger(e.pageX, e.pageY);
},
/*
* Default handler invoked when the mouse enters a trigger node. Set the
* content of the tooltip box and fires the triggerEnter event, which
* inturn notifies all listeners. Listeners may prevent the tooltip
* from being displayed.
*/
_enterTrigger : function(node, x, y) {
this._setCurrentTrigger(node, x, y);
this.fire('triggerEnter', {
node : node,
pageX : x,
pageY : y
});
},
/*
* Default handler for the triggerEvent event, which will setup the
* timer to display the tooltip, if the default handler has not been
* prevented.
*/
_defTriggerEnterFn : function(e) {
var node = e.node;
if (!this.get('disabled')) {
this._clearTimers();
var delay = (this.get('visible')) ? 0 : this.get('showDelay');
this._timers.show =
Y.later(delay, this, this._showTooltip, [ node ]);
}
},
/*
* Default handler invoked when the mouse leaves the current trigger
* node. Fires the triggerLeave event and sets up the hide timer
*/
_leaveTrigger : function(node) {
this.fire('triggerLeave');
this._clearCurrentTrigger();
this._clearTimers();
this._timers.hide =
Y.later(this.get('hideDelay'), this, this._hideTooltip);
},
/*
* Default handler invoked for mousemove events on the trigger node.
* Stores the current mouse x, y positions
*/
_overTrigger : function(x, y) {
this._currTrigger.mouseX = x;
this._currTrigger.mouseY = y;
},
/*
* Shows the tooltip, after moving it to the current mouse position.
*/
_showTooltip : function(node) {
var x = this._currTrigger.mouseX + Tooltip.OFFSET_X;
var y = this._currTrigger.mouseY + Tooltip.OFFSET_Y;
var cn = this.get('contentBox');
var max = Y.DOM.winHeight() - cn.get('clientHeight') - this.sbWidth;
if (y > max) {
y = max;
}
max = Y.DOM.winWidth() - cn.get('clientWidth') - this.sbWidth;
if (x > max) {
x = max;
}
this.move(x, y);
this.show();
this._clearTimers();
this._timers.hide =
Y.later(this.get('autoHideDelay'), this, this._hideTooltip);
},
/*
* Hides the tooltip, after clearing existing timers.
*/
_hideTooltip : function() {
this._clearTimers();
this.hide();
},
/*
* Set the currently bound trigger node information, clearing out the
* title attribute if set and setting up mousemove/out listeners.
*/
_setCurrentTrigger : function(node, x, y) {
var currTrigger = this._currTrigger, triggerHandles
= this._eventHandles.trigger;
this.setTriggerContent(node);
triggerHandles.mouseMove =
Y.on('mousemove', Y.bind(this._onNodeMouseMove, this), node);
triggerHandles.mouseOut =
Y.on('mouseout', Y.bind(this._onNodeMouseOut, this), node);
var title = node.getAttribute('title');
if (title) {
node.setAttribute('title', '');
}
currTrigger.mouseX = x;
currTrigger.mouseY = y;
currTrigger.node = node;
currTrigger.title = title;
},
/*
* Clear out the current trigger state, restoring the title attribute
* on the trigger node, if it was originally set.
*/
_clearCurrentTrigger : function() {
var currTrigger = this._currTrigger, triggerHandles =
this._eventHandles.trigger;
if (currTrigger.node) {
var node = currTrigger.node;
var title = currTrigger.title || '';
currTrigger.node = null;
currTrigger.title = '';
triggerHandles.mouseMove.detach();
triggerHandles.mouseOut.detach();
triggerHandles.mouseMove = null;
triggerHandles.mouseOut = null;
if (title) {
node.setAttribute('title', title);
}
}
},
/*
* Cancel any existing show/hide timers
*/
_clearTimers : function() {
var timers = this._timers;
if (timers.hide) {
timers.hide.cancel();
timers.hide = null;
}
if (timers.show) {
timers.show.cancel();
timers.show = null;
}
},
/*
* Detach any stored event handles
*/
_clearHandles : function() {
var eventHandles = this._eventHandles;
if (eventHandles.delegate) {
this._eventHandles.delegate.detach();
}
if (eventHandles.trigger.mouseOut) {
eventHandles.trigger.mouseOut.detach();
}
if (eventHandles.trigger.mouseMove) {
eventHandles.trigger.mouseMove.detach();
}
}
});
// dynamic:false = Modify the existing Tooltip class
Y.Tooltip = Y.Base.build(Tooltip.NAME, Tooltip,
[ Y.WidgetPosition, Y.WidgetStack ],
{ dynamic : false } );
}, '1.0', { requires: ['widget', 'widget-position', 'widget-stack']});
// vim: set filetype=javascript ts=4