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