define([ "dojo/_base/array", // array.forEach array.indexOf array.map "dojo/_base/declare", // declare "dojo/_base/fx", // fx.fadeIn fx.fadeOut "dojo/dom", // dom.byId "dojo/dom-class", // domClass.add "dojo/dom-geometry", // domGeometry.getMarginBox domGeometry.position "dojo/dom-style", // domStyle.set, domStyle.get "dojo/_base/lang", // lang.hitch lang.isArrayLike "dojo/_base/sniff", // has("ie") "dojo/_base/window", // win.body "./_base/manager", // manager.defaultDuration "./place", "./_Widget", "./_TemplatedMixin", "./BackgroundIframe", "dojo/text!./templates/Tooltip.html", "." // sets dijit.showTooltip etc. for back-compat ], function(array, declare, fx, dom, domClass, domGeometry, domStyle, lang, has, win, manager, place, _Widget, _TemplatedMixin, BackgroundIframe, template, dijit){ /*===== var _Widget = dijit._Widget; var BackgroundIframe = dijit.BackgroundIframe; var _TemplatedMixin = dijit._TemplatedMixin; =====*/ // module: // dijit/Tooltip // summary: // Defines dijit.Tooltip widget (to display a tooltip), showTooltip()/hideTooltip(), and _MasterTooltip var MasterTooltip = declare("dijit._MasterTooltip", [_Widget, _TemplatedMixin], { // summary: // Internal widget that holds the actual tooltip markup, // which occurs once per page. // Called by Tooltip widgets which are just containers to hold // the markup // tags: // protected // duration: Integer // Milliseconds to fade in/fade out duration: manager.defaultDuration, templateString: template, postCreate: function(){ win.body().appendChild(this.domNode); this.bgIframe = new BackgroundIframe(this.domNode); // Setup fade-in and fade-out functions. this.fadeIn = fx.fadeIn({ node: this.domNode, duration: this.duration, onEnd: lang.hitch(this, "_onShow") }); this.fadeOut = fx.fadeOut({ node: this.domNode, duration: this.duration, onEnd: lang.hitch(this, "_onHide") }); }, show: function(innerHTML, aroundNode, position, rtl, textDir){ // summary: // Display tooltip w/specified contents to right of specified node // (To left if there's no space on the right, or if rtl == true) // innerHTML: String // Contents of the tooltip // aroundNode: DomNode || dijit.__Rectangle // Specifies that tooltip should be next to this node / area // position: String[]? // List of positions to try to position tooltip (ex: ["right", "above"]) // rtl: Boolean? // Corresponds to `WidgetBase.dir` attribute, where false means "ltr" and true // means "rtl"; specifies GUI direction, not text direction. // textDir: String? // Corresponds to `WidgetBase.textdir` attribute; specifies direction of text. if(this.aroundNode && this.aroundNode === aroundNode && this.containerNode.innerHTML == innerHTML){ return; } // reset width; it may have been set by orient() on a previous tooltip show() this.domNode.width = "auto"; if(this.fadeOut.status() == "playing"){ // previous tooltip is being hidden; wait until the hide completes then show new one this._onDeck=arguments; return; } this.containerNode.innerHTML=innerHTML; this.set("textDir", textDir); this.containerNode.align = rtl? "right" : "left"; //fix the text alignment var pos = place.around(this.domNode, aroundNode, position && position.length ? position : Tooltip.defaultPosition, !rtl, lang.hitch(this, "orient")); // Position the tooltip connector for middle alignment. // This could not have been done in orient() since the tooltip wasn't positioned at that time. var aroundNodeCoords = pos.aroundNodePos; if(pos.corner.charAt(0) == 'M' && pos.aroundCorner.charAt(0) == 'M'){ this.connectorNode.style.top = aroundNodeCoords.y + ((aroundNodeCoords.h - this.connectorNode.offsetHeight) >> 1) - pos.y + "px"; this.connectorNode.style.left = ""; }else if(pos.corner.charAt(1) == 'M' && pos.aroundCorner.charAt(1) == 'M'){ this.connectorNode.style.left = aroundNodeCoords.x + ((aroundNodeCoords.w - this.connectorNode.offsetWidth) >> 1) - pos.x + "px"; } // show it domStyle.set(this.domNode, "opacity", 0); this.fadeIn.play(); this.isShowingNow = true; this.aroundNode = aroundNode; }, orient: function(/*DomNode*/ node, /*String*/ aroundCorner, /*String*/ tooltipCorner, /*Object*/ spaceAvailable, /*Object*/ aroundNodeCoords){ // summary: // Private function to set CSS for tooltip node based on which position it's in. // This is called by the dijit popup code. It will also reduce the tooltip's // width to whatever width is available // tags: // protected this.connectorNode.style.top = ""; //reset to default //Adjust the spaceAvailable width, without changing the spaceAvailable object var tooltipSpaceAvaliableWidth = spaceAvailable.w - this.connectorNode.offsetWidth; node.className = "dijitTooltip " + { "MR-ML": "dijitTooltipRight", "ML-MR": "dijitTooltipLeft", "TM-BM": "dijitTooltipAbove", "BM-TM": "dijitTooltipBelow", "BL-TL": "dijitTooltipBelow dijitTooltipABLeft", "TL-BL": "dijitTooltipAbove dijitTooltipABLeft", "BR-TR": "dijitTooltipBelow dijitTooltipABRight", "TR-BR": "dijitTooltipAbove dijitTooltipABRight", "BR-BL": "dijitTooltipRight", "BL-BR": "dijitTooltipLeft" }[aroundCorner + "-" + tooltipCorner]; // reduce tooltip's width to the amount of width available, so that it doesn't overflow screen this.domNode.style.width = "auto"; var size = domGeometry.getContentBox(this.domNode); var width = Math.min((Math.max(tooltipSpaceAvaliableWidth,1)), size.w); var widthWasReduced = width < size.w; this.domNode.style.width = width+"px"; //Adjust width for tooltips that have a really long word or a nowrap setting if(widthWasReduced){ this.containerNode.style.overflow = "auto"; //temp change to overflow to detect if our tooltip needs to be wider to support the content var scrollWidth = this.containerNode.scrollWidth; this.containerNode.style.overflow = "visible"; //change it back if(scrollWidth > width){ scrollWidth = scrollWidth + domStyle.get(this.domNode,"paddingLeft") + domStyle.get(this.domNode,"paddingRight"); this.domNode.style.width = scrollWidth + "px"; } } // Reposition the tooltip connector. if(tooltipCorner.charAt(0) == 'B' && aroundCorner.charAt(0) == 'B'){ var mb = domGeometry.getMarginBox(node); var tooltipConnectorHeight = this.connectorNode.offsetHeight; if(mb.h > spaceAvailable.h){ // The tooltip starts at the top of the page and will extend past the aroundNode var aroundNodePlacement = spaceAvailable.h - ((aroundNodeCoords.h + tooltipConnectorHeight) >> 1); this.connectorNode.style.top = aroundNodePlacement + "px"; this.connectorNode.style.bottom = ""; }else{ // Align center of connector with center of aroundNode, except don't let bottom // of connector extend below bottom of tooltip content, or top of connector // extend past top of tooltip content this.connectorNode.style.bottom = Math.min( Math.max(aroundNodeCoords.h/2 - tooltipConnectorHeight/2, 0), mb.h - tooltipConnectorHeight) + "px"; this.connectorNode.style.top = ""; } }else{ // reset the tooltip back to the defaults this.connectorNode.style.top = ""; this.connectorNode.style.bottom = ""; } return Math.max(0, size.w - tooltipSpaceAvaliableWidth); }, _onShow: function(){ // summary: // Called at end of fade-in operation // tags: // protected if(has("ie")){ // the arrow won't show up on a node w/an opacity filter this.domNode.style.filter=""; } }, hide: function(aroundNode){ // summary: // Hide the tooltip if(this._onDeck && this._onDeck[1] == aroundNode){ // this hide request is for a show() that hasn't even started yet; // just cancel the pending show() this._onDeck=null; }else if(this.aroundNode === aroundNode){ // this hide request is for the currently displayed tooltip this.fadeIn.stop(); this.isShowingNow = false; this.aroundNode = null; this.fadeOut.play(); }else{ // just ignore the call, it's for a tooltip that has already been erased } }, _onHide: function(){ // summary: // Called at end of fade-out operation // tags: // protected this.domNode.style.cssText=""; // to position offscreen again this.containerNode.innerHTML=""; if(this._onDeck){ // a show request has been queued up; do it now this.show.apply(this, this._onDeck); this._onDeck=null; } }, _setAutoTextDir: function(/*Object*/node){ // summary: // Resolve "auto" text direction for children nodes // tags: // private this.applyTextDir(node, has("ie") ? node.outerText : node.textContent); array.forEach(node.children, function(child){this._setAutoTextDir(child); }, this); }, _setTextDirAttr: function(/*String*/ textDir){ // summary: // Setter for textDir. // description: // Users shouldn't call this function; they should be calling // set('textDir', value) // tags: // private this._set("textDir", typeof textDir != 'undefined'? textDir : ""); if (textDir == "auto"){ this._setAutoTextDir(this.containerNode); }else{ this.containerNode.dir = this.textDir; } } }); dijit.showTooltip = function(innerHTML, aroundNode, position, rtl, textDir){ // summary: // Static method to display tooltip w/specified contents in specified position. // See description of dijit.Tooltip.defaultPosition for details on position parameter. // If position is not specified then dijit.Tooltip.defaultPosition is used. // innerHTML: String // Contents of the tooltip // aroundNode: dijit.__Rectangle // Specifies that tooltip should be next to this node / area // position: String[]? // List of positions to try to position tooltip (ex: ["right", "above"]) // rtl: Boolean? // Corresponds to `WidgetBase.dir` attribute, where false means "ltr" and true // means "rtl"; specifies GUI direction, not text direction. // textDir: String? // Corresponds to `WidgetBase.textdir` attribute; specifies direction of text. if(!Tooltip._masterTT){ dijit._masterTT = Tooltip._masterTT = new MasterTooltip(); } return Tooltip._masterTT.show(innerHTML, aroundNode, position, rtl, textDir); }; dijit.hideTooltip = function(aroundNode){ // summary: // Static method to hide the tooltip displayed via showTooltip() return Tooltip._masterTT && Tooltip._masterTT.hide(aroundNode); }; var Tooltip = declare("dijit.Tooltip", _Widget, { // summary: // Pops up a tooltip (a help message) when you hover over a node. // label: String // Text to display in the tooltip. // Specified as innerHTML when creating the widget from markup. label: "", // showDelay: Integer // Number of milliseconds to wait after hovering over/focusing on the object, before // the tooltip is displayed. showDelay: 400, // connectId: String|String[] // Id of domNode(s) to attach the tooltip to. // When user hovers over specified dom node, the tooltip will appear. connectId: [], // position: String[] // See description of `dijit.Tooltip.defaultPosition` for details on position parameter. position: [], _setConnectIdAttr: function(/*String|String[]*/ newId){ // summary: // Connect to specified node(s) // Remove connections to old nodes (if there are any) array.forEach(this._connections || [], function(nested){ array.forEach(nested, lang.hitch(this, "disconnect")); }, this); // Make array of id's to connect to, excluding entries for nodes that don't exist yet, see startup() this._connectIds = array.filter(lang.isArrayLike(newId) ? newId : (newId ? [newId] : []), function(id){ return dom.byId(id); }); // Make connections this._connections = array.map(this._connectIds, function(id){ var node = dom.byId(id); return [ this.connect(node, "onmouseenter", "_onHover"), this.connect(node, "onmouseleave", "_onUnHover"), this.connect(node, "onfocus", "_onHover"), this.connect(node, "onblur", "_onUnHover") ]; }, this); this._set("connectId", newId); }, addTarget: function(/*DOMNODE || String*/ node){ // summary: // Attach tooltip to specified node if it's not already connected // TODO: remove in 2.0 and just use set("connectId", ...) interface var id = node.id || node; if(array.indexOf(this._connectIds, id) == -1){ this.set("connectId", this._connectIds.concat(id)); } }, removeTarget: function(/*DomNode || String*/ node){ // summary: // Detach tooltip from specified node // TODO: remove in 2.0 and just use set("connectId", ...) interface var id = node.id || node, // map from DOMNode back to plain id string idx = array.indexOf(this._connectIds, id); if(idx >= 0){ // remove id (modifies original this._connectIds but that's OK in this case) this._connectIds.splice(idx, 1); this.set("connectId", this._connectIds); } }, buildRendering: function(){ this.inherited(arguments); domClass.add(this.domNode,"dijitTooltipData"); }, startup: function(){ this.inherited(arguments); // If this tooltip was created in a template, or for some other reason the specified connectId[s] // didn't exist during the widget's initialization, then connect now. var ids = this.connectId; array.forEach(lang.isArrayLike(ids) ? ids : [ids], this.addTarget, this); }, _onHover: function(/*Event*/ e){ // summary: // Despite the name of this method, it actually handles both hover and focus // events on the target node, setting a timer to show the tooltip. // tags: // private if(!this._showTimer){ var target = e.target; this._showTimer = setTimeout(lang.hitch(this, function(){this.open(target)}), this.showDelay); } }, _onUnHover: function(/*Event*/ /*===== e =====*/){ // summary: // Despite the name of this method, it actually handles both mouseleave and blur // events on the target node, hiding the tooltip. // tags: // private // keep a tooltip open if the associated element still has focus (even though the // mouse moved away) if(this._focus){ return; } if(this._showTimer){ clearTimeout(this._showTimer); delete this._showTimer; } this.close(); }, open: function(/*DomNode*/ target){ // summary: // Display the tooltip; usually not called directly. // tags: // private if(this._showTimer){ clearTimeout(this._showTimer); delete this._showTimer; } Tooltip.show(this.label || this.domNode.innerHTML, target, this.position, !this.isLeftToRight(), this.textDir); this._connectNode = target; this.onShow(target, this.position); }, close: function(){ // summary: // Hide the tooltip or cancel timer for show of tooltip // tags: // private if(this._connectNode){ // if tooltip is currently shown Tooltip.hide(this._connectNode); delete this._connectNode; this.onHide(); } if(this._showTimer){ // if tooltip is scheduled to be shown (after a brief delay) clearTimeout(this._showTimer); delete this._showTimer; } }, onShow: function(/*===== target, position =====*/){ // summary: // Called when the tooltip is shown // tags: // callback }, onHide: function(){ // summary: // Called when the tooltip is hidden // tags: // callback }, uninitialize: function(){ this.close(); this.inherited(arguments); } }); Tooltip._MasterTooltip = MasterTooltip; // for monkey patching Tooltip.show = dijit.showTooltip; // export function through module return value Tooltip.hide = dijit.hideTooltip; // export function through module return value // dijit.Tooltip.defaultPosition: String[] // This variable controls the position of tooltips, if the position is not specified to // the Tooltip widget or *TextBox widget itself. It's an array of strings with the following values: // // * before: places tooltip to the left of the target node/widget, or to the right in // the case of RTL scripts like Hebrew and Arabic // * after: places tooltip to the right of the target node/widget, or to the left in // the case of RTL scripts like Hebrew and Arabic // * above: tooltip goes above target node // * below: tooltip goes below target node // * top: tooltip goes above target node but centered connector // * bottom: tooltip goes below target node but centered connector // // The list is positions is tried, in order, until a position is found where the tooltip fits // within the viewport. // // Be careful setting this parameter. A value of "above" may work fine until the user scrolls // the screen so that there's no room above the target node. Nodes with drop downs, like // DropDownButton or FilteringSelect, are especially problematic, in that you need to be sure // that the drop down and tooltip don't overlap, even when the viewport is scrolled so that there // is only room below (or above) the target node, but not both. Tooltip.defaultPosition = ["after", "before"]; return Tooltip; });