Upgrade to ExtJS 4.0.7 - Released 10/19/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      * @property {HTMLElement} triggerElement
124      * When a ToolTip is configured with the `{@link #delegate}`
125      * option to cause selected child elements of the `{@link #target}`
126      * Element to each trigger a seperate show event, this property is set to
127      * the DOM element which triggered the show.
128      */
129     /**
130      * @cfg {HTMLElement/Ext.Element/String} target
131      * The target element or string id to monitor for mouseover events to trigger
132      * showing this ToolTip.
133      */
134     /**
135      * @cfg {Boolean} [autoHide=true]
136      * True to automatically hide the tooltip after the
137      * mouse exits the target element or after the `{@link #dismissDelay}`
138      * has expired if set.  If `{@link #closable} = true`
139      * a close tool button will be rendered into the tooltip header.
140      */
141     /**
142      * @cfg {Number} showDelay
143      * Delay in milliseconds before the tooltip displays after the mouse enters the target element.
144      */
145     showDelay: 500,
146     /**
147      * @cfg {Number} hideDelay
148      * Delay in milliseconds after the mouse exits the target element but before the tooltip actually hides.
149      * Set to 0 for the tooltip to hide immediately.
150      */
151     hideDelay: 200,
152     /**
153      * @cfg {Number} dismissDelay
154      * Delay in milliseconds before the tooltip automatically hides. To disable automatic hiding, set
155      * dismissDelay = 0.
156      */
157     dismissDelay: 5000,
158     /**
159      * @cfg {Number[]} [mouseOffset=[15,18]]
160      * An XY offset from the mouse position where the tooltip should be shown.
161      */
162     /**
163      * @cfg {Boolean} trackMouse
164      * True to have the tooltip follow the mouse as it moves over the target element.
165      */
166     trackMouse: false,
167     /**
168      * @cfg {String} anchor
169      * If specified, indicates that the tip should be anchored to a
170      * particular side of the target element or mouse pointer ("top", "right", "bottom",
171      * or "left"), with an arrow pointing back at the target or mouse pointer. If
172      * {@link #constrainPosition} is enabled, this will be used as a preferred value
173      * only and may be flipped as needed.
174      */
175     /**
176      * @cfg {Boolean} anchorToTarget
177      * True to anchor the tooltip to the target element, false to anchor it relative to the mouse coordinates.
178      * When `anchorToTarget` is true, use `{@link #defaultAlign}` to control tooltip alignment to the
179      * target element.  When `anchorToTarget` is false, use `{@link #anchor}` instead to control alignment.
180      */
181     anchorToTarget: true,
182     /**
183      * @cfg {Number} anchorOffset
184      * A numeric pixel value used to offset the default position of the anchor arrow.  When the anchor
185      * position is on the top or bottom of the tooltip, `anchorOffset` will be used as a horizontal offset.
186      * Likewise, when the anchor position is on the left or right side, `anchorOffset` will be used as
187      * a vertical offset.
188      */
189     anchorOffset: 0,
190     /**
191      * @cfg {String} delegate
192      *
193      * A {@link Ext.DomQuery DomQuery} selector which allows selection of individual elements within the
194      * `{@link #target}` element to trigger showing and hiding the ToolTip as the mouse moves within the
195      * target.
196      *
197      * When specified, the child element of the target which caused a show event is placed into the
198      * `{@link #triggerElement}` property before the ToolTip is shown.
199      *
200      * This may be useful when a Component has regular, repeating elements in it, each of which need a
201      * ToolTip which contains information specific to that element.
202      *
203      * See the delegate example in class documentation of {@link Ext.tip.ToolTip}.
204      */
205
206     // private
207     targetCounter: 0,
208     quickShowInterval: 250,
209
210     // private
211     initComponent: function() {
212         var me = this;
213         me.callParent(arguments);
214         me.lastActive = new Date();
215         me.setTarget(me.target);
216         me.origAnchor = me.anchor;
217     },
218
219     // private
220     onRender: function(ct, position) {
221         var me = this;
222         me.callParent(arguments);
223         me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
224         me.anchorEl = me.el.createChild({
225             cls: Ext.baseCSSPrefix + 'tip-anchor ' + me.anchorCls
226         });
227     },
228
229     // private
230     afterRender: function() {
231         var me = this,
232             zIndex;
233
234         me.callParent(arguments);
235         zIndex = parseInt(me.el.getZIndex(), 10) || 0;
236         me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.Element.DISPLAY);
237     },
238
239     /**
240      * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element.
241      * @param {String/HTMLElement/Ext.Element} t The Element, HtmlElement, or ID of an element to bind to
242      */
243     setTarget: function(target) {
244         var me = this,
245             t = Ext.get(target),
246             tg;
247
248         if (me.target) {
249             tg = Ext.get(me.target);
250             me.mun(tg, 'mouseover', me.onTargetOver, me);
251             me.mun(tg, 'mouseout', me.onTargetOut, me);
252             me.mun(tg, 'mousemove', me.onMouseMove, me);
253         }
254
255         me.target = t;
256         if (t) {
257
258             me.mon(t, {
259                 // TODO - investigate why IE6/7 seem to fire recursive resize in e.getXY
260                 // breaking QuickTip#onTargetOver (EXTJSIV-1608)
261                 freezeEvent: true,
262
263                 mouseover: me.onTargetOver,
264                 mouseout: me.onTargetOut,
265                 mousemove: me.onMouseMove,
266                 scope: me
267             });
268         }
269         if (me.anchor) {
270             me.anchorTarget = me.target;
271         }
272     },
273
274     // private
275     onMouseMove: function(e) {
276         var me = this,
277             t = me.delegate ? e.getTarget(me.delegate) : me.triggerElement = true,
278             xy;
279         if (t) {
280             me.targetXY = e.getXY();
281             if (t === me.triggerElement) {
282                 if (!me.hidden && me.trackMouse) {
283                     xy = me.getTargetXY();
284                     if (me.constrainPosition) {
285                         xy = me.el.adjustForConstraints(xy, me.el.getScopeParent());
286                     }
287                     me.setPagePosition(xy);
288                 }
289             } else {
290                 me.hide();
291                 me.lastActive = new Date(0);
292                 me.onTargetOver(e);
293             }
294         } else if ((!me.closable && me.isVisible()) && me.autoHide !== false) {
295             me.hide();
296         }
297     },
298
299     // private
300     getTargetXY: function() {
301         var me = this,
302             mouseOffset;
303         if (me.delegate) {
304             me.anchorTarget = me.triggerElement;
305         }
306         if (me.anchor) {
307             me.targetCounter++;
308                 var offsets = me.getOffsets(),
309                     xy = (me.anchorToTarget && !me.trackMouse) ? me.el.getAlignToXY(me.anchorTarget, me.getAnchorAlign()) : me.targetXY,
310                     dw = Ext.Element.getViewWidth() - 5,
311                     dh = Ext.Element.getViewHeight() - 5,
312                     de = document.documentElement,
313                     bd = document.body,
314                     scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5,
315                     scrollY = (de.scrollTop || bd.scrollTop || 0) + 5,
316                     axy = [xy[0] + offsets[0], xy[1] + offsets[1]],
317                     sz = me.getSize(),
318                     constrainPosition = me.constrainPosition;
319
320             me.anchorEl.removeCls(me.anchorCls);
321
322             if (me.targetCounter < 2 && constrainPosition) {
323                 if (axy[0] < scrollX) {
324                     if (me.anchorToTarget) {
325                         me.defaultAlign = 'l-r';
326                         if (me.mouseOffset) {
327                             me.mouseOffset[0] *= -1;
328                         }
329                     }
330                     me.anchor = 'left';
331                     return me.getTargetXY();
332                 }
333                 if (axy[0] + sz.width > dw) {
334                     if (me.anchorToTarget) {
335                         me.defaultAlign = 'r-l';
336                         if (me.mouseOffset) {
337                             me.mouseOffset[0] *= -1;
338                         }
339                     }
340                     me.anchor = 'right';
341                     return me.getTargetXY();
342                 }
343                 if (axy[1] < scrollY) {
344                     if (me.anchorToTarget) {
345                         me.defaultAlign = 't-b';
346                         if (me.mouseOffset) {
347                             me.mouseOffset[1] *= -1;
348                         }
349                     }
350                     me.anchor = 'top';
351                     return me.getTargetXY();
352                 }
353                 if (axy[1] + sz.height > dh) {
354                     if (me.anchorToTarget) {
355                         me.defaultAlign = 'b-t';
356                         if (me.mouseOffset) {
357                             me.mouseOffset[1] *= -1;
358                         }
359                     }
360                     me.anchor = 'bottom';
361                     return me.getTargetXY();
362                 }
363             }
364
365             me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
366             me.anchorEl.addCls(me.anchorCls);
367             me.targetCounter = 0;
368             return axy;
369         } else {
370             mouseOffset = me.getMouseOffset();
371             return (me.targetXY) ? [me.targetXY[0] + mouseOffset[0], me.targetXY[1] + mouseOffset[1]] : mouseOffset;
372         }
373     },
374
375     getMouseOffset: function() {
376         var me = this,
377         offset = me.anchor ? [0, 0] : [15, 18];
378         if (me.mouseOffset) {
379             offset[0] += me.mouseOffset[0];
380             offset[1] += me.mouseOffset[1];
381         }
382         return offset;
383     },
384
385     // private
386     getAnchorPosition: function() {
387         var me = this,
388             m;
389         if (me.anchor) {
390             me.tipAnchor = me.anchor.charAt(0);
391         } else {
392             m = me.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/);
393             //<debug>
394             if (!m) {
395                 Ext.Error.raise('The AnchorTip.defaultAlign value "' + me.defaultAlign + '" is invalid.');
396             }
397             //</debug>
398             me.tipAnchor = m[1].charAt(0);
399         }
400
401         switch (me.tipAnchor) {
402         case 't':
403             return 'top';
404         case 'b':
405             return 'bottom';
406         case 'r':
407             return 'right';
408         }
409         return 'left';
410     },
411
412     // private
413     getAnchorAlign: function() {
414         switch (this.anchor) {
415         case 'top':
416             return 'tl-bl';
417         case 'left':
418             return 'tl-tr';
419         case 'right':
420             return 'tr-tl';
421         default:
422             return 'bl-tl';
423         }
424     },
425
426     // private
427     getOffsets: function() {
428         var me = this,
429             mouseOffset,
430             offsets,
431             ap = me.getAnchorPosition().charAt(0);
432         if (me.anchorToTarget && !me.trackMouse) {
433             switch (ap) {
434             case 't':
435                 offsets = [0, 9];
436                 break;
437             case 'b':
438                 offsets = [0, -13];
439                 break;
440             case 'r':
441                 offsets = [ - 13, 0];
442                 break;
443             default:
444                 offsets = [9, 0];
445                 break;
446             }
447         } else {
448             switch (ap) {
449             case 't':
450                 offsets = [ - 15 - me.anchorOffset, 30];
451                 break;
452             case 'b':
453                 offsets = [ - 19 - me.anchorOffset, -13 - me.el.dom.offsetHeight];
454                 break;
455             case 'r':
456                 offsets = [ - 15 - me.el.dom.offsetWidth, -13 - me.anchorOffset];
457                 break;
458             default:
459                 offsets = [25, -13 - me.anchorOffset];
460                 break;
461             }
462         }
463         mouseOffset = me.getMouseOffset();
464         offsets[0] += mouseOffset[0];
465         offsets[1] += mouseOffset[1];
466
467         return offsets;
468     },
469
470     // private
471     onTargetOver: function(e) {
472         var me = this,
473             t;
474
475         if (me.disabled || e.within(me.target.dom, true)) {
476             return;
477         }
478         t = e.getTarget(me.delegate);
479         if (t) {
480             me.triggerElement = t;
481             me.clearTimer('hide');
482             me.targetXY = e.getXY();
483             me.delayShow();
484         }
485     },
486
487     // private
488     delayShow: function() {
489         var me = this;
490         if (me.hidden && !me.showTimer) {
491             if (Ext.Date.getElapsed(me.lastActive) < me.quickShowInterval) {
492                 me.show();
493             } else {
494                 me.showTimer = Ext.defer(me.show, me.showDelay, me);
495             }
496         }
497         else if (!me.hidden && me.autoHide !== false) {
498             me.show();
499         }
500     },
501
502     // private
503     onTargetOut: function(e) {
504         var me = this;
505         if (me.disabled || e.within(me.target.dom, true)) {
506             return;
507         }
508         me.clearTimer('show');
509         if (me.autoHide !== false) {
510             me.delayHide();
511         }
512     },
513
514     // private
515     delayHide: function() {
516         var me = this;
517         if (!me.hidden && !me.hideTimer) {
518             me.hideTimer = Ext.defer(me.hide, me.hideDelay, me);
519         }
520     },
521
522     /**
523      * Hides this tooltip if visible.
524      */
525     hide: function() {
526         var me = this;
527         me.clearTimer('dismiss');
528         me.lastActive = new Date();
529         if (me.anchorEl) {
530             me.anchorEl.hide();
531         }
532         me.callParent(arguments);
533         delete me.triggerElement;
534     },
535
536     /**
537      * Shows this tooltip at the current event target XY position.
538      */
539     show: function() {
540         var me = this;
541
542         // Show this Component first, so that sizing can be calculated
543         // pre-show it off screen so that the el will have dimensions
544         this.callParent();
545         if (this.hidden === false) {
546             me.setPagePosition(-10000, -10000);
547
548             if (me.anchor) {
549                 me.anchor = me.origAnchor;
550             }
551             me.showAt(me.getTargetXY());
552
553             if (me.anchor) {
554                 me.syncAnchor();
555                 me.anchorEl.show();
556             } else {
557                 me.anchorEl.hide();
558             }
559         }
560     },
561
562     // inherit docs
563     showAt: function(xy) {
564         var me = this;
565         me.lastActive = new Date();
566         me.clearTimers();
567
568         // Only call if this is hidden. May have been called from show above.
569         if (!me.isVisible()) {
570             this.callParent(arguments);
571         }
572
573         // Show may have been vetoed.
574         if (me.isVisible()) {
575             me.setPagePosition(xy[0], xy[1]);
576             if (me.constrainPosition || me.constrain) {
577                 me.doConstrain();
578             }
579             me.toFront(true);
580         }
581
582         if (me.dismissDelay && me.autoHide !== false) {
583             me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me);
584         }
585         if (me.anchor) {
586             me.syncAnchor();
587             if (!me.anchorEl.isVisible()) {
588                 me.anchorEl.show();
589             }
590         } else {
591             me.anchorEl.hide();
592         }
593     },
594
595     // private
596     syncAnchor: function() {
597         var me = this,
598             anchorPos,
599             targetPos,
600             offset;
601         switch (me.tipAnchor.charAt(0)) {
602         case 't':
603             anchorPos = 'b';
604             targetPos = 'tl';
605             offset = [20 + me.anchorOffset, 1];
606             break;
607         case 'r':
608             anchorPos = 'l';
609             targetPos = 'tr';
610             offset = [ - 1, 12 + me.anchorOffset];
611             break;
612         case 'b':
613             anchorPos = 't';
614             targetPos = 'bl';
615             offset = [20 + me.anchorOffset, -1];
616             break;
617         default:
618             anchorPos = 'r';
619             targetPos = 'tl';
620             offset = [1, 12 + me.anchorOffset];
621             break;
622         }
623         me.anchorEl.alignTo(me.el, anchorPos + '-' + targetPos, offset);
624     },
625
626     // private
627     setPagePosition: function(x, y) {
628         var me = this;
629         me.callParent(arguments);
630         if (me.anchor) {
631             me.syncAnchor();
632         }
633     },
634
635     // private
636     clearTimer: function(name) {
637         name = name + 'Timer';
638         clearTimeout(this[name]);
639         delete this[name];
640     },
641
642     // private
643     clearTimers: function() {
644         var me = this;
645         me.clearTimer('show');
646         me.clearTimer('dismiss');
647         me.clearTimer('hide');
648     },
649
650     // private
651     onShow: function() {
652         var me = this;
653         me.callParent();
654         me.mon(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
655     },
656
657     // private
658     onHide: function() {
659         var me = this;
660         me.callParent();
661         me.mun(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
662     },
663
664     // private
665     onDocMouseDown: function(e) {
666         var me = this;
667         if (me.autoHide !== true && !me.closable && !e.within(me.el.dom)) {
668             me.disable();
669             Ext.defer(me.doEnable, 100, me);
670         }
671     },
672
673     // private
674     doEnable: function() {
675         if (!this.isDestroyed) {
676             this.enable();
677         }
678     },
679
680     // private
681     onDisable: function() {
682         this.callParent();
683         this.clearTimers();
684         this.hide();
685     },
686
687     beforeDestroy: function() {
688         var me = this;
689         me.clearTimers();
690         Ext.destroy(me.anchorEl);
691         delete me.anchorEl;
692         delete me.target;
693         delete me.anchorTarget;
694         delete me.triggerElement;
695         me.callParent();
696     },
697
698     // private
699     onDestroy: function() {
700         Ext.getDoc().un('mousedown', this.onDocMouseDown, this);
701         this.callParent();
702     }
703 });
704