Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / tip / ToolTip.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * ToolTip is a {@link Ext.tip.Tip} implementation that handles the common case of displaying a
17  * tooltip when hovering over a certain element or elements on the page. It allows fine-grained
18  * control over the tooltip's alignment relative to the target element or mouse, and the timing
19  * of when it is automatically shown and hidden.
20  * 
21  * This implementation does **not** have a built-in method of automatically populating the tooltip's
22  * text based on the target element; you must either configure a fixed {@link #html} value for each
23  * ToolTip instance, or implement custom logic (e.g. in a {@link #beforeshow} event listener) to
24  * generate the appropriate tooltip content on the fly. See {@link Ext.tip.QuickTip} for a more
25  * convenient way of automatically populating and configuring a tooltip based on specific DOM
26  * attributes of each target element.
27  * 
28  * # Basic Example
29  * 
30  *     var tip = Ext.create('Ext.tip.ToolTip', {
31  *         target: 'clearButton',
32  *         html: 'Press this button to clear the form'
33  *     });
34  * 
35  * {@img Ext.tip.ToolTip/Ext.tip.ToolTip1.png Basic Ext.tip.ToolTip}
36  * 
37  * # Delegation
38  * 
39  * In addition to attaching a ToolTip to a single element, you can also use delegation to attach
40  * one ToolTip to many elements under a common parent. This is more efficient than creating many
41  * ToolTip instances. To do this, point the {@link #target} config to a common ancestor of all the
42  * elements, and then set the {@link #delegate} config to a CSS selector that will select all the
43  * appropriate sub-elements.
44  * 
45  * When using delegation, it is likely that you will want to programmatically change the content
46  * of the ToolTip based on each delegate element; you can do this by implementing a custom
47  * listener for the {@link #beforeshow} event. Example:
48  * 
49  *     var store = Ext.create('Ext.data.ArrayStore', {
50  *         fields: ['company', 'price', 'change'],
51  *         data: [
52  *             ['3m Co',                               71.72, 0.02],
53  *             ['Alcoa Inc',                           29.01, 0.42],
54  *             ['Altria Group Inc',                    83.81, 0.28],
55  *             ['American Express Company',            52.55, 0.01],
56  *             ['American International Group, Inc.',  64.13, 0.31],
57  *             ['AT&T Inc.',                           31.61, -0.48]
58  *         ]
59  *     });
60  *  
61  *     var grid = Ext.create('Ext.grid.Panel', {
62  *         title: 'Array Grid',
63  *         store: store,
64  *         columns: [
65  *             {text: 'Company', flex: 1, dataIndex: 'company'},
66  *             {text: 'Price', width: 75, dataIndex: 'price'},
67  *             {text: 'Change', width: 75, dataIndex: 'change'}
68  *         ],
69  *         height: 200,
70  *         width: 400,
71  *         renderTo: Ext.getBody()
72  *     });
73  *  
74  *     grid.getView().on('render', function(view) {
75  *         view.tip = Ext.create('Ext.tip.ToolTip', {
76  *             // The overall target element.
77  *             target: view.el,
78  *             // Each grid row causes its own seperate show and hide.
79  *             delegate: view.itemSelector,
80  *             // Moving within the row should not hide the tip.
81  *             trackMouse: true,
82  *             // Render immediately so that tip.body can be referenced prior to the first show.
83  *             renderTo: Ext.getBody(),
84  *             listeners: {
85  *                 // Change content dynamically depending on which element triggered the show.
86  *                 beforeshow: function updateTipBody(tip) {
87  *                     tip.update('Over company "' + view.getRecord(tip.triggerElement).get('company') + '"');
88  *                 }
89  *             }
90  *         });
91  *     });
92  * 
93  * {@img Ext.tip.ToolTip/Ext.tip.ToolTip2.png Ext.tip.ToolTip with delegation}
94  * 
95  * # Alignment
96  * 
97  * The following configuration properties allow control over how the ToolTip is aligned relative to
98  * the target element and/or mouse pointer:
99  * 
100  * - {@link #anchor}
101  * - {@link #anchorToTarget}
102  * - {@link #anchorOffset}
103  * - {@link #trackMouse}
104  * - {@link #mouseOffset}
105  * 
106  * # Showing/Hiding
107  * 
108  * The following configuration properties allow control over how and when the ToolTip is automatically
109  * shown and hidden:
110  * 
111  * - {@link #autoHide}
112  * - {@link #showDelay}
113  * - {@link #hideDelay}
114  * - {@link #dismissDelay}
115  * 
116  * @docauthor Jason Johnston <jason@sencha.com>
117  */
118 Ext.define('Ext.tip.ToolTip', {
119     extend: 'Ext.tip.Tip',
120     alias: 'widget.tooltip',
121     alternateClassName: 'Ext.ToolTip',
122     /**
123      * When a ToolTip is configured with the `{@link #delegate}`
124      * option to cause selected child elements of the `{@link #target}`
125      * Element to each trigger a seperate show event, this property is set to
126      * the DOM element which triggered the show.
127      * @type DOMElement
128      * @property triggerElement
129      */
130     /**
131      * @cfg {Mixed} target The target HTMLElement, Ext.core.Element or id to monitor
132      * for mouseover events to trigger showing this ToolTip.
133      */
134     /**
135      * @cfg {Boolean} autoHide True to automatically hide the tooltip after the
136      * mouse exits the target element or after the `{@link #dismissDelay}`
137      * has expired if set (defaults to true).  If `{@link #closable} = true`
138      * a close tool button will be rendered into the tooltip header.
139      */
140     /**
141      * @cfg {Number} showDelay Delay in milliseconds before the tooltip displays
142      * after the mouse enters the target element (defaults to 500)
143      */
144     showDelay: 500,
145     /**
146      * @cfg {Number} hideDelay Delay in milliseconds after the mouse exits the
147      * target element but before the tooltip actually hides (defaults to 200).
148      * Set to 0 for the tooltip to hide immediately.
149      */
150     hideDelay: 200,
151     /**
152      * @cfg {Number} dismissDelay Delay in milliseconds before the tooltip
153      * automatically hides (defaults to 5000). To disable automatic hiding, set
154      * dismissDelay = 0.
155      */
156     dismissDelay: 5000,
157     /**
158      * @cfg {Array} mouseOffset An XY offset from the mouse position where the
159      * tooltip should be shown (defaults to [15,18]).
160      */
161     /**
162      * @cfg {Boolean} trackMouse True to have the tooltip follow the mouse as it
163      * moves over the target element (defaults to false).
164      */
165     trackMouse: false,
166     /**
167      * @cfg {String} anchor If specified, indicates that the tip should be anchored to a
168      * particular side of the target element or mouse pointer ("top", "right", "bottom",
169      * or "left"), with an arrow pointing back at the target or mouse pointer. If
170      * {@link #constrainPosition} is enabled, this will be used as a preferred value
171      * only and may be flipped as needed.
172      */
173     /**
174      * @cfg {Boolean} anchorToTarget True to anchor the tooltip to the target
175      * element, false to anchor it relative to the mouse coordinates (defaults
176      * to true).  When `anchorToTarget` is true, use
177      * `{@link #defaultAlign}` to control tooltip alignment to the
178      * target element.  When `anchorToTarget` is false, use
179      * `{@link #anchorPosition}` instead to control alignment.
180      */
181     anchorToTarget: true,
182     /**
183      * @cfg {Number} anchorOffset A numeric pixel value used to offset the
184      * default position of the anchor arrow (defaults to 0).  When the anchor
185      * position is on the top or bottom of the tooltip, `anchorOffset`
186      * will be used as a horizontal offset.  Likewise, when the anchor position
187      * is on the left or right side, `anchorOffset` will be used as
188      * a vertical offset.
189      */
190     anchorOffset: 0,
191     /**
192      * @cfg {String} delegate
193      *
194      * A {@link Ext.DomQuery DomQuery} selector which allows selection of individual elements within the
195      * `{@link #target}` element to trigger showing and hiding the ToolTip as the mouse moves within the
196      * target.
197      *
198      * When specified, the child element of the target which caused a show event is placed into the
199      * `{@link #triggerElement}` property before the ToolTip is shown.
200      *
201      * This may be useful when a Component has regular, repeating elements in it, each of which need a
202      * ToolTip which contains information specific to that element.
203      * 
204      * See the delegate example in class documentation of {@link Ext.tip.ToolTip}.
205      */
206
207     // private
208     targetCounter: 0,
209     quickShowInterval: 250,
210
211     // private
212     initComponent: function() {
213         var me = this;
214         me.callParent(arguments);
215         me.lastActive = new Date();
216         me.setTarget(me.target);
217         me.origAnchor = me.anchor;
218     },
219
220     // private
221     onRender: function(ct, position) {
222         var me = this;
223         me.callParent(arguments);
224         me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
225         me.anchorEl = me.el.createChild({
226             cls: Ext.baseCSSPrefix + 'tip-anchor ' + me.anchorCls
227         });
228     },
229
230     // private
231     afterRender: function() {
232         var me = this,
233             zIndex;
234
235         me.callParent(arguments);
236         zIndex = parseInt(me.el.getZIndex(), 10) || 0;
237         me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.core.Element.DISPLAY);
238     },
239
240     /**
241      * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element.
242      * @param {Mixed} t The Element, HtmlElement, or ID of an element to bind to
243      */
244     setTarget: function(target) {
245         var me = this,
246             t = Ext.get(target),
247             tg;
248
249         if (me.target) {
250             tg = Ext.get(me.target);
251             me.mun(tg, 'mouseover', me.onTargetOver, me);
252             me.mun(tg, 'mouseout', me.onTargetOut, me);
253             me.mun(tg, 'mousemove', me.onMouseMove, me);
254         }
255         
256         me.target = t;
257         if (t) {
258             
259             me.mon(t, {
260                 // TODO - investigate why IE6/7 seem to fire recursive resize in e.getXY
261                 // breaking QuickTip#onTargetOver (EXTJSIV-1608)
262                 freezeEvent: true,
263
264                 mouseover: me.onTargetOver,
265                 mouseout: me.onTargetOut,
266                 mousemove: me.onMouseMove,
267                 scope: me
268             });
269         }
270         if (me.anchor) {
271             me.anchorTarget = me.target;
272         }
273     },
274
275     // private
276     onMouseMove: function(e) {
277         var me = this,
278             t = me.delegate ? e.getTarget(me.delegate) : me.triggerElement = true,
279             xy;
280         if (t) {
281             me.targetXY = e.getXY();
282             if (t === me.triggerElement) {
283                 if (!me.hidden && me.trackMouse) {
284                     xy = me.getTargetXY();
285                     if (me.constrainPosition) {
286                         xy = me.el.adjustForConstraints(xy, me.el.dom.parentNode);
287                     }
288                     me.setPagePosition(xy);
289                 }
290             } else {
291                 me.hide();
292                 me.lastActive = new Date(0);
293                 me.onTargetOver(e);
294             }
295         } else if ((!me.closable && me.isVisible()) && me.autoHide !== false) {
296             me.hide();
297         }
298     },
299
300     // private
301     getTargetXY: function() {
302         var me = this,
303             mouseOffset;
304         if (me.delegate) {
305             me.anchorTarget = me.triggerElement;
306         }
307         if (me.anchor) {
308             me.targetCounter++;
309                 var offsets = me.getOffsets(),
310                     xy = (me.anchorToTarget && !me.trackMouse) ? me.el.getAlignToXY(me.anchorTarget, me.getAnchorAlign()) : me.targetXY,
311                     dw = Ext.core.Element.getViewWidth() - 5,
312                     dh = Ext.core.Element.getViewHeight() - 5,
313                     de = document.documentElement,
314                     bd = document.body,
315                     scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5,
316                     scrollY = (de.scrollTop || bd.scrollTop || 0) + 5,
317                     axy = [xy[0] + offsets[0], xy[1] + offsets[1]],
318                     sz = me.getSize(),
319                     constrainPosition = me.constrainPosition;
320
321             me.anchorEl.removeCls(me.anchorCls);
322
323             if (me.targetCounter < 2 && constrainPosition) {
324                 if (axy[0] < scrollX) {
325                     if (me.anchorToTarget) {
326                         me.defaultAlign = 'l-r';
327                         if (me.mouseOffset) {
328                             me.mouseOffset[0] *= -1;
329                         }
330                     }
331                     me.anchor = 'left';
332                     return me.getTargetXY();
333                 }
334                 if (axy[0] + sz.width > dw) {
335                     if (me.anchorToTarget) {
336                         me.defaultAlign = 'r-l';
337                         if (me.mouseOffset) {
338                             me.mouseOffset[0] *= -1;
339                         }
340                     }
341                     me.anchor = 'right';
342                     return me.getTargetXY();
343                 }
344                 if (axy[1] < scrollY) {
345                     if (me.anchorToTarget) {
346                         me.defaultAlign = 't-b';
347                         if (me.mouseOffset) {
348                             me.mouseOffset[1] *= -1;
349                         }
350                     }
351                     me.anchor = 'top';
352                     return me.getTargetXY();
353                 }
354                 if (axy[1] + sz.height > dh) {
355                     if (me.anchorToTarget) {
356                         me.defaultAlign = 'b-t';
357                         if (me.mouseOffset) {
358                             me.mouseOffset[1] *= -1;
359                         }
360                     }
361                     me.anchor = 'bottom';
362                     return me.getTargetXY();
363                 }
364             }
365
366             me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
367             me.anchorEl.addCls(me.anchorCls);
368             me.targetCounter = 0;
369             return axy;
370         } else {
371             mouseOffset = me.getMouseOffset();
372             return (me.targetXY) ? [me.targetXY[0] + mouseOffset[0], me.targetXY[1] + mouseOffset[1]] : mouseOffset;
373         }
374     },
375
376     getMouseOffset: function() {
377         var me = this,
378         offset = me.anchor ? [0, 0] : [15, 18];
379         if (me.mouseOffset) {
380             offset[0] += me.mouseOffset[0];
381             offset[1] += me.mouseOffset[1];
382         }
383         return offset;
384     },
385
386     // private
387     getAnchorPosition: function() {
388         var me = this,
389             m;
390         if (me.anchor) {
391             me.tipAnchor = me.anchor.charAt(0);
392         } else {
393             m = me.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/);
394             //<debug>
395             if (!m) {
396                 Ext.Error.raise('The AnchorTip.defaultAlign value "' + me.defaultAlign + '" is invalid.');
397             }
398             //</debug>
399             me.tipAnchor = m[1].charAt(0);
400         }
401
402         switch (me.tipAnchor) {
403         case 't':
404             return 'top';
405         case 'b':
406             return 'bottom';
407         case 'r':
408             return 'right';
409         }
410         return 'left';
411     },
412
413     // private
414     getAnchorAlign: function() {
415         switch (this.anchor) {
416         case 'top':
417             return 'tl-bl';
418         case 'left':
419             return 'tl-tr';
420         case 'right':
421             return 'tr-tl';
422         default:
423             return 'bl-tl';
424         }
425     },
426
427     // private
428     getOffsets: function() {
429         var me = this,
430             mouseOffset,
431             offsets,
432             ap = me.getAnchorPosition().charAt(0);
433         if (me.anchorToTarget && !me.trackMouse) {
434             switch (ap) {
435             case 't':
436                 offsets = [0, 9];
437                 break;
438             case 'b':
439                 offsets = [0, -13];
440                 break;
441             case 'r':
442                 offsets = [ - 13, 0];
443                 break;
444             default:
445                 offsets = [9, 0];
446                 break;
447             }
448         } else {
449             switch (ap) {
450             case 't':
451                 offsets = [ - 15 - me.anchorOffset, 30];
452                 break;
453             case 'b':
454                 offsets = [ - 19 - me.anchorOffset, -13 - me.el.dom.offsetHeight];
455                 break;
456             case 'r':
457                 offsets = [ - 15 - me.el.dom.offsetWidth, -13 - me.anchorOffset];
458                 break;
459             default:
460                 offsets = [25, -13 - me.anchorOffset];
461                 break;
462             }
463         }
464         mouseOffset = me.getMouseOffset();
465         offsets[0] += mouseOffset[0];
466         offsets[1] += mouseOffset[1];
467
468         return offsets;
469     },
470
471     // private
472     onTargetOver: function(e) {
473         var me = this,
474             t;
475
476         if (me.disabled || e.within(me.target.dom, true)) {
477             return;
478         }
479         t = e.getTarget(me.delegate);
480         if (t) {
481             me.triggerElement = t;
482             me.clearTimer('hide');
483             me.targetXY = e.getXY();
484             me.delayShow();
485         }
486     },
487
488     // private
489     delayShow: function() {
490         var me = this;
491         if (me.hidden && !me.showTimer) {
492             if (Ext.Date.getElapsed(me.lastActive) < me.quickShowInterval) {
493                 me.show();
494             } else {
495                 me.showTimer = Ext.defer(me.show, me.showDelay, me);
496             }
497         }
498         else if (!me.hidden && me.autoHide !== false) {
499             me.show();
500         }
501     },
502
503     // private
504     onTargetOut: function(e) {
505         var me = this;
506         if (me.disabled || e.within(me.target.dom, true)) {
507             return;
508         }
509         me.clearTimer('show');
510         if (me.autoHide !== false) {
511             me.delayHide();
512         }
513     },
514
515     // private
516     delayHide: function() {
517         var me = this;
518         if (!me.hidden && !me.hideTimer) {
519             me.hideTimer = Ext.defer(me.hide, me.hideDelay, me);
520         }
521     },
522
523     /**
524      * Hides this tooltip if visible.
525      */
526     hide: function() {
527         var me = this;
528         me.clearTimer('dismiss');
529         me.lastActive = new Date();
530         if (me.anchorEl) {
531             me.anchorEl.hide();
532         }
533         me.callParent(arguments);
534         delete me.triggerElement;
535     },
536
537     /**
538      * Shows this tooltip at the current event target XY position.
539      */
540     show: function() {
541         var me = this;
542
543         // Show this Component first, so that sizing can be calculated
544         // pre-show it off screen so that the el will have dimensions
545         this.callParent();
546         if (this.hidden === false) {
547             me.setPagePosition(-10000, -10000);
548
549             if (me.anchor) {
550                 me.anchor = me.origAnchor;
551             }
552             me.showAt(me.getTargetXY());
553
554             if (me.anchor) {
555                 me.syncAnchor();
556                 me.anchorEl.show();
557             } else {
558                 me.anchorEl.hide();
559             }
560         }
561     },
562
563     // inherit docs
564     showAt: function(xy) {
565         var me = this;
566         me.lastActive = new Date();
567         me.clearTimers();
568
569         // Only call if this is hidden. May have been called from show above.
570         if (!me.isVisible()) {
571             this.callParent(arguments);
572         }
573
574         // Show may have been vetoed.
575         if (me.isVisible()) {
576             me.setPagePosition(xy[0], xy[1]);
577             if (me.constrainPosition || me.constrain) {
578                 me.doConstrain();
579             }
580             me.toFront(true);
581         }
582
583         if (me.dismissDelay && me.autoHide !== false) {
584             me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me);
585         }
586         if (me.anchor) {
587             me.syncAnchor();
588             if (!me.anchorEl.isVisible()) {
589                 me.anchorEl.show();
590             }
591         } else {
592             me.anchorEl.hide();
593         }
594     },
595
596     // private
597     syncAnchor: function() {
598         var me = this,
599             anchorPos,
600             targetPos,
601             offset;
602         switch (me.tipAnchor.charAt(0)) {
603         case 't':
604             anchorPos = 'b';
605             targetPos = 'tl';
606             offset = [20 + me.anchorOffset, 1];
607             break;
608         case 'r':
609             anchorPos = 'l';
610             targetPos = 'tr';
611             offset = [ - 1, 12 + me.anchorOffset];
612             break;
613         case 'b':
614             anchorPos = 't';
615             targetPos = 'bl';
616             offset = [20 + me.anchorOffset, -1];
617             break;
618         default:
619             anchorPos = 'r';
620             targetPos = 'tl';
621             offset = [1, 12 + me.anchorOffset];
622             break;
623         }
624         me.anchorEl.alignTo(me.el, anchorPos + '-' + targetPos, offset);
625     },
626
627     // private
628     setPagePosition: function(x, y) {
629         var me = this;
630         me.callParent(arguments);
631         if (me.anchor) {
632             me.syncAnchor();
633         }
634     },
635
636     // private
637     clearTimer: function(name) {
638         name = name + 'Timer';
639         clearTimeout(this[name]);
640         delete this[name];
641     },
642
643     // private
644     clearTimers: function() {
645         var me = this;
646         me.clearTimer('show');
647         me.clearTimer('dismiss');
648         me.clearTimer('hide');
649     },
650
651     // private
652     onShow: function() {
653         var me = this;
654         me.callParent();
655         me.mon(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
656     },
657
658     // private
659     onHide: function() {
660         var me = this;
661         me.callParent();
662         me.mun(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
663     },
664
665     // private
666     onDocMouseDown: function(e) {
667         var me = this;
668         if (me.autoHide !== true && !me.closable && !e.within(me.el.dom)) {
669             me.disable();
670             Ext.defer(me.doEnable, 100, me);
671         }
672     },
673
674     // private
675     doEnable: function() {
676         if (!this.isDestroyed) {
677             this.enable();
678         }
679     },
680
681     // private
682     onDisable: function() {
683         this.callParent();
684         this.clearTimers();
685         this.hide();
686     },
687
688     beforeDestroy: function() {
689         var me = this;
690         me.clearTimers();
691         Ext.destroy(me.anchorEl);
692         delete me.anchorEl;
693         delete me.target;
694         delete me.anchorTarget;
695         delete me.triggerElement;
696         me.callParent();
697     },
698
699     // private
700     onDestroy: function() {
701         Ext.getDoc().un('mousedown', this.onDocMouseDown, this);
702         this.callParent();
703     }
704 });
705