Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / widgets / tips / ToolTip.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**\r
8  * @class Ext.ToolTip\r
9  * @extends Ext.Tip\r
10  * A standard tooltip implementation for providing additional information when hovering over a target element.\r
11  * @constructor\r
12  * Create a new Tooltip\r
13  * @param {Object} config The configuration options\r
14  */\r
15 Ext.ToolTip = Ext.extend(Ext.Tip, {\r
16     /**\r
17      * When a Tooltip is configured with the {@link #delegate} option to cause selected child elements of the {@link #target}\r
18      * Element to each trigger a seperate show event, this property is set to the DOM element which triggered the show.\r
19      * @type DOMElement\r
20      * @property triggerElement\r
21      */\r
22     /**\r
23      * @cfg {Mixed} target The target HTMLElement, Ext.Element or id to monitor for mouseover events to trigger\r
24      * showing this ToolTip.\r
25      */\r
26     /**\r
27      * @cfg {Boolean} autoHide True to automatically hide the tooltip after the mouse exits the target element\r
28      * or after the {@link #dismissDelay} has expired if set (defaults to true).  If {@link closable} = true a close\r
29      * tool button will be rendered into the tooltip header.\r
30      */\r
31     /**\r
32      * @cfg {Number} showDelay Delay in milliseconds before the tooltip displays after the mouse enters the\r
33      * target element (defaults to 500)\r
34      */\r
35     showDelay: 500,\r
36     /**\r
37      * @cfg {Number} hideDelay Delay in milliseconds after the mouse exits the target element but before the\r
38      * tooltip actually hides (defaults to 200).  Set to 0 for the tooltip to hide immediately.\r
39      */\r
40     hideDelay: 200,\r
41     /**\r
42      * @cfg {Number} dismissDelay Delay in milliseconds before the tooltip automatically hides (defaults to 5000).\r
43      * To disable automatic hiding, set dismissDelay = 0.\r
44      */\r
45     dismissDelay: 5000,\r
46     /**\r
47      * @cfg {Array} mouseOffset An XY offset from the mouse position where the tooltip should be shown (defaults to [15,18]).\r
48      */\r
49     /**\r
50      * @cfg {Boolean} trackMouse True to have the tooltip follow the mouse as it moves over the target element (defaults to false).\r
51      */\r
52     trackMouse : false,\r
53     /**\r
54      * @cfg {Boolean} anchorToTarget True to anchor the tooltip to the target element, false to\r
55      * anchor it relative to the mouse coordinates (defaults to true).  When anchorToTarget is\r
56      * true, use {@link #defaultAlign} to control tooltip alignment to the target element.  When\r
57      * anchorToTarget is false, use {@link #anchorPosition} instead to control alignment.\r
58      */\r
59     anchorToTarget: true,\r
60     /**\r
61      * @cfg {Number} anchorOffset A numeric pixel value used to offset the default position of the\r
62      * anchor arrow (defaults to 0).  When the anchor position is on the top or bottom of the tooltip,\r
63      * anchorOffset will be used as a horizontal offset.  Likewise, when the anchor position is on the\r
64      * left or right side, anchorOffset will be used as a vertical offset.\r
65      */\r
66     anchorOffset: 0,\r
67     /**\r
68      * @cfg {String} delegate <p>Optional. A {@link Ext.DomQuery DomQuery} selector which allows selection of individual elements\r
69      * within the {@link #target} element to trigger showing and hiding the ToolTip as the mouse moves within the target.</p>\r
70      * <p>When specified, the child element of the target which caused a show event is placed into the {@link #triggerElement} property\r
71      * before the ToolTip is shown.</p>\r
72      * <p>This may be useful when a Component has regular, repeating elements in it, each of which need a Tooltip which contains\r
73      * information specific to that element. For example:</p><pre><code>\r
74 var myGrid = new Ext.grid.gridPanel(gridConfig);\r
75 myGrid.on('render', function(grid) {\r
76     var store = grid.getStore();  // Capture the Store.\r
77     var view = grid.getView();    // Capture the GridView.\r
78     myGrid.tip = new Ext.ToolTip({\r
79         target: view.mainBody,    // The overall target element.\r
80         delegate: '.x-grid3-row', // Each grid row causes its own seperate show and hide.\r
81         trackMouse: true,         // Moving within the row should not hide the tip.\r
82         renderTo: document.body,  // Render immediately so that tip.body can be referenced prior to the first show.\r
83         listeners: {              // Change content dynamically depending on which element triggered the show.\r
84             beforeshow: function updateTipBody(tip) {\r
85                 var rowIndex = view.findRowIndex(tip.triggerElement);\r
86                 tip.body.dom.innerHTML = "Over Record ID " + store.getAt(rowIndex).id;\r
87             }\r
88         }\r
89     });\r
90 });</code></pre>\r
91      */\r
92 \r
93     // private\r
94     targetCounter: 0,\r
95 \r
96     constrainPosition: false,\r
97 \r
98     // private\r
99     initComponent: function(){\r
100         Ext.ToolTip.superclass.initComponent.call(this);\r
101         this.lastActive = new Date();\r
102         this.initTarget(this.target);\r
103         this.origAnchor = this.anchor;\r
104     },\r
105 \r
106     // private\r
107     onRender : function(ct, position){\r
108         Ext.ToolTip.superclass.onRender.call(this, ct, position);\r
109         this.anchorCls = 'x-tip-anchor-' + this.getAnchorPosition();\r
110         this.anchorEl = this.el.createChild({\r
111             cls: 'x-tip-anchor ' + this.anchorCls\r
112         });\r
113     },\r
114 \r
115     // private\r
116     afterRender : function(){\r
117         Ext.ToolTip.superclass.afterRender.call(this);\r
118         this.anchorEl.setStyle('z-index', this.el.getZIndex() + 1);\r
119     },\r
120 \r
121     /**\r
122      * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element.\r
123      * @param {Mixed} t The Element, HtmlElement, or ID of an element to bind to\r
124      */\r
125     initTarget : function(target){\r
126         var t;\r
127         if((t = Ext.get(target))){\r
128             if(this.target){\r
129                 this.target = Ext.get(this.target);\r
130                 this.target.un('mouseover', this.onTargetOver, this);\r
131                 this.target.un('mouseout', this.onTargetOut, this);\r
132                 this.target.un('mousemove', this.onMouseMove, this);\r
133             }\r
134             this.mon(t, {\r
135                 mouseover: this.onTargetOver,\r
136                 mouseout: this.onTargetOut,\r
137                 mousemove: this.onMouseMove,\r
138                 scope: this\r
139             });\r
140             this.target = t;\r
141         }\r
142         if(this.anchor){\r
143             this.anchorTarget = this.target;\r
144         }\r
145     },\r
146 \r
147     // private\r
148     onMouseMove : function(e){\r
149         var t = this.delegate ? e.getTarget(this.delegate) : this.triggerElement = true;\r
150         if (t) {\r
151             this.targetXY = e.getXY();\r
152             if (t === this.triggerElement) {\r
153                 if(!this.hidden && this.trackMouse){\r
154                     this.setPagePosition(this.getTargetXY());\r
155                 }\r
156             } else {\r
157                 this.hide();\r
158                 this.lastActive = new Date(0);\r
159                 this.onTargetOver(e);\r
160             }\r
161         } else if (!this.closable && this.isVisible()) {\r
162             this.hide();\r
163         }\r
164     },\r
165 \r
166     // private\r
167     getTargetXY : function(){\r
168         if(this.anchor){\r
169             this.targetCounter++;\r
170             var offsets = this.getOffsets();\r
171             var xy = (this.anchorToTarget && !this.trackMouse) ?\r
172                 this.el.getAlignToXY(this.anchorTarget, this.getAnchorAlign()) :\r
173                 this.targetXY;\r
174 \r
175             var dw = Ext.lib.Dom.getViewWidth()-5;\r
176             var dh = Ext.lib.Dom.getViewHeight()-5;\r
177             var scrollX = (document.documentElement.scrollLeft || document.body.scrollLeft || 0)+5;\r
178             var scrollY = (document.documentElement.scrollTop || document.body.scrollTop || 0)+5;\r
179 \r
180             var axy = [xy[0] + offsets[0], xy[1] + offsets[1]];\r
181             var sz = this.getSize();\r
182             this.anchorEl.removeClass(this.anchorCls);\r
183 \r
184             if(this.targetCounter < 2){\r
185                 if(axy[0] < scrollX){\r
186                     if(this.anchorToTarget){\r
187                         this.defaultAlign = 'l-r';\r
188                         if(this.mouseOffset){this.mouseOffset[0] *= -1;}\r
189                     }\r
190                     this.anchor = 'left';\r
191                     return this.getTargetXY();\r
192                 }\r
193                 if(axy[0]+sz.width > dw){\r
194                     if(this.anchorToTarget){\r
195                         this.defaultAlign = 'r-l';\r
196                         if(this.mouseOffset){this.mouseOffset[0] *= -1;}\r
197                     }\r
198                     this.anchor = 'right';\r
199                     return this.getTargetXY();\r
200                 }\r
201                 if(axy[1] < scrollY){\r
202                     if(this.anchorToTarget){\r
203                         this.defaultAlign = 't-b';\r
204                         if(this.mouseOffset){this.mouseOffset[1] *= -1;}\r
205                     }\r
206                     this.anchor = 'top';\r
207                     return this.getTargetXY();\r
208                 }\r
209                 if(axy[1]+sz.height > dh){\r
210                     if(this.anchorToTarget){\r
211                         this.defaultAlign = 'b-t';\r
212                         if(this.mouseOffset){this.mouseOffset[1] *= -1;}\r
213                     }\r
214                     this.anchor = 'bottom';\r
215                     return this.getTargetXY();\r
216                 }\r
217             }\r
218 \r
219             this.anchorCls = 'x-tip-anchor-'+this.getAnchorPosition();\r
220             this.anchorEl.addClass(this.anchorCls);\r
221             this.targetCounter = 0;\r
222             return axy;\r
223         }else{\r
224             var mouseOffset = this.getMouseOffset();\r
225             return [this.targetXY[0]+mouseOffset[0], this.targetXY[1]+mouseOffset[1]];\r
226         }\r
227     },\r
228 \r
229     getMouseOffset : function(){\r
230         var offset = this.anchor ? [0,0] : [15,18];\r
231         if(this.mouseOffset){\r
232             offset[0] += this.mouseOffset[0];\r
233             offset[1] += this.mouseOffset[1];\r
234         }\r
235         return offset;\r
236     },\r
237 \r
238     // private\r
239     getAnchorPosition : function(){\r
240         if(this.anchor){\r
241             this.tipAnchor = this.anchor.charAt(0);\r
242         }else{\r
243             var m = this.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/);\r
244             if(!m){\r
245                throw "AnchorTip.defaultAlign is invalid";\r
246             }\r
247             this.tipAnchor = m[1].charAt(0);\r
248         }\r
249 \r
250         switch(this.tipAnchor){\r
251             case 't': return 'top';\r
252             case 'b': return 'bottom';\r
253             case 'r': return 'right';\r
254         }\r
255         return 'left';\r
256     },\r
257 \r
258     // private\r
259     getAnchorAlign : function(){\r
260         switch(this.anchor){\r
261             case 'top'  : return 'tl-bl';\r
262             case 'left' : return 'tl-tr';\r
263             case 'right': return 'tr-tl';\r
264             default     : return 'bl-tl';\r
265         }\r
266     },\r
267 \r
268     // private\r
269     getOffsets: function(){\r
270         var offsets, ap = this.getAnchorPosition().charAt(0);\r
271         if(this.anchorToTarget && !this.trackMouse){\r
272             switch(ap){\r
273                 case 't':\r
274                     offsets = [0, 9];\r
275                     break;\r
276                 case 'b':\r
277                     offsets = [0, -13];\r
278                     break;\r
279                 case 'r':\r
280                     offsets = [-13, 0];\r
281                     break;\r
282                 default:\r
283                     offsets = [9, 0];\r
284                     break;\r
285             }\r
286         }else{\r
287             switch(ap){\r
288                 case 't':\r
289                     offsets = [-15-this.anchorOffset, 30];\r
290                     break;\r
291                 case 'b':\r
292                     offsets = [-19-this.anchorOffset, -13-this.el.dom.offsetHeight];\r
293                     break;\r
294                 case 'r':\r
295                     offsets = [-15-this.el.dom.offsetWidth, -13-this.anchorOffset];\r
296                     break;\r
297                 default:\r
298                     offsets = [25, -13-this.anchorOffset];\r
299                     break;\r
300             }\r
301         }\r
302         var mouseOffset = this.getMouseOffset();\r
303         offsets[0] += mouseOffset[0];\r
304         offsets[1] += mouseOffset[1];\r
305 \r
306         return offsets;\r
307     },\r
308 \r
309     // private\r
310     onTargetOver : function(e){\r
311         if(this.disabled || e.within(this.target.dom, true)){\r
312             return;\r
313         }\r
314         var t = e.getTarget(this.delegate);\r
315         if (t) {\r
316             this.triggerElement = t;\r
317             this.clearTimer('hide');\r
318             this.targetXY = e.getXY();\r
319             this.delayShow();\r
320         }\r
321     },\r
322 \r
323     // private\r
324     delayShow : function(){\r
325         if(this.hidden && !this.showTimer){\r
326             if(this.lastActive.getElapsed() < this.quickShowInterval){\r
327                 this.show();\r
328             }else{\r
329                 this.showTimer = this.show.defer(this.showDelay, this);\r
330             }\r
331         }else if(!this.hidden && this.autoHide !== false){\r
332             this.show();\r
333         }\r
334     },\r
335 \r
336     // private\r
337     onTargetOut : function(e){\r
338         if(this.disabled || e.within(this.target.dom, true)){\r
339             return;\r
340         }\r
341         this.clearTimer('show');\r
342         if(this.autoHide !== false){\r
343             this.delayHide();\r
344         }\r
345     },\r
346 \r
347     // private\r
348     delayHide : function(){\r
349         if(!this.hidden && !this.hideTimer){\r
350             this.hideTimer = this.hide.defer(this.hideDelay, this);\r
351         }\r
352     },\r
353 \r
354     /**\r
355      * Hides this tooltip if visible.\r
356      */\r
357     hide: function(){\r
358         this.clearTimer('dismiss');\r
359         this.lastActive = new Date();\r
360         if(this.anchorEl){\r
361             this.anchorEl.hide();\r
362         }\r
363         Ext.ToolTip.superclass.hide.call(this);\r
364         delete this.triggerElement;\r
365     },\r
366 \r
367     /**\r
368      * Shows this tooltip at the current event target XY position.\r
369      */\r
370     show : function(){\r
371         if(this.anchor){\r
372             // pre-show it off screen so that the el will have dimensions\r
373             // for positioning calcs when getting xy next\r
374             this.showAt([-1000,-1000]);\r
375             this.origConstrainPosition = this.constrainPosition;\r
376             this.constrainPosition = false;\r
377             this.anchor = this.origAnchor;\r
378         }\r
379         this.showAt(this.getTargetXY());\r
380 \r
381         if(this.anchor){\r
382             this.syncAnchor();\r
383             this.anchorEl.show();\r
384             this.constrainPosition = this.origConstrainPosition;\r
385         }else{\r
386             this.anchorEl.hide();\r
387         }\r
388     },\r
389 \r
390     // inherit docs\r
391     showAt : function(xy){\r
392         this.lastActive = new Date();\r
393         this.clearTimers();\r
394         Ext.ToolTip.superclass.showAt.call(this, xy);\r
395         if(this.dismissDelay && this.autoHide !== false){\r
396             this.dismissTimer = this.hide.defer(this.dismissDelay, this);\r
397         }\r
398     },\r
399 \r
400     // private\r
401     syncAnchor : function(){\r
402         var anchorPos, targetPos, offset;\r
403         switch(this.tipAnchor.charAt(0)){\r
404             case 't':\r
405                 anchorPos = 'b';\r
406                 targetPos = 'tl';\r
407                 offset = [20+this.anchorOffset, 2];\r
408                 break;\r
409             case 'r':\r
410                 anchorPos = 'l';\r
411                 targetPos = 'tr';\r
412                 offset = [-2, 11+this.anchorOffset];\r
413                 break;\r
414             case 'b':\r
415                 anchorPos = 't';\r
416                 targetPos = 'bl';\r
417                 offset = [20+this.anchorOffset, -2];\r
418                 break;\r
419             default:\r
420                 anchorPos = 'r';\r
421                 targetPos = 'tl';\r
422                 offset = [2, 11+this.anchorOffset];\r
423                 break;\r
424         }\r
425         this.anchorEl.alignTo(this.el, anchorPos+'-'+targetPos, offset);\r
426     },\r
427 \r
428     // private\r
429     setPagePosition : function(x, y){\r
430         Ext.ToolTip.superclass.setPagePosition.call(this, x, y);\r
431         if(this.anchor){\r
432             this.syncAnchor();\r
433         }\r
434     },\r
435 \r
436     // private\r
437     clearTimer : function(name){\r
438         name = name + 'Timer';\r
439         clearTimeout(this[name]);\r
440         delete this[name];\r
441     },\r
442 \r
443     // private\r
444     clearTimers : function(){\r
445         this.clearTimer('show');\r
446         this.clearTimer('dismiss');\r
447         this.clearTimer('hide');\r
448     },\r
449 \r
450     // private\r
451     onShow : function(){\r
452         Ext.ToolTip.superclass.onShow.call(this);\r
453         Ext.getDoc().on('mousedown', this.onDocMouseDown, this);\r
454     },\r
455 \r
456     // private\r
457     onHide : function(){\r
458         Ext.ToolTip.superclass.onHide.call(this);\r
459         Ext.getDoc().un('mousedown', this.onDocMouseDown, this);\r
460     },\r
461 \r
462     // private\r
463     onDocMouseDown : function(e){\r
464         if(this.autoHide !== true && !this.closable && !e.within(this.el.dom)){\r
465             this.disable();\r
466             this.enable.defer(100, this);\r
467         }\r
468     },\r
469 \r
470     // private\r
471     onDisable : function(){\r
472         this.clearTimers();\r
473         this.hide();\r
474     },\r
475 \r
476     // private\r
477     adjustPosition : function(x, y){\r
478         if(this.contstrainPosition){\r
479             var ay = this.targetXY[1], h = this.getSize().height;\r
480             if(y <= ay && (y+h) >= ay){\r
481                 y = ay-h-5;\r
482             }\r
483         }\r
484         return {x : x, y: y};\r
485     },\r
486 \r
487     // private\r
488     onDestroy : function(){\r
489         Ext.getDoc().un('mousedown', this.onDocMouseDown, this);\r
490         Ext.ToolTip.superclass.onDestroy.call(this);\r
491     }\r
492 });