source: Dev/trunk/src/client/dijit/Tooltip.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 18.7 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.indexOf array.map
3        "dojo/_base/declare", // declare
4        "dojo/_base/fx", // fx.fadeIn fx.fadeOut
5        "dojo/dom", // dom.byId
6        "dojo/dom-class", // domClass.add
7        "dojo/dom-geometry", // domGeometry.position
8        "dojo/dom-style", // domStyle.set, domStyle.get
9        "dojo/_base/lang", // lang.hitch lang.isArrayLike
10        "dojo/mouse",
11        "dojo/on",
12        "dojo/sniff", // has("ie")
13        "./_base/manager",      // manager.defaultDuration
14        "./place",
15        "./_Widget",
16        "./_TemplatedMixin",
17        "./BackgroundIframe",
18        "dojo/text!./templates/Tooltip.html",
19        "./main"                // sets dijit.showTooltip etc. for back-compat
20], function(array, declare, fx, dom, domClass, domGeometry, domStyle, lang, mouse, on, has,
21                        manager, place, _Widget, _TemplatedMixin, BackgroundIframe, template, dijit){
22
23        // module:
24        //              dijit/Tooltip
25
26
27        // TODO: Tooltip should really share more positioning code with TooltipDialog, like:
28        //              - the orient() method
29        //              - the connector positioning code in show()
30        //              - the dijitTooltip[Dialog] class
31        //
32        // The problem is that Tooltip's implementation supplies it's own <iframe> and interacts directly
33        // with dijit/place, rather than going through dijit/popup like TooltipDialog and other popups (ex: Menu).
34
35        var MasterTooltip = declare("dijit._MasterTooltip", [_Widget, _TemplatedMixin], {
36                // summary:
37                //              Internal widget that holds the actual tooltip markup,
38                //              which occurs once per page.
39                //              Called by Tooltip widgets which are just containers to hold
40                //              the markup
41                // tags:
42                //              protected
43
44                // duration: Integer
45                //              Milliseconds to fade in/fade out
46                duration: manager.defaultDuration,
47
48                templateString: template,
49
50                postCreate: function(){
51                        this.ownerDocumentBody.appendChild(this.domNode);
52
53                        this.bgIframe = new BackgroundIframe(this.domNode);
54
55                        // Setup fade-in and fade-out functions.
56                        this.fadeIn = fx.fadeIn({ node: this.domNode, duration: this.duration, onEnd: lang.hitch(this, "_onShow") });
57                        this.fadeOut = fx.fadeOut({ node: this.domNode, duration: this.duration, onEnd: lang.hitch(this, "_onHide") });
58                },
59
60                show: function(innerHTML, aroundNode, position, rtl, textDir){
61                        // summary:
62                        //              Display tooltip w/specified contents to right of specified node
63                        //              (To left if there's no space on the right, or if rtl == true)
64                        // innerHTML: String
65                        //              Contents of the tooltip
66                        // aroundNode: DomNode|dijit/place.__Rectangle
67                        //              Specifies that tooltip should be next to this node / area
68                        // position: String[]?
69                        //              List of positions to try to position tooltip (ex: ["right", "above"])
70                        // rtl: Boolean?
71                        //              Corresponds to `WidgetBase.dir` attribute, where false means "ltr" and true
72                        //              means "rtl"; specifies GUI direction, not text direction.
73                        // textDir: String?
74                        //              Corresponds to `WidgetBase.textdir` attribute; specifies direction of text.
75
76
77                        if(this.aroundNode && this.aroundNode === aroundNode && this.containerNode.innerHTML == innerHTML){
78                                return;
79                        }
80
81                        if(this.fadeOut.status() == "playing"){
82                                // previous tooltip is being hidden; wait until the hide completes then show new one
83                                this._onDeck=arguments;
84                                return;
85                        }
86                        this.containerNode.innerHTML=innerHTML;
87
88                        if(textDir){
89                                this.set("textDir", textDir);
90                        }
91
92                        this.containerNode.align = rtl? "right" : "left"; //fix the text alignment
93
94                        var pos = place.around(this.domNode, aroundNode,
95                                position && position.length ? position : Tooltip.defaultPosition, !rtl, lang.hitch(this, "orient"));
96
97                        // Position the tooltip connector for middle alignment.
98                        // This could not have been done in orient() since the tooltip wasn't positioned at that time.
99                        var aroundNodeCoords = pos.aroundNodePos;
100                        if(pos.corner.charAt(0) == 'M' && pos.aroundCorner.charAt(0) == 'M'){
101                                this.connectorNode.style.top = aroundNodeCoords.y + ((aroundNodeCoords.h - this.connectorNode.offsetHeight) >> 1) - pos.y + "px";
102                                this.connectorNode.style.left = "";
103                        }else if(pos.corner.charAt(1) == 'M' && pos.aroundCorner.charAt(1) == 'M'){
104                                this.connectorNode.style.left = aroundNodeCoords.x + ((aroundNodeCoords.w - this.connectorNode.offsetWidth) >> 1) - pos.x + "px";
105                        }else{
106                                // Not *-centered, but just above/below/after/before
107                                this.connectorNode.style.left = "";
108                                this.connectorNode.style.top = "";
109                        }
110
111                        // show it
112                        domStyle.set(this.domNode, "opacity", 0);
113                        this.fadeIn.play();
114                        this.isShowingNow = true;
115                        this.aroundNode = aroundNode;
116                },
117
118                orient: function(/*DomNode*/ node, /*String*/ aroundCorner, /*String*/ tooltipCorner, /*Object*/ spaceAvailable, /*Object*/ aroundNodeCoords){
119                        // summary:
120                        //              Private function to set CSS for tooltip node based on which position it's in.
121                        //              This is called by the dijit popup code.   It will also reduce the tooltip's
122                        //              width to whatever width is available
123                        // tags:
124                        //              protected
125
126                        this.connectorNode.style.top = ""; //reset to default
127
128                        var heightAvailable = spaceAvailable.h,
129                                widthAvailable = spaceAvailable.w;
130
131                        node.className = "dijitTooltip " +
132                                {
133                                        "MR-ML": "dijitTooltipRight",
134                                        "ML-MR": "dijitTooltipLeft",
135                                        "TM-BM": "dijitTooltipAbove",
136                                        "BM-TM": "dijitTooltipBelow",
137                                        "BL-TL": "dijitTooltipBelow dijitTooltipABLeft",
138                                        "TL-BL": "dijitTooltipAbove dijitTooltipABLeft",
139                                        "BR-TR": "dijitTooltipBelow dijitTooltipABRight",
140                                        "TR-BR": "dijitTooltipAbove dijitTooltipABRight",
141                                        "BR-BL": "dijitTooltipRight",
142                                        "BL-BR": "dijitTooltipLeft"
143                                }[aroundCorner + "-" + tooltipCorner];
144
145                        // reset width; it may have been set by orient() on a previous tooltip show()
146                        this.domNode.style.width = "auto";
147
148                        // Reduce tooltip's width to the amount of width available, so that it doesn't overflow screen.
149                        // Note that sometimes widthAvailable is negative, but we guard against setting style.width to a
150                        // negative number since that causes an exception on IE.
151                        var size = domGeometry.position(this.domNode);
152                        if(has("ie") == 9){
153                                // workaround strange IE9 bug where setting width to offsetWidth causes words to wrap
154                                size.w += 2;
155                        }
156
157                        var width = Math.min((Math.max(widthAvailable,1)), size.w);
158
159                        domGeometry.setMarginBox(this.domNode, {w: width});
160
161                        // Reposition the tooltip connector.
162                        if(tooltipCorner.charAt(0) == 'B' && aroundCorner.charAt(0) == 'B'){
163                                var bb = domGeometry.position(node);
164                                var tooltipConnectorHeight = this.connectorNode.offsetHeight;
165                                if(bb.h > heightAvailable){
166                                        // The tooltip starts at the top of the page and will extend past the aroundNode
167                                        var aroundNodePlacement = heightAvailable - ((aroundNodeCoords.h + tooltipConnectorHeight) >> 1);
168                                        this.connectorNode.style.top = aroundNodePlacement + "px";
169                                        this.connectorNode.style.bottom = "";
170                                }else{
171                                        // Align center of connector with center of aroundNode, except don't let bottom
172                                        // of connector extend below bottom of tooltip content, or top of connector
173                                        // extend past top of tooltip content
174                                        this.connectorNode.style.bottom = Math.min(
175                                                Math.max(aroundNodeCoords.h/2 - tooltipConnectorHeight/2, 0),
176                                                bb.h - tooltipConnectorHeight) + "px";
177                                        this.connectorNode.style.top = "";
178                                }
179                        }else{
180                                // reset the tooltip back to the defaults
181                                this.connectorNode.style.top = "";
182                                this.connectorNode.style.bottom = "";
183                        }
184
185                        return Math.max(0, size.w - widthAvailable);
186                },
187
188                _onShow: function(){
189                        // summary:
190                        //              Called at end of fade-in operation
191                        // tags:
192                        //              protected
193                        if(has("ie")){
194                                // the arrow won't show up on a node w/an opacity filter
195                                this.domNode.style.filter="";
196                        }
197                },
198
199                hide: function(aroundNode){
200                        // summary:
201                        //              Hide the tooltip
202
203                        if(this._onDeck && this._onDeck[1] == aroundNode){
204                                // this hide request is for a show() that hasn't even started yet;
205                                // just cancel the pending show()
206                                this._onDeck=null;
207                        }else if(this.aroundNode === aroundNode){
208                                // this hide request is for the currently displayed tooltip
209                                this.fadeIn.stop();
210                                this.isShowingNow = false;
211                                this.aroundNode = null;
212                                this.fadeOut.play();
213                        }else{
214                                // just ignore the call, it's for a tooltip that has already been erased
215                        }
216                },
217
218                _onHide: function(){
219                        // summary:
220                        //              Called at end of fade-out operation
221                        // tags:
222                        //              protected
223
224                        this.domNode.style.cssText="";  // to position offscreen again
225                        this.containerNode.innerHTML="";
226                        if(this._onDeck){
227                                // a show request has been queued up; do it now
228                                this.show.apply(this, this._onDeck);
229                                this._onDeck=null;
230                        }
231                }
232        });
233
234        if(has("dojo-bidi")){
235                MasterTooltip.extend({
236                        _setAutoTextDir: function(/*Object*/node){
237                                // summary:
238                                //              Resolve "auto" text direction for children nodes
239                                // tags:
240                                //              private
241
242                                this.applyTextDir(node);
243                                array.forEach(node.children, function(child){ this._setAutoTextDir(child); }, this);
244                        },
245
246                        _setTextDirAttr: function(/*String*/ textDir){
247                                // summary:
248                                //              Setter for textDir.
249                                // description:
250                                //              Users shouldn't call this function; they should be calling
251                                //              set('textDir', value)
252                                // tags:
253                                //              private
254
255                                this._set("textDir", textDir);
256
257                                if (textDir == "auto"){
258                                        this._setAutoTextDir(this.containerNode);
259                                }else{
260                                        this.containerNode.dir = this.textDir;
261                                }
262                        }
263                });
264        }
265
266        dijit.showTooltip = function(innerHTML, aroundNode, position, rtl, textDir){
267                // summary:
268                //              Static method to display tooltip w/specified contents in specified position.
269                //              See description of dijit/Tooltip.defaultPosition for details on position parameter.
270                //              If position is not specified then dijit/Tooltip.defaultPosition is used.
271                // innerHTML: String
272                //              Contents of the tooltip
273                // aroundNode: place.__Rectangle
274                //              Specifies that tooltip should be next to this node / area
275                // position: String[]?
276                //              List of positions to try to position tooltip (ex: ["right", "above"])
277                // rtl: Boolean?
278                //              Corresponds to `WidgetBase.dir` attribute, where false means "ltr" and true
279                //              means "rtl"; specifies GUI direction, not text direction.
280                // textDir: String?
281                //              Corresponds to `WidgetBase.textdir` attribute; specifies direction of text.
282
283                // After/before don't work, but for back-compat convert them to the working after-centered, before-centered.
284                // Possibly remove this in 2.0.   Alternately, get before/after to work.
285                if(position){
286                        position = array.map(position, function(val){
287                                return {after: "after-centered", before: "before-centered"}[val] || val;
288                        });
289                }
290
291                if(!Tooltip._masterTT){ dijit._masterTT = Tooltip._masterTT = new MasterTooltip(); }
292                return Tooltip._masterTT.show(innerHTML, aroundNode, position, rtl, textDir);
293        };
294
295        dijit.hideTooltip = function(aroundNode){
296                // summary:
297                //              Static method to hide the tooltip displayed via showTooltip()
298                return Tooltip._masterTT && Tooltip._masterTT.hide(aroundNode);
299        };
300
301        var Tooltip = declare("dijit.Tooltip", _Widget, {
302                // summary:
303                //              Pops up a tooltip (a help message) when you hover over a node.
304                //              Also provides static show() and hide() methods that can be used without instantiating a dijit/Tooltip.
305
306                // label: String
307                //              HTML to display in the tooltip.
308                //              Specified as innerHTML when creating the widget from markup.
309                label: "",
310
311                // showDelay: Integer
312                //              Number of milliseconds to wait after hovering over/focusing on the object, before
313                //              the tooltip is displayed.
314                showDelay: 400,
315
316                // connectId: String|String[]|DomNode|DomNode[]
317                //              Id of domNode(s) to attach the tooltip to.
318                //              When user hovers over specified dom node(s), the tooltip will appear.
319                connectId: [],
320
321                // position: String[]
322                //              See description of `dijit/Tooltip.defaultPosition` for details on position parameter.
323                position: [],
324
325                // selector: String?
326                //              CSS expression to apply this Tooltip to descendants of connectIds, rather than to
327                //              the nodes specified by connectIds themselves.    Useful for applying a Tooltip to
328                //              a range of rows in a table, tree, etc.   Use in conjunction with getContent() parameter.
329                //              Ex: connectId: myTable, selector: "tr", getContent: function(node){ return ...; }
330                //
331                //              The application must require() an appropriate level of dojo/query to handle the selector.
332                selector: "",
333
334                // TODO: in 2.0 remove support for multiple connectIds.   selector gives the same effect.
335                // So, change connectId to a "", remove addTarget()/removeTarget(), etc.
336
337                _setConnectIdAttr: function(/*String|String[]|DomNode|DomNode[]*/ newId){
338                        // summary:
339                        //              Connect to specified node(s)
340
341                        // Remove connections to old nodes (if there are any)
342                        array.forEach(this._connections || [], function(nested){
343                                array.forEach(nested, function(handle){ handle.remove(); });
344                        }, this);
345
346                        // Make array of id's to connect to, excluding entries for nodes that don't exist yet, see startup()
347                        this._connectIds = array.filter(lang.isArrayLike(newId) ? newId : (newId ? [newId] : []),
348                                        function(id){ return dom.byId(id, this.ownerDocument); }, this);
349
350                        // Make connections
351                        this._connections = array.map(this._connectIds, function(id){
352                                var node = dom.byId(id, this.ownerDocument),
353                                        selector = this.selector,
354                                        delegatedEvent = selector ?
355                                                function(eventType){ return on.selector(selector, eventType); } :
356                                                function(eventType){ return eventType; },
357                                        self = this;
358                                return [
359                                        on(node, delegatedEvent(mouse.enter), function(){
360                                                self._onHover(this);
361                                        }),
362                                        on(node, delegatedEvent("focusin"), function(){
363                                                self._onHover(this);
364                                        }),
365                                        on(node, delegatedEvent(mouse.leave), lang.hitch(self, "_onUnHover")),
366                                        on(node, delegatedEvent("focusout"), lang.hitch(self, "_onUnHover"))
367                                ];
368                        }, this);
369
370                        this._set("connectId", newId);
371                },
372
373                addTarget: function(/*OomNode|String*/ node){
374                        // summary:
375                        //              Attach tooltip to specified node if it's not already connected
376
377                        // TODO: remove in 2.0 and just use set("connectId", ...) interface
378
379                        var id = node.id || node;
380                        if(array.indexOf(this._connectIds, id) == -1){
381                                this.set("connectId", this._connectIds.concat(id));
382                        }
383                },
384
385                removeTarget: function(/*DomNode|String*/ node){
386                        // summary:
387                        //              Detach tooltip from specified node
388
389                        // TODO: remove in 2.0 and just use set("connectId", ...) interface
390
391                        var id = node.id || node,       // map from DOMNode back to plain id string
392                                idx = array.indexOf(this._connectIds, id);
393                        if(idx >= 0){
394                                // remove id (modifies original this._connectIds but that's OK in this case)
395                                this._connectIds.splice(idx, 1);
396                                this.set("connectId", this._connectIds);
397                        }
398                },
399
400                buildRendering: function(){
401                        this.inherited(arguments);
402                        domClass.add(this.domNode,"dijitTooltipData");
403                },
404
405                startup: function(){
406                        this.inherited(arguments);
407
408                        // If this tooltip was created in a template, or for some other reason the specified connectId[s]
409                        // didn't exist during the widget's initialization, then connect now.
410                        var ids = this.connectId;
411                        array.forEach(lang.isArrayLike(ids) ? ids : [ids], this.addTarget, this);
412                },
413
414                getContent: function(/*DomNode*/ node){
415                        // summary:
416                        //              User overridable function that return the text to display in the tooltip.
417                        // tags:
418                        //              extension
419                        return this.label || this.domNode.innerHTML;
420                },
421
422                _onHover: function(/*DomNode*/ target){
423                        // summary:
424                        //              Despite the name of this method, it actually handles both hover and focus
425                        //              events on the target node, setting a timer to show the tooltip.
426                        // tags:
427                        //              private
428                        if(!this._showTimer){
429                                this._showTimer = this.defer(function(){ this.open(target); }, this.showDelay);
430                        }
431                },
432
433                _onUnHover: function(){
434                        // summary:
435                        //              Despite the name of this method, it actually handles both mouseleave and blur
436                        //              events on the target node, hiding the tooltip.
437                        // tags:
438                        //              private
439
440                        if(this._showTimer){
441                                this._showTimer.remove();
442                                delete this._showTimer;
443                        }
444                        this.close();
445                },
446
447                open: function(/*DomNode*/ target){
448                        // summary:
449                        //              Display the tooltip; usually not called directly.
450                        // tags:
451                        //              private
452
453                        if(this._showTimer){
454                                this._showTimer.remove();
455                                delete this._showTimer;
456                        }
457
458                        var content = this.getContent(target);
459                        if(!content){
460                                return;
461                        }
462                        Tooltip.show(content, target, this.position, !this.isLeftToRight(), this.textDir);
463
464                        this._connectNode = target;             // _connectNode means "tooltip currently displayed for this node"
465                        this.onShow(target, this.position);
466                },
467
468                close: function(){
469                        // summary:
470                        //              Hide the tooltip or cancel timer for show of tooltip
471                        // tags:
472                        //              private
473
474                        if(this._connectNode){
475                                // if tooltip is currently shown
476                                Tooltip.hide(this._connectNode);
477                                delete this._connectNode;
478                                this.onHide();
479                        }
480                        if(this._showTimer){
481                                // if tooltip is scheduled to be shown (after a brief delay)
482                                this._showTimer.remove();
483                                delete this._showTimer;
484                        }
485                },
486
487                onShow: function(/*===== target, position =====*/){
488                        // summary:
489                        //              Called when the tooltip is shown
490                        // tags:
491                        //              callback
492                },
493
494                onHide: function(){
495                        // summary:
496                        //              Called when the tooltip is hidden
497                        // tags:
498                        //              callback
499                },
500
501                destroy: function(){
502                        this.close();
503
504                        // Remove connections manually since they aren't registered to be removed by _WidgetBase
505                        array.forEach(this._connections || [], function(nested){
506                                array.forEach(nested, function(handle){ handle.remove(); });
507                        }, this);
508
509                        this.inherited(arguments);
510                }
511        });
512
513        Tooltip._MasterTooltip = MasterTooltip;         // for monkey patching
514        Tooltip.show = dijit.showTooltip;               // export function through module return value
515        Tooltip.hide = dijit.hideTooltip;               // export function through module return value
516
517        Tooltip.defaultPosition = ["after-centered", "before-centered"];
518
519        /*=====
520        lang.mixin(Tooltip, {
521                 // defaultPosition: String[]
522                 //             This variable controls the position of tooltips, if the position is not specified to
523                 //             the Tooltip widget or *TextBox widget itself.  It's an array of strings with the values
524                 //             possible for `dijit/place.around()`.   The recommended values are:
525                 //
526                 //             - before-centered: centers tooltip to the left of the anchor node/widget, or to the right
527                 //               in the case of RTL scripts like Hebrew and Arabic
528                 //             - after-centered: centers tooltip to the right of the anchor node/widget, or to the left
529                 //               in the case of RTL scripts like Hebrew and Arabic
530                 //             - above-centered: tooltip is centered above anchor node
531                 //             - below-centered: tooltip is centered above anchor node
532                 //
533                 //             The list is positions is tried, in order, until a position is found where the tooltip fits
534                 //             within the viewport.
535                 //
536                 //             Be careful setting this parameter.  A value of "above-centered" may work fine until the user scrolls
537                 //             the screen so that there's no room above the target node.   Nodes with drop downs, like
538                 //             DropDownButton or FilteringSelect, are especially problematic, in that you need to be sure
539                 //             that the drop down and tooltip don't overlap, even when the viewport is scrolled so that there
540                 //             is only room below (or above) the target node, but not both.
541         });
542        =====*/
543        return Tooltip;
544});
Note: See TracBrowser for help on using the repository browser.