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