` tag must use the default CSS `position`.
34 |
35 | ## Design Goals
36 |
37 | * **Tooltips that behave like they would in desktop applications**
38 |
39 | Tooltips should not flicker or be difficult to interact with. Only one tooltip should be visible on the screen at a time. When the cursor moves to another item with a tooltip, then the last tooltip should close gracefully before the new one opens.
40 |
41 | * **Fade-in and fade-out**
42 |
43 | The tooltips will have smooth fade-in and out cycles instead of abruptly appearing a disappearing. The fade effects should not conflict with any other effects in the document.
44 |
45 | * **Check for hover intent**
46 |
47 | Tooltips should not suddenly appear as soon as the mouse cursor happens to cross the element. They should only open when the cursor hovers over an element for a moment indicating that the user is actively focused on that element.
48 |
49 | * **Support multiple instances**
50 |
51 | Support various kinds of tooltips in one document, each with their own settings and content, even with different tooltip divs and styling. All while still preserving the one-tooltip rule and behaving like one instance.
52 |
53 | * **Totally portable**
54 |
55 | The plugin will not require any other plugins or extensions to function. There will be no dependencies other than the core jQuery library. The plugin will not require any images, all layout will be entirely CSS based.
56 |
57 | * **Easy to use**
58 |
59 | Despite all of the complexity involved (timers, animations, multiple instances), the plugin should be very simple to use, requiring little to no configuration to get running.
60 |
61 | * **Easy to customize**
62 |
63 | Tooltip layout and functionality should be extensible and simple for developers to adapt to match their web sites. Layout will be done entirely with CSS and the plugin will not attach any inline styles other than to control visibility and positioning.
64 |
65 | ## Installation
66 |
67 | The first step for using this plugin in your project is to include the needed files.
68 |
69 | ### Manual installation
70 |
71 | The most direct way to install this plugin is to download the latest version from the [project page](https://stevenbenner.github.io/jquery-powertip/) and copy the necessary files into your project. At the very least you will want one of the js files and one of the css files.
72 |
73 | ### npm Installation
74 |
75 | This plugin has been published to npm as [jquery-powertip](https://www.npmjs.com/package/jquery-powertip). This means that if you are using npm as your package manager then you can install PowerTip in your project by simply adding it to your package.json and/or running the following command:
76 |
77 | `npm install jquery-powertip --save`
78 |
79 | Then you can include it in your pages however you like (HTML tags, browserify, Require.js).
80 |
81 | ### Including resources
82 |
83 | #### HTML
84 |
85 | Once the PowerTip files are in your project you can simply include them in your web page with the following HTML tags:
86 |
87 | ```html
88 |
89 |
90 | ```
91 |
92 | **Important note:** Make sure you include jQuery before PowerTip in your HTML.
93 |
94 | #### Browserify
95 |
96 | PowerTip supports the CommonJS loading specification. If you are using npm to manage your packages and [Browserify](http://browserify.org/) to build your project, then you can load it and use it with a simple `require('jquery-powertip')`.
97 |
98 | The PowerTip API will be loaded into jQuery as well as the return object from the `require()`.
99 |
100 | **Important notes:** You will still need to include the CSS in your web page.
101 |
102 | #### RequireJS
103 |
104 | PowerTip also supports the AMD loading specification used by [RequireJS](http://requirejs.org/). You can load and use it by adding the path to your paths configuration and referencing it in your `define()` call(s).
105 |
106 | Example paths configuration:
107 |
108 | ```javascript
109 | require.config({
110 | paths: {
111 | jquery: 'https://code.jquery.com/jquery-3.7.1',
112 | 'jquery.powertip': '../dist/jquery.powertip'
113 | }
114 | });
115 | ```
116 |
117 | The PowerTip API will be loaded into jQuery as well as returned to the PowerTip parameter in your `define()` (`jquery.powertip` in the example above).
118 |
119 | **Important notes:**
120 |
121 | * You will still need to include the CSS in your web page.
122 | * Make sure you have a reference to `jquery` in your paths configuration.
123 |
124 | ## Usage
125 |
126 | Running the plugin is about as standard as it gets.
127 |
128 | ```javascript
129 | $('.tooltips').powerTip(options);
130 | ```
131 |
132 | Where `options` is an object with the various settings you want to override (all defined below).
133 |
134 | For example, if you want to attach tooltips to all elements with the "info" class, and have those tooltips appear above and to the right of those elements you would use the following code:
135 |
136 | ```javascript
137 | $('.info').powerTip({
138 | placement: 'ne' // north-east tooltip position
139 | });
140 | ```
141 |
142 | ### Setting tooltip content
143 |
144 | Generally, if your tooltips are just plain text then you probably want to set your tooltip text with the HTML `title` attribute on the elements themselves. This approach is very intuitive and backwards compatible. But there are several ways to specify the content.
145 |
146 | #### Title attribute
147 |
148 | The simplest method, as well as the only one that will continue to work for users who have JavaScript disabled in their browsers.
149 |
150 | ```html
151 | Some Link
152 | ```
153 |
154 | #### data-powertip
155 |
156 | Basically the same as setting the `title` attribute, but using an HTML5 [custom data attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*). You can set this in the markup or with JavaScript at any time. It only accepts a simple string, but that string can contain markup. This will also accept a function that returns a string.
157 |
158 | ```javascript
159 | $('#element').data('powertip', 'This will be the tooltip text.');
160 | ```
161 |
162 | or
163 |
164 | ```javascript
165 | $('#element').data('powertip', function() {
166 | return 'This will be the tooltip text.';
167 | });
168 | ```
169 |
170 | or
171 |
172 | ```html
173 | Some Link
174 | ```
175 |
176 | #### data-powertipjq
177 |
178 | This is a data interface that will accept a jQuery object. You can create a jQuery object containing complex markup (and even events) and attach it to the element via jQuery's `.data()` method at any time. This will also accept a function that returns a jQuery object.
179 |
180 | ```javascript
181 | var tooltip = $('
This will be the tooltip text. It even has an onclick event!
This will be the tooltip text. It even has an onclick event!
');
192 | tooltip.on('click', function() { /* ... */ });
193 | return tooltip;
194 | });
195 | ```
196 |
197 | #### data-powertiptarget
198 |
199 | You can specify the ID of an element in the DOM to pull the content from. PowerTip will replicate the markup of that element in the tooltip without modifying or destroying the original.
200 |
201 | ```html
202 |
207 | ```
208 |
209 | ```javascript
210 | $('#element').data('powertiptarget', 'myToolTip');
211 | ```
212 |
213 | ### Changing the tooltip content
214 |
215 | After you invoke `powerTip()` on an element the `title` attribute will be deleted and the HTML data attributes will be cached internally by jQuery. This means that if you want to change the tooltip for any element that you have already run PowerTip on then you must use the `.data()` method provided by jQuery. Changing the markup attributes will have no effect.
216 |
217 | Tooltips that are created using the HTML `title` attribute will have their content saved as "powertip" in the data collection. If you want to change the content of a tooltip after setting it with the `title` attribute, then you must change the "powertip" data attribute.
218 |
219 | Example:
220 |
221 | ```javascript
222 | $('#element').data('powertip', 'new tooltip content');
223 | ```
224 |
225 | ### Security considerations
226 |
227 | It should be noted that PowerTip uses jQuery's [append()](https://api.jquery.com/append/) method for placing content in the tooltip. This method can potentially execute code. Do not attempt to show tooltips with content from untrusted sources without sanitizing the input or you may introduce an [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting) vulnerability on your web page.
228 |
229 | ## Options
230 |
231 | The tooltip behavior is determined by a series of options that you can override. You can pass the options as an object directly to the plugin as an argument when you call it. For example:
232 |
233 | ```javascript
234 | $('.tips').powerTip({
235 | option1: 'value',
236 | option2: 'value',
237 | option3: 'value'
238 | });
239 | ```
240 |
241 | The settings will only apply to those tooltips matched in the selector. This means that you can have different sets of tooltips on the same page with different options. For example:
242 |
243 | ```javascript
244 | $('.tips').powerTip(/** options for regular tooltips **/);
245 |
246 | $('.specialTips').powerTip(/** options for special tooltips **/);
247 | ```
248 |
249 | You can change the default options for all tooltips by setting their values in the `$.fn.powerTip.defaults` object before you call `powerTip()`. For example:
250 |
251 | ```javascript
252 | // change the default tooltip placement to south
253 | $.fn.powerTip.defaults.placement = 's';
254 |
255 | $('.tips').powerTip(); // these tips will appear underneath the element
256 | ```
257 |
258 | Of course those defaults will be overridden with any options you pass directly to the `powerTip()` call.
259 |
260 | ### List of options
261 |
262 | | Name | Type | Description |
263 | | ----- | ----- | ----- |
264 | | `followMouse` | Boolean | (default: `false`) If set to `true` the tooltip will follow the user's mouse cursor. Note that if a tooltip with `followMouse` enabled is opened by an event without mouse data (like "focus" via keyboard navigation) then it will revert to static placement with smart positioning enabled. So you may wish to set `placement` as well. |
265 | | `mouseOnToPopup` | Boolean | (default: `false`) Allow the mouse to hover on the tooltip. This lets users interact with the content in the tooltip. Only applies if `followMouse` is set to `false` and `manual` is set to `false`. |
266 | | `placement` | String | (default: `'n'`) Placement location of the tooltip relative to the element it is open for. Values can be `n`, `e`, `s`, `w`, `nw`, `ne`, `sw`, `se`, `nw-alt`, `ne-alt`, `sw-alt`, or `se-alt` (as in north, east, south, and west). This only matters if `followMouse` is set to `false`. |
267 | | `smartPlacement` | Boolean | (default: `false`) When enabled the plugin will try to keep tips inside the browser viewport. If a tooltip would extend outside of the viewport then its placement will be changed to an orientation that would be entirely within the current viewport. Only applies if `followMouse` is set to `false`. |
268 | | `popupId` | String | (default: `'powerTip'`) HTML id attribute for the tooltip div. |
269 | | `popupClass` | String | (default: `''`) Space separated custom HTML classes for the tooltip div. Since this plugs directly into jQuery's `addClass()` method it will also accept a function that returns a string. |
270 | | `offset` | Number | (default: `10`) Pixel offset of the tooltip. This will be the offset from the element the tooltip is open for, or from the mouse cursor if `followMouse` is `true`. |
271 | | `fadeInTime` | Number | (default: `200`) Tooltip fade-in time in milliseconds. |
272 | | `fadeOutTime` | Number | (default: `100`) Tooltip fade-out time in milliseconds. |
273 | | `closeDelay` | Number | (default: `100`) Time in milliseconds to wait after mouse cursor leaves the element before closing the tooltip. This serves two purposes: first, it is the mechanism that lets the mouse cursor reach the tooltip (cross the gap between the element and the tooltip div) for `mouseOnToPopup` tooltips. And, second, it lets the cursor briefly leave the element and return without causing the whole fade-out, intent test, and fade-in cycle to happen. |
274 | | `intentPollInterval` | Number | (default: `100`) Hover intent polling interval in milliseconds. |
275 | | `intentSensitivity` | Number | (default: `7`) Hover intent sensitivity. The tooltip will not open unless the number of pixels the mouse has moved within the `intentPollInterval` is less than this value. These default values mean that if the mouse cursor has moved 7 or more pixels in 100 milliseconds the tooltip will not open. |
276 | | `manual` | Boolean | (default: `false`) If set to `true` then PowerTip will not hook up its event handlers, letting you create your own event handlers to control when tooltips are shown (using the API to open and close tooltips). |
277 | | `openEvents` | Array of Strings | (default: `[ 'mouseenter', 'focus' ]`) Specifies which jQuery events should cause the tooltip to open. Only applies if `manual` is set to `false`. |
278 | | `closeEvents` | Array of Strings | (default: `[ 'mouseleave', 'blur' ]`) Specifies which jQuery events should cause the tooltip to close. Only applies if `manual` is set to `false`. |
279 |
280 | ## Tooltip CSS
281 |
282 | **If you use one of the included CSS files, then you do not need to add any other CSS to get PowerTip running.**
283 |
284 | PowerTip includes some base CSS that you can just add to your site and be done with it, but you may want to change the styles or even craft your own styles to match your design. PowerTip is specifically designed to give you full control of your tooltips with CSS, with just a few basic requirements.
285 |
286 | I recommend that you either adapt one of the base stylesheets to suit your needs or override its rules so that you don't forget anything.
287 |
288 | **Important notes:**
289 |
290 | * The default id of the PowerTip element is `powerTip`. But this can be changed via the `popupId` option.
291 | * The PowerTip element is always a direct child of body, appended after all other content on the page.
292 | * The tooltip element is not created until you run `powerTip()`.
293 | * PowerTip will set the `display`, `visibility`, `opacity`, `top`, `left`, `right`, and `bottom` properties using inline styles.
294 |
295 | ### CSS requirements
296 |
297 | The bare minimum that PowerTip requires to work is that the `#powerTip` element be given absolute positioning and set to not display. For example:
298 |
299 | ```css
300 | #powerTip {
301 | position: absolute;
302 | display: none;
303 | }
304 | ```
305 |
306 | ### CSS recommendations
307 |
308 | #### High z-index
309 |
310 | You will want your tooltips to display over all other elements on your web page. This is done by setting the z-index value to a number greater than the z-index of any other elements on the page. It's probably a good idea to just set the z-index for the tooltip element to the maximum integer value (2147483647). For example:
311 |
312 | ```css
313 | #powerTip {
314 | z-index: 2147483647;
315 | }
316 | ```
317 |
318 | #### CSS arrows
319 |
320 | You probably want to create some CSS arrows for your tooltips (unless you only use mouse-follow tooltips). This topic would be an article unto itself, so if you want to make your own CSS arrows from scratch you should just Google "css arrows" to see how it's done.
321 |
322 | CSS arrows are created by using borders of a specific color and transparent borders. PowerTip adds the arrows by creating an empty `:before` pseudo element and absolutely positioning it around the tooltip.
323 |
324 | It is important to note that if you increase the size of the tooltip arrows and want users to be able to interact with the tooltip content via the `mouseOnToPopup` option then you will probably need to increase the `closeDelay` option to provide enough time for the cursor to cross the gap between the element and the tooltip div.
325 |
326 | #### Fixed width
327 |
328 | It is recommended, but not required, that tooltips have a static width. PowerTip is designed to work with elastic tooltips, but it can look odd if you have huge tooltips, so it is probably best for you to set a width on the tooltip element or (if you have short tooltip text) disable text wrapping. For example:
329 |
330 | ```css
331 | #powerTip {
332 | width: 300px;
333 | }
334 | ```
335 |
336 | or
337 |
338 | ```css
339 | #powerTip {
340 | white-space: nowrap;
341 | }
342 | ```
343 |
344 | ## API
345 |
346 | There are some scenarios where you may want to manually open/close or update/remove tooltips via JavaScript. To make this possible, PowerTip exposes several API methods on the `$.powerTip` object.
347 |
348 | | Method | Description |
349 | | ----- | ----- |
350 | | `show(element, event)` | This function will force the tooltip for the specified element to open. You pass it a jQuery object with the element that you want to show the tooltip for. If the jQuery object you pass to this function has more than one matched elements, then only the first element will show its tooltip. You can also pass it the event (a `$.Event`) with the pageX and pageY properties for mouse tracking. |
351 | | `hide(element, immediate)` | Closes any open tooltip. You do not need to specify which tooltip you would like to close (because there can be only one). If you set immediate to `true`, there will be no close delay. |
352 | | `toggle(element, event)` | This will toggle the tooltip, opening a closed tooltip or closing an open tooltip. The event argument is optional. If a mouse event is passed then this function will enable hover intent testing when opening a tooltip, or enable a close delay when closing a tooltip. Non-mouse events are ignored. |
353 | | `reposition(element)` | Repositions an open tooltip on the specified element. Use this if the tooltip or the element it opened for has changed its size or position. |
354 | | `destroy(element)` | This will destroy and roll back any PowerTip instance attached to the matched elements. If no element is specified then all PowerTip instances will be destroyed, including the document events and tooltip elements. |
355 |
356 | You can also pass the API method names as strings to the `powerTip()` function. For example `$('#element').powerTip('show');` will cause the matched element to show its tooltip.
357 |
358 | ### Examples
359 |
360 | ```javascript
361 | // run powertip on submit button
362 | $('#submit').powerTip();
363 |
364 | // open tooltip for submit button
365 | $.powerTip.show($('#submit'));
366 |
367 | // close (any open) tooltip
368 | $.powerTip.hide();
369 | ```
370 |
371 | ### Notes
372 |
373 | * Remember that one of the rules for PowerTip is that only one tooltip will be visible at a time, so any open tooltips will be closed before a new tooltip is shown.
374 | * Forcing a tooltip to open via the `show()` method does not disable the normal hover tooltips for any other elements. If the user moves their cursor to another element with a tooltip after you call `show()` then the tooltip you opened will be closed so that the tooltip for the user's current hover target can open.
375 |
376 | ## PowerTip Events
377 |
378 | PowerTip will trigger several events during operation that you can bind custom code to. These events make it much easier to extend the plugin and work with tooltips during their life cycle. Using events should not be needed in most cases, they are provided for developers who need a deeper level of integration with the tooltip system.
379 |
380 | ### List of events
381 |
382 | | Event Name | Description |
383 | | ----- | ----- |
384 | | `powerTipPreRender` | The pre-render event happens before PowerTip fills the content of the tooltip. This is a good opportunity to set the tooltip content data (e.g. data-powertip, data-powertipjq). |
385 | | `powerTipRender` | Render happens after the content has been placed into the tooltip, but before the tooltip has been displayed. Here you can modify the tooltip content manually or attach events. |
386 | | `powerTipOpen` | This happens after the tooltip has completed its fade-in cycle and is fully open. You might want to use this event to do animations or add other bits of visual sugar. |
387 | | `powerTipClose` | Occurs after the tooltip has completed its fade-out cycle and fully closed, but the tooltip content is still in place. This event is useful do doing cleanup work after the user is done with the tooltip. |
388 |
389 | ### Using events
390 |
391 | You can use these events by binding to them on the element(s) that you ran `powerTip()` on, the recommended way to do that is with the jQuery `on()` method. For example:
392 |
393 | ```javascript
394 | $('.tips').on({
395 | powerTipPreRender: function() {
396 | console.log('powerTipRender', this);
397 |
398 | // generate some dynamic content
399 | $(this).data('powertip' , '
Default title
Default content
');
400 | },
401 | powerTipRender: function() {
402 | console.log('powerTipRender', this);
403 |
404 | // change some content dynamically
405 | $('#powerTip').find('.title').text('This is a dynamic title.');
406 | },
407 | powerTipOpen: function() {
408 | console.log('powerTipOpen', this);
409 |
410 | // animate something when the tooltip opens
411 | $('#powerTip').find('.title').animate({ opacity: .1 }, 1000).animate({ opacity: 1 }, 1000);
412 | },
413 | powerTipClose: function() {
414 | console.log('powerTipClose', this);
415 |
416 | // cleanup the animation
417 | $('#powerTip').find('.title').stop(true, true);
418 | }
419 | });
420 | ```
421 |
422 | The context (the `this` keyword) of these functions will be the element that the tooltip is open for.
423 |
424 | ## About smart placement
425 |
426 | Smart placement is a feature that will attempt to keep non-mouse-follow tooltips within the browser viewport. When it is enabled, PowerTip will automatically change the placement of any tooltip that would appear outside of the viewport, such as a tooltip that would push outside the left or right bounds of the window, or a tooltip that would be hidden below the fold.
427 |
428 | **Without smart placement:**
429 |
430 | 
431 |
432 | **With smart placement:**
433 |
434 | 
435 |
436 | It does this by detecting that a tooltip would appear outside of the viewport, then trying a series of other placement options until it finds one that isn't going to be outside of the viewport. You can define the placement fall backs and priorities yourself by overriding them in the `$.fn.powerTip.smartPlacementLists` object.
437 |
438 | These are the default smart placement priority lists:
439 |
440 | ```javascript
441 | $.fn.powerTip.smartPlacementLists = {
442 | n: [ 'n', 'ne', 'nw', 's' ],
443 | e: [ 'e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e' ],
444 | s: [ 's', 'se', 'sw', 'n' ],
445 | w: [ 'w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w' ],
446 | nw: [ 'nw', 'w', 'sw', 'n', 's', 'se', 'nw' ],
447 | ne: [ 'ne', 'e', 'se', 'n', 's', 'sw', 'ne' ],
448 | sw: [ 'sw', 'w', 'nw', 's', 'n', 'ne', 'sw' ],
449 | se: [ 'se', 'e', 'ne', 's', 'n', 'nw', 'se' ],
450 | 'nw-alt': [ 'nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e' ],
451 | 'ne-alt': [ 'ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w' ],
452 | 'sw-alt': [ 'sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e' ],
453 | 'se-alt': [ 'se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w' ]
454 | };
455 | ```
456 |
457 | As you can see, each placement option has an array of placement options that it can fall back on. The first item in the array is the highest priority placement, the last is the lowest priority. The last item in the array is also the default. If none of the placement options can be fully displayed within the viewport then the last item in the array is the placement used to show the tooltip.
458 |
459 | You can override these default placement priority lists before you call `powerTip()` and define your own smart placement fall back order. Like so:
460 |
461 | ```javascript
462 | // define custom smart placement order
463 | $.fn.powerTip.smartPlacementLists.n = [ 'n', 's', 'e', 'w' ];
464 |
465 | // these tips will use the custom 'north' smart placement list
466 | $('.tips').powerTip({
467 | placement: 'n',
468 | smartPlacement: true
469 | });
470 | ```
471 |
472 | Smart placement is **disabled** by default because I believe that the world would be a better place if features that override explicit configuration values were disabled by default.
473 |
474 | ## Custom PowerTip Integration
475 |
476 | If you need to use PowerTip in a non-standard way, that is to say, if you need tooltips to open and close in some way other than the default mouse-on/mouse-off behavior then you can create your own event handlers and tell PowerTip when it should open and close tooltips.
477 |
478 | This is actually quite easy, you just tell PowerTip not to hook the default mouse and keyboard events when you run the plugin by setting the `manual` option to `true`, then use the API to open and close tooltips. While this is a bit more technical then just using the default behavior, it works just as well. In fact, PowerTip uses this same public API internally.
479 |
480 | ### Disable event binding
481 |
482 | To disable binding of the events that are normally attached when you run `powerTip()` just set the `manual` option to `true`.
483 |
484 | ```javascript
485 | $('.tooltips').powerTip({ manual: true });
486 | ```
487 |
488 | Now PowerTip has initialized and set up the `.tooltips` elements, but it will not open tooltips for those elements automatically. You must manually open the tooltips using the API.
489 |
490 | ### Building your own event handlers
491 |
492 | Here is an example of a manually implemented click-to-open tooltip to show you how it's done:
493 |
494 | ```javascript
495 | // run PowerTip - but disable the default event hooks
496 | $('.tooltips').powerTip({ manual: true });
497 |
498 | // hook custom onclick function
499 | $('.tooltips').on('click', function() {
500 | // toggle the tooltip for the element that received the click event
501 | $.powerTip.toggle(this);
502 | });
503 |
504 | // Note: this is just for example - for click-to-open you should probably just
505 | // use the openEvents/closeEvents options, like this:
506 | // $('.tooltips').powerTip({ openEvents: [ 'click' ], closeEvents: [ 'click' ] });
507 | ```
508 |
509 | This code will open a tooltip when the element is clicked and close it when the element is clicked again, or when another one of the `.tooltips` elements gets clicked.
510 |
511 | Now it's worth noting that this example doesn't take advantage of the hover intent feature or the tooltip delays because the mouse position was not passed to the `toggle()` method.
512 |
513 | So, let's look at a more complex situation. In the following example we hook up mouse events just like PowerTip would internally (open on mouse enter, close on mouse leave).
514 |
515 | ```javascript
516 | // run PowerTip - but disable the default event hooks
517 | $('.tooltips').powerTip({ manual: true });
518 |
519 | // hook custom mouse events
520 | $('.tooltips').on({
521 | mouseenter: function(event) {
522 | // note that we pass the jQuery mouse event to the show() method
523 | // this lets PowerTip do the hover intent testing
524 | $.powerTip.show(this, event);
525 | },
526 | mouseleave: function() {
527 | // note that we pass the element to the hide() method
528 | // this lets PowerTip wait before closing the tooltip, if the user's
529 | // mouse cursor returns to this element before the tooltip closes then
530 | // the close will be canceled
531 | $.powerTip.hide(this);
532 | }
533 | });
534 | ```
535 |
536 | And there you have it. If you want to enable the hover intent testing then you will need to pass the mouse event to the `show()` method and if you want to enable the close delay feature then you have to pass that element to the `hide()` method.
537 |
538 | ### Additional notes
539 |
540 | * Only mouse events (`mouseenter`, `mouseleave`, `hover`, `mousemove`) have the required properties (`pageX`, and `pageY`) to do hover intent testing. Click events and keyboard events will not work.
541 | * You should not use the `destroy()` method while your custom handlers are hooked up, it may cause unexpected things to happen (like mouse position tracking not working).
542 | * In most cases you should probably be using the `openEvents` and `closeEvents` options to bind tooltips to non-default events.
543 |
--------------------------------------------------------------------------------
/doc/gh-pages.template.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | description: "PowerTip is a jQuery plugin for creating smooth, modern tooltips."
4 | ---
5 |
6 | PowerTip is a jQuery tooltip plugin with a smooth user experience that features a very flexible design which is easy to customize, gives you a variety of different ways to create tooltips, supports adding complex data to tooltips, and has a robust API for developers seeking greater integration with their web applications.
7 |
8 |
13 |
14 | ## Examples
15 |
16 | Here are some basic examples of PowerTip in action. You can also fiddle with PowerTip on the official [JSFiddle demo](https://jsfiddle.net/stevenbenner/2baqv/).
17 |
18 | ### Placement examples
19 |
20 |
41 | The PowerTip for this box will follow the mouse.
42 |
43 |
44 |
45 | ### Mouse on to popup example
46 |
47 |
48 |
49 | The PowerTip for this box will appear on the right and you will be able to interact with its content.
50 |
51 |
52 |
53 | <%=
54 | doc.replace(
55 | /```(\w+)((?:.*\r?\n)*?)```/g,
56 | '{% highlight $1 %}$2{% endhighlight %}'
57 | )
58 | %>
59 |
60 | ## Change Log
61 | <%
62 | _.each(changelog, function(details, version) {
63 | var date = details.date;
64 |
65 | if (date instanceof Date) {
66 | date = grunt.template.date(new Date(date.getTime() + date.getTimezoneOffset() * 60000), 'longDate');
67 | }
68 |
69 | if (details.diff) {
70 | print('\n\n### [' + version + '](' + details.diff + ')');
71 | } else {
72 | print('\n\n### ' + version);
73 | }
74 | print(' - ' + details.description + ' (' + date + ')\n');
75 |
76 | _.each(details.changes, function(value, key, list) {
77 | print('\n* **' + value.section + '**');
78 | _.each(value.changes, function(value, key, list) {
79 | print('\n\t* ' + value);
80 | });
81 | });
82 | });
83 | %>
84 |
85 | ## Contributors
86 |
87 | Special thanks to the [contributors](https://github.com/stevenbenner/jquery-powertip/graphs/contributors) who have helped build PowerTip.
88 |
--------------------------------------------------------------------------------
/doc/release-process.md:
--------------------------------------------------------------------------------
1 | # PowerTip Release Process
2 |
3 | **THIS DOCUMENT IS FOR INTERNAL REFERENCE ONLY** - I am documenting the release process so that I have a nice checklist to go over when releasing a new version of PowerTip. You probably don't care how PowerTip is built and released unless you plan on maintaining your own fork.
4 |
5 | ## Version Format
6 |
7 | PowerTip uses [Semantic Versioning](https://semver.org/) and the version is in the format of [MAJOR].[MINOR].[PATCH]. Versioning is dictated by the exposed API that PowerTip users consume when using the plugin.
8 |
9 | This includes anything in the following namespaces:
10 |
11 | * `$.fn.powerTip`
12 | * `$.fn.powerTip.defaults`
13 | * `$.fn.powerTip.smartPlacementLists`
14 | * `$.powerTip`
15 |
16 | The events that fire during the tooltip life cycle are also considered to be part of the API for versioning purposes.
17 |
18 | ### Semantic Versioning Requirements
19 |
20 | > * MAJOR version when you make incompatible API changes,
21 | > * MINOR version when you add functionality in a backwards-compatible manner, and
22 | > * PATCH version when you make backwards-compatible bug fixes.
23 |
24 | ## The Release Process
25 |
26 | 1. **Update the date and diff for the release in CHANGELOG.yml**
27 |
28 | The CHANGELOG.yml file is used to generate the release notes seen on the project page.
29 |
30 | 2. **Bump the version in package.json**
31 |
32 | The package.json is used when building the project and when publishing to npm.
33 |
34 | 3. **Run `grunt build:release`**
35 |
36 | This will build and test all of the code and generate the zip archive for release in the dist folder.
37 |
38 | 4. **Tag the version**
39 |
40 | Make sure the changes from steps 1 and 2 have been committed.
41 |
42 | `git tag -a vX.X.X -m "version X.X.X"`
43 |
44 | 5. **Run `grunt deploy`**
45 |
46 | This will build the project page content, commit it to the gh-pages branch, and return to the master branch. It does not push any changes to the repo.
47 |
48 | 6. **Review commits and push them**
49 |
50 | **POINT OF NO RETURN**
51 |
52 | This is the fail-safe step. Make sure the build looks right. Make sure the commits from steps 1 and 2 are correct. Make sure the commits added to the gh-pages branch look right. If everything looks good then push the commits and the tag.
53 |
54 | 7. **Publish to npm**
55 |
56 | *Prefer npm version 4.0.0 or greater for prepublishOnly script*
57 |
58 | First, verify that the package to be release to npm contains the expected files in the expected structure. Run `grunt build:npm && npm pack`. This will generate the appropriate dist folder contents and create the tgz package. Look over the tgz package to make sure everything looks good.
59 |
60 | Now publish the new release to the npm repository by running the `npm publish` command.
61 |
62 | 7. **Add new release to GitHub repo**
63 |
64 | Add a release for the tag you just created, copy and paste the release notes, and add the zip archive to the release.
65 |
66 | 8. **Update JSFiddle if needed** (it usually will not be needed)
67 |
68 | The [PowerTip JSFiddle](https://jsfiddle.net/stevenbenner/2baqv/) is used by people wanting to quickly play with the plugin before really digging into it. If there were any breaking changes or significant new features then they should be added to the JSFiddle.
69 |
--------------------------------------------------------------------------------
/examples/examples.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PowerTip Examples
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
148 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery-powertip",
3 | "title": "PowerTip",
4 | "description": "A jQuery plugin that creates hover tooltips.",
5 | "version": "1.3.2",
6 | "main": "dist/jquery.powertip.js",
7 | "homepage": "https://stevenbenner.github.io/jquery-powertip/",
8 | "author": {
9 | "name": "Steven Benner",
10 | "url": "https://stevenbenner.com/"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/stevenbenner/jquery-powertip.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/stevenbenner/jquery-powertip/issues"
18 | },
19 | "license": "MIT",
20 | "keywords": [
21 | "jquery-plugin",
22 | "ecosystem:jquery",
23 | "powertip",
24 | "jquery",
25 | "tooltip",
26 | "tooltips",
27 | "ui",
28 | "browser"
29 | ],
30 | "dependencies": {
31 | "jquery": "1.7 - 4"
32 | },
33 | "devDependencies": {
34 | "@stevenbenner/eslint-config": "~3.0",
35 | "grunt": "~1.6",
36 | "grunt-browserify": "~6.0",
37 | "grunt-contrib-clean": "~2.0",
38 | "grunt-contrib-compress": "~2.0",
39 | "grunt-contrib-concat": "~2.1",
40 | "grunt-contrib-copy": "~1.0",
41 | "grunt-contrib-csslint": "~2.0",
42 | "grunt-contrib-cssmin": "~5.0",
43 | "grunt-contrib-qunit": "~8.0",
44 | "grunt-contrib-uglify": "~5.2",
45 | "grunt-eslint": "~24.3",
46 | "grunt-indent": "~1.0",
47 | "grunt-jsonlint": "~2.1",
48 | "grunt-shell": "~4.0",
49 | "jit-grunt": "~0.10",
50 | "qunit": "~2.20",
51 | "time-grunt": "~2.0"
52 | },
53 | "scripts": {
54 | "grunt": "grunt",
55 | "test": "grunt test --verbose --stack",
56 | "prepublishOnly": "grunt build:npm"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 3
4 | },
5 | "env": {
6 | "amd": true,
7 | "browser": true,
8 | "commonjs": true,
9 | "jquery": true,
10 | "node": false
11 | },
12 | "rules": {
13 | "strict": [
14 | "error",
15 | "never"
16 | ],
17 | "no-var": "off",
18 | "prefer-template": "off"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/core.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PowerTip Core
3 | *
4 | * @fileoverview Core variables, plugin object, and API.
5 | * @link https://stevenbenner.github.io/jquery-powertip/
6 | * @author Steven Benner (https://stevenbenner.com/)
7 | * @requires jQuery 1.7+
8 | */
9 |
10 | // useful private variables
11 | var $document = $(document),
12 | $window = $(window),
13 | $body = $('body');
14 |
15 | // constants
16 | var DATA_DISPLAYCONTROLLER = 'displayController',
17 | DATA_HASACTIVEHOVER = 'hasActiveHover',
18 | DATA_FORCEDOPEN = 'forcedOpen',
19 | DATA_HASMOUSEMOVE = 'hasMouseMove',
20 | DATA_MOUSEONTOTIP = 'mouseOnToPopup',
21 | DATA_ORIGINALTITLE = 'originalTitle',
22 | DATA_POWERTIP = 'powertip',
23 | DATA_POWERTIPJQ = 'powertipjq',
24 | DATA_POWERTIPTARGET = 'powertiptarget',
25 | EVENT_NAMESPACE = '.powertip',
26 | RAD2DEG = 180 / Math.PI,
27 | MOUSE_EVENTS = [
28 | 'click',
29 | 'dblclick',
30 | 'mousedown',
31 | 'mouseup',
32 | 'mousemove',
33 | 'mouseover',
34 | 'mouseout',
35 | 'mouseenter',
36 | 'mouseleave',
37 | 'contextmenu'
38 | ];
39 |
40 | /**
41 | * Session data
42 | * Private properties global to all powerTip instances
43 | */
44 | var session = {
45 | elements: [],
46 | tooltips: null,
47 | isTipOpen: false,
48 | isFixedTipOpen: false,
49 | isClosing: false,
50 | tipOpenImminent: false,
51 | activeHover: null,
52 | currentX: 0,
53 | currentY: 0,
54 | previousX: 0,
55 | previousY: 0,
56 | desyncTimeout: null,
57 | closeDelayTimeout: null,
58 | mouseTrackingActive: false,
59 | delayInProgress: false,
60 | windowWidth: 0,
61 | windowHeight: 0,
62 | scrollTop: 0,
63 | scrollLeft: 0
64 | };
65 |
66 | /**
67 | * Collision enumeration
68 | * @enum {number}
69 | */
70 | var Collision = {
71 | none: 0,
72 | top: 1,
73 | bottom: 2,
74 | left: 4,
75 | right: 8
76 | };
77 |
78 | /**
79 | * Display hover tooltips on the matched elements.
80 | * @param {(Object|string)=} opts The options object to use for the plugin, or
81 | * the name of a method to invoke on the first matched element.
82 | * @param {*=} [arg] Argument for an invoked method (optional).
83 | * @return {jQuery} jQuery object for the matched selectors.
84 | */
85 | $.fn.powerTip = function(opts, arg) {
86 | var targetElements = this,
87 | options,
88 | tipController;
89 |
90 | // don't do any work if there were no matched elements
91 | if (!targetElements.length) {
92 | return targetElements;
93 | }
94 |
95 | // handle api method calls on the plugin, e.g. powerTip('hide')
96 | if (typeof opts === 'string' && $.powerTip[opts]) {
97 | return $.powerTip[opts].call(targetElements, targetElements, arg);
98 | }
99 |
100 | // extend options
101 | options = $.extend({}, $.fn.powerTip.defaults, opts);
102 |
103 | // handle repeated powerTip calls on the same element by destroying any
104 | // original instance hooked to it and replacing it with this call
105 | $.powerTip.destroy(targetElements);
106 |
107 | // instantiate the TooltipController for this instance
108 | tipController = new TooltipController(options);
109 |
110 | // hook mouse and viewport dimension tracking
111 | initTracking();
112 |
113 | // setup the elements
114 | targetElements.each(function elementSetup() {
115 | var $this = $(this),
116 | dataPowertip = $this.data(DATA_POWERTIP),
117 | dataElem = $this.data(DATA_POWERTIPJQ),
118 | dataTarget = $this.data(DATA_POWERTIPTARGET),
119 | title = $this.attr('title');
120 |
121 | // attempt to use title attribute text if there is no data-powertip,
122 | // data-powertipjq or data-powertiptarget. If we do use the title
123 | // attribute, delete the attribute so the browser will not show it
124 | if (!dataPowertip && !dataTarget && !dataElem && title) {
125 | $this.data(DATA_POWERTIP, title);
126 | $this.data(DATA_ORIGINALTITLE, title);
127 | $this.removeAttr('title');
128 | }
129 |
130 | // create hover controllers for each element
131 | $this.data(
132 | DATA_DISPLAYCONTROLLER,
133 | new DisplayController($this, options, tipController)
134 | );
135 | });
136 |
137 | // attach events to matched elements if the manual option is not enabled
138 | if (!options.manual) {
139 | // attach open events
140 | $.each(options.openEvents, function(idx, evt) {
141 | if ($.inArray(evt, options.closeEvents) > -1) {
142 | // event is in both openEvents and closeEvents, so toggle it
143 | targetElements.on(evt + EVENT_NAMESPACE, function elementToggle(event) {
144 | $.powerTip.toggle(this, event);
145 | });
146 | } else {
147 | targetElements.on(evt + EVENT_NAMESPACE, function elementOpen(event) {
148 | $.powerTip.show(this, event);
149 | });
150 | }
151 | });
152 |
153 | // attach close events
154 | $.each(options.closeEvents, function(idx, evt) {
155 | if ($.inArray(evt, options.openEvents) < 0) {
156 | targetElements.on(evt + EVENT_NAMESPACE, function elementClose(event) {
157 | // set immediate to true for any event without mouse info
158 | $.powerTip.hide(this, !isMouseEvent(event));
159 | });
160 | }
161 | });
162 |
163 | // attach escape key close event
164 | targetElements.on('keydown' + EVENT_NAMESPACE, function elementKeyDown(event) {
165 | // always close tooltip when the escape key is pressed
166 | if (event.keyCode === 27) {
167 | $.powerTip.hide(this, true);
168 | }
169 | });
170 | }
171 |
172 | // remember elements that the plugin is attached to
173 | session.elements.push(targetElements);
174 |
175 | return targetElements;
176 | };
177 |
178 | /**
179 | * Default options for the powerTip plugin.
180 | */
181 | $.fn.powerTip.defaults = {
182 | fadeInTime: 200,
183 | fadeOutTime: 100,
184 | followMouse: false,
185 | popupId: 'powerTip',
186 | popupClass: null,
187 | intentSensitivity: 7,
188 | intentPollInterval: 100,
189 | closeDelay: 100,
190 | placement: 'n',
191 | smartPlacement: false,
192 | offset: 10,
193 | mouseOnToPopup: false,
194 | manual: false,
195 | openEvents: [ 'mouseenter', 'focus' ],
196 | closeEvents: [ 'mouseleave', 'blur' ]
197 | };
198 |
199 | /**
200 | * Default smart placement priority lists.
201 | * The first item in the array is the highest priority, the last is the lowest.
202 | * The last item is also the default, which will be used if all previous options
203 | * do not fit.
204 | */
205 | $.fn.powerTip.smartPlacementLists = {
206 | n: [ 'n', 'ne', 'nw', 's' ],
207 | e: [ 'e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e' ],
208 | s: [ 's', 'se', 'sw', 'n' ],
209 | w: [ 'w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w' ],
210 | nw: [ 'nw', 'w', 'sw', 'n', 's', 'se', 'nw' ],
211 | ne: [ 'ne', 'e', 'se', 'n', 's', 'sw', 'ne' ],
212 | sw: [ 'sw', 'w', 'nw', 's', 'n', 'ne', 'sw' ],
213 | se: [ 'se', 'e', 'ne', 's', 'n', 'nw', 'se' ],
214 | 'nw-alt': [ 'nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e' ],
215 | 'ne-alt': [ 'ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w' ],
216 | 'sw-alt': [ 'sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e' ],
217 | 'se-alt': [ 'se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w' ]
218 | };
219 |
220 | /**
221 | * Public API
222 | */
223 | $.powerTip = {
224 | /**
225 | * Attempts to show the tooltip for the specified element.
226 | * @param {jQuery|Element} element The element to open the tooltip for.
227 | * @param {jQuery.Event=} event jQuery event for hover intent and mouse
228 | * tracking (optional).
229 | * @return {jQuery|Element} The original jQuery object or DOM Element.
230 | */
231 | show: function apiShowTip(element, event) {
232 | // if we were given a mouse event then run the hover intent testing,
233 | // otherwise, simply show the tooltip asap
234 | if (isMouseEvent(event)) {
235 | trackMouse(event);
236 | session.previousX = event.pageX;
237 | session.previousY = event.pageY;
238 | $(element).data(DATA_DISPLAYCONTROLLER).show();
239 | } else {
240 | $(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true);
241 | }
242 | return element;
243 | },
244 |
245 | /**
246 | * Repositions the tooltip on the element.
247 | * @param {jQuery|Element} element The element the tooltip is shown for.
248 | * @return {jQuery|Element} The original jQuery object or DOM Element.
249 | */
250 | reposition: function apiResetPosition(element) {
251 | $(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition();
252 | return element;
253 | },
254 |
255 | /**
256 | * Attempts to close any open tooltips.
257 | * @param {(jQuery|Element)=} element The element with the tooltip that
258 | * should be closed (optional).
259 | * @param {boolean=} immediate Disable close delay (optional).
260 | * @return {jQuery|Element|undefined} The original jQuery object or DOM
261 | * Element, if one was specified.
262 | */
263 | hide: function apiCloseTip(element, immediate) {
264 | var displayController;
265 |
266 | // set immediate to true when no element is specified
267 | immediate = element ? immediate : true;
268 |
269 | // find the relevant display controller
270 | if (element) {
271 | displayController = $(element).first().data(DATA_DISPLAYCONTROLLER);
272 | } else if (session.activeHover) {
273 | displayController = session.activeHover.data(DATA_DISPLAYCONTROLLER);
274 | }
275 |
276 | // if found, hide the tip
277 | if (displayController) {
278 | displayController.hide(immediate);
279 | }
280 |
281 | return element;
282 | },
283 |
284 | /**
285 | * Toggles the tooltip for the specified element. This will open a closed
286 | * tooltip, or close an open tooltip.
287 | * @param {jQuery|Element} element The element with the tooltip that
288 | * should be toggled.
289 | * @param {jQuery.Event=} event jQuery event for hover intent and mouse
290 | * tracking (optional).
291 | * @return {jQuery|Element} The original jQuery object or DOM Element.
292 | */
293 | toggle: function apiToggle(element, event) {
294 | if (session.activeHover && session.activeHover.is(element)) {
295 | // tooltip for element is active, so close it
296 | $.powerTip.hide(element, !isMouseEvent(event));
297 | } else {
298 | // tooltip for element is not active, so open it
299 | $.powerTip.show(element, event);
300 | }
301 | return element;
302 | },
303 |
304 | /**
305 | * Destroy and roll back any powerTip() instance on the specified elements.
306 | * If no elements are specified then all elements that the plugin is
307 | * currently attached to will be rolled back.
308 | * @param {(jQuery|Element)=} element The element with the powerTip instance.
309 | * @return {jQuery|Element|undefined} The original jQuery object or DOM
310 | * Element, if one was specified.
311 | */
312 | destroy: function apiDestroy(element) {
313 | var $element,
314 | foundPowerTip = false,
315 | runTipCheck = true,
316 | i;
317 |
318 | // if the plugin is not hooked to any elements then there is no point
319 | // trying to destroy anything, or dealing with the possible errors
320 | if (session.elements.length === 0) {
321 | return element;
322 | }
323 |
324 | if (element) {
325 | // make sure we're working with a jQuery object
326 | $element = $(element);
327 | } else {
328 | // if we are being asked to destroy all instances, then iterate the
329 | // array of jQuery objects that we've been tracking and call destroy
330 | // for each group
331 | $.each(session.elements, function cleanElsTracking(idx, els) {
332 | $.powerTip.destroy(els);
333 | });
334 |
335 | // reset elements list
336 | // if a dev calls .remove() on an element before calling this
337 | // destroy() method then jQuery will have deleted all of the .data()
338 | // information, so it will not be recognized as a PowerTip element,
339 | // which could leave dangling references in this array - causing
340 | // future destroy() (no param) invocations to not fully clean up -
341 | // so make sure the array is empty and set a flag to skip the
342 | // element check before proceeding
343 | session.elements = [];
344 | runTipCheck = false;
345 |
346 | // set $element to an empty jQuery object to proceed
347 | $element = $();
348 | }
349 |
350 | // check if PowerTip has been set on any of the elements - if PowerTip
351 | // has not been found then return early to skip the slow .not()
352 | // operation below - only if we did not reset the elements list above
353 | if (runTipCheck) {
354 | $element.each(function checkForPowerTip() {
355 | var $this = $(this);
356 | if ($this.data(DATA_DISPLAYCONTROLLER)) {
357 | foundPowerTip = true;
358 | return false;
359 | }
360 | return true;
361 | });
362 | if (!foundPowerTip) {
363 | return element;
364 | }
365 | }
366 |
367 | // if a tooltip is currently open for an element we are being asked to
368 | // destroy then it should be forced to close
369 | if (session.isTipOpen && !session.isClosing && $element.filter(session.activeHover).length > 0) {
370 | // if the tooltip is waiting to close then cancel that delay timer
371 | if (session.delayInProgress) {
372 | session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
373 | }
374 | // hide the tooltip, immediately
375 | $.powerTip.hide(session.activeHover, true);
376 | }
377 |
378 | // unhook events and destroy plugin changes to each element
379 | $element.off(EVENT_NAMESPACE).each(function destroy() {
380 | var $this = $(this),
381 | dataAttributes = [
382 | DATA_ORIGINALTITLE,
383 | DATA_DISPLAYCONTROLLER,
384 | DATA_HASACTIVEHOVER,
385 | DATA_FORCEDOPEN
386 | ];
387 |
388 | // revert title attribute
389 | if ($this.data(DATA_ORIGINALTITLE)) {
390 | $this.attr('title', $this.data(DATA_ORIGINALTITLE));
391 | dataAttributes.push(DATA_POWERTIP);
392 | }
393 |
394 | // remove data attributes
395 | $this.removeData(dataAttributes);
396 | });
397 |
398 | // remove destroyed element from active elements collection
399 | for (i = session.elements.length - 1; i >= 0; i--) {
400 | session.elements[i] = session.elements[i].not($element);
401 |
402 | // check if there are any more elements left in this collection, if
403 | // there is not then remove it from the elements array
404 | if (session.elements[i].length === 0) {
405 | session.elements.splice(i, 1);
406 | }
407 | }
408 |
409 | // if there are no active elements left then we will unhook all of the
410 | // events that we've bound code to and remove the tooltip elements
411 | if (session.elements.length === 0) {
412 | $window.off(EVENT_NAMESPACE);
413 | $document.off(EVENT_NAMESPACE);
414 | session.mouseTrackingActive = false;
415 | if (session.tooltips) {
416 | session.tooltips.remove();
417 | session.tooltips = null;
418 | }
419 | }
420 |
421 | return element;
422 | }
423 | };
424 |
425 | // API aliasing
426 | // for backwards compatibility with versions earlier than 1.2.0
427 | $.powerTip.showTip = $.powerTip.show;
428 | $.powerTip.closeTip = $.powerTip.hide;
429 |
--------------------------------------------------------------------------------
/src/csscoordinates.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PowerTip CSSCoordinates
3 | *
4 | * @fileoverview CSSCoordinates object for describing CSS positions.
5 | * @link https://stevenbenner.github.io/jquery-powertip/
6 | * @author Steven Benner (https://stevenbenner.com/)
7 | * @requires jQuery 1.7+
8 | */
9 |
10 | /**
11 | * Creates a new CSSCoordinates object.
12 | * @private
13 | * @constructor
14 | */
15 | function CSSCoordinates() {
16 | var me = this;
17 |
18 | // initialize object properties
19 | me.top = 'auto';
20 | me.left = 'auto';
21 | me.right = 'auto';
22 | me.bottom = 'auto';
23 |
24 | /**
25 | * Set a property to a value.
26 | * @private
27 | * @param {string} property The name of the property.
28 | * @param {number} value The value of the property.
29 | */
30 | me.set = function(property, value) {
31 | if (typeof value === 'number') {
32 | me[property] = Math.round(value);
33 | }
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/displaycontroller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PowerTip DisplayController
3 | *
4 | * @fileoverview DisplayController object used to manage tooltips for elements.
5 | * @link https://stevenbenner.github.io/jquery-powertip/
6 | * @author Steven Benner (https://stevenbenner.com/)
7 | * @requires jQuery 1.7+
8 | */
9 |
10 | /**
11 | * Creates a new tooltip display controller.
12 | * @private
13 | * @constructor
14 | * @param {jQuery} element The element that this controller will handle.
15 | * @param {Object} options Options object containing settings.
16 | * @param {TooltipController} tipController The TooltipController object for
17 | * this instance.
18 | */
19 | function DisplayController(element, options, tipController) {
20 | var hoverTimer = null,
21 | myCloseDelay = null;
22 |
23 | /**
24 | * Begins the process of showing a tooltip.
25 | * @private
26 | * @param {boolean=} immediate Skip intent testing (optional).
27 | * @param {boolean=} forceOpen Ignore cursor position and force tooltip to
28 | * open (optional).
29 | */
30 | function openTooltip(immediate, forceOpen) {
31 | cancelTimer();
32 | if (!element.data(DATA_HASACTIVEHOVER)) {
33 | if (!immediate) {
34 | session.tipOpenImminent = true;
35 | hoverTimer = setTimeout(
36 | function intentDelay() {
37 | hoverTimer = null;
38 | checkForIntent();
39 | },
40 | options.intentPollInterval
41 | );
42 | } else {
43 | if (forceOpen) {
44 | element.data(DATA_FORCEDOPEN, true);
45 | }
46 | closeAnyDelayed();
47 | tipController.showTip(element);
48 | }
49 | } else {
50 | // cursor left and returned to this element, cancel close
51 | cancelClose();
52 | }
53 | }
54 |
55 | /**
56 | * Begins the process of closing a tooltip.
57 | * @private
58 | * @param {boolean=} disableDelay Disable close delay (optional).
59 | */
60 | function closeTooltip(disableDelay) {
61 | // if this instance already has a close delay in progress then halt it
62 | if (myCloseDelay) {
63 | session.closeDelayTimeout = clearTimeout(myCloseDelay);
64 | myCloseDelay = session.closeDelayTimeout;
65 | session.delayInProgress = false;
66 | }
67 | cancelTimer();
68 | session.tipOpenImminent = false;
69 | if (element.data(DATA_HASACTIVEHOVER)) {
70 | element.data(DATA_FORCEDOPEN, false);
71 | if (!disableDelay) {
72 | session.delayInProgress = true;
73 | session.closeDelayTimeout = setTimeout(
74 | function closeDelay() {
75 | session.closeDelayTimeout = null;
76 | tipController.hideTip(element);
77 | session.delayInProgress = false;
78 | myCloseDelay = null;
79 | },
80 | options.closeDelay
81 | );
82 | // save internal reference close delay id so we can check if the
83 | // active close delay belongs to this instance
84 | myCloseDelay = session.closeDelayTimeout;
85 | } else {
86 | tipController.hideTip(element);
87 | }
88 | }
89 | }
90 |
91 | /**
92 | * Checks mouse position to make sure that the user intended to hover on the
93 | * specified element before showing the tooltip.
94 | * @private
95 | */
96 | function checkForIntent() {
97 | // calculate mouse position difference
98 | var xDifference = Math.abs(session.previousX - session.currentX),
99 | yDifference = Math.abs(session.previousY - session.currentY),
100 | totalDifference = xDifference + yDifference;
101 |
102 | // check if difference has passed the sensitivity threshold
103 | if (totalDifference < options.intentSensitivity) {
104 | cancelClose();
105 | closeAnyDelayed();
106 | tipController.showTip(element);
107 | } else {
108 | // try again
109 | session.previousX = session.currentX;
110 | session.previousY = session.currentY;
111 | openTooltip();
112 | }
113 | }
114 |
115 | /**
116 | * Cancels active hover timer.
117 | * @private
118 | * @param {boolean=} stopClose Cancel any active close delay timer.
119 | */
120 | function cancelTimer(stopClose) {
121 | hoverTimer = clearTimeout(hoverTimer);
122 | // cancel the current close delay if the active close delay is for this
123 | // element or the stopClose argument is true
124 | if (session.closeDelayTimeout && myCloseDelay === session.closeDelayTimeout || stopClose) {
125 | cancelClose();
126 | }
127 | }
128 |
129 | /**
130 | * Cancels any active close delay timer.
131 | * @private
132 | */
133 | function cancelClose() {
134 | session.closeDelayTimeout = clearTimeout(session.closeDelayTimeout);
135 | session.delayInProgress = false;
136 | }
137 |
138 | /**
139 | * Asks any tooltips waiting on their close delay to close now.
140 | * @private
141 | */
142 | function closeAnyDelayed() {
143 | // if another element is waiting for its close delay then we should ask
144 | // it to close immediately so we can proceed without unexpected timeout
145 | // code being run during this tooltip's lifecycle
146 | if (session.delayInProgress && session.activeHover && !session.activeHover.is(element)) {
147 | session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true);
148 | }
149 | }
150 |
151 | /**
152 | * Repositions the tooltip on this element.
153 | * @private
154 | */
155 | function repositionTooltip() {
156 | tipController.resetPosition(element);
157 | }
158 |
159 | // expose the methods
160 | this.show = openTooltip;
161 | this.hide = closeTooltip;
162 | this.cancel = cancelTimer;
163 | this.resetPosition = repositionTooltip;
164 | }
165 |
--------------------------------------------------------------------------------
/src/placementcalculator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PowerTip PlacementCalculator
3 | *
4 | * @fileoverview PlacementCalculator object that computes tooltip position.
5 | * @link https://stevenbenner.github.io/jquery-powertip/
6 | * @author Steven Benner (https://stevenbenner.com/)
7 | * @requires jQuery 1.7+
8 | */
9 |
10 | /**
11 | * Creates a new Placement Calculator.
12 | * @private
13 | * @constructor
14 | */
15 | function PlacementCalculator() {
16 | /**
17 | * Compute the CSS position to display a tooltip at the specified placement
18 | * relative to the specified element.
19 | * @private
20 | * @param {jQuery} element The element that the tooltip should target.
21 | * @param {string} placement The placement for the tooltip.
22 | * @param {number} tipWidth Width of the tooltip element in pixels.
23 | * @param {number} tipHeight Height of the tooltip element in pixels.
24 | * @param {number} offset Distance to offset tooltips in pixels.
25 | * @return {CSSCoordinates} A CSSCoordinates object with the position.
26 | */
27 | function computePlacementCoords(element, placement, tipWidth, tipHeight, offset) {
28 | var placementBase = placement.split('-')[0], // ignore 'alt' for corners
29 | coords = new CSSCoordinates(),
30 | position;
31 |
32 | if (isSvgElement(element)) {
33 | position = getSvgPlacement(element, placementBase);
34 | } else {
35 | position = getHtmlPlacement(element, placementBase);
36 | }
37 |
38 | // calculate the appropriate x and y position in the document
39 | switch (placement) {
40 | case 'n':
41 | coords.set('left', position.left - (tipWidth / 2));
42 | coords.set('bottom', session.windowHeight - position.top + offset);
43 | break;
44 | case 'e':
45 | coords.set('left', position.left + offset);
46 | coords.set('top', position.top - (tipHeight / 2));
47 | break;
48 | case 's':
49 | coords.set('left', position.left - (tipWidth / 2));
50 | coords.set('top', position.top + offset);
51 | break;
52 | case 'w':
53 | coords.set('top', position.top - (tipHeight / 2));
54 | coords.set('right', session.windowWidth - position.left + offset);
55 | break;
56 | case 'nw':
57 | coords.set('bottom', session.windowHeight - position.top + offset);
58 | coords.set('right', session.windowWidth - position.left - 20);
59 | break;
60 | case 'nw-alt':
61 | coords.set('left', position.left);
62 | coords.set('bottom', session.windowHeight - position.top + offset);
63 | break;
64 | case 'ne':
65 | coords.set('left', position.left - 20);
66 | coords.set('bottom', session.windowHeight - position.top + offset);
67 | break;
68 | case 'ne-alt':
69 | coords.set('bottom', session.windowHeight - position.top + offset);
70 | coords.set('right', session.windowWidth - position.left);
71 | break;
72 | case 'sw':
73 | coords.set('top', position.top + offset);
74 | coords.set('right', session.windowWidth - position.left - 20);
75 | break;
76 | case 'sw-alt':
77 | coords.set('left', position.left);
78 | coords.set('top', position.top + offset);
79 | break;
80 | case 'se':
81 | coords.set('left', position.left - 20);
82 | coords.set('top', position.top + offset);
83 | break;
84 | case 'se-alt':
85 | coords.set('top', position.top + offset);
86 | coords.set('right', session.windowWidth - position.left);
87 | break;
88 | }
89 |
90 | return coords;
91 | }
92 |
93 | /**
94 | * Finds the tooltip attachment point in the document for a HTML DOM element
95 | * for the specified placement.
96 | * @private
97 | * @param {jQuery} element The element that the tooltip should target.
98 | * @param {string} placement The placement for the tooltip.
99 | * @return {Object} An object with the top,left position values.
100 | */
101 | function getHtmlPlacement(element, placement) {
102 | var objectOffset = element.offset(),
103 | objectWidth = element.outerWidth(),
104 | objectHeight = element.outerHeight(),
105 | left,
106 | top;
107 |
108 | // calculate the appropriate x and y position in the document
109 | switch (placement) {
110 | case 'n':
111 | left = objectOffset.left + objectWidth / 2;
112 | top = objectOffset.top;
113 | break;
114 | case 'e':
115 | left = objectOffset.left + objectWidth;
116 | top = objectOffset.top + objectHeight / 2;
117 | break;
118 | case 's':
119 | left = objectOffset.left + objectWidth / 2;
120 | top = objectOffset.top + objectHeight;
121 | break;
122 | case 'w':
123 | left = objectOffset.left;
124 | top = objectOffset.top + objectHeight / 2;
125 | break;
126 | case 'nw':
127 | left = objectOffset.left;
128 | top = objectOffset.top;
129 | break;
130 | case 'ne':
131 | left = objectOffset.left + objectWidth;
132 | top = objectOffset.top;
133 | break;
134 | case 'sw':
135 | left = objectOffset.left;
136 | top = objectOffset.top + objectHeight;
137 | break;
138 | case 'se':
139 | left = objectOffset.left + objectWidth;
140 | top = objectOffset.top + objectHeight;
141 | break;
142 | }
143 |
144 | return {
145 | top: top,
146 | left: left
147 | };
148 | }
149 |
150 | /**
151 | * Finds the tooltip attachment point in the document for a SVG element for
152 | * the specified placement.
153 | * @private
154 | * @param {jQuery} element The element that the tooltip should target.
155 | * @param {string} placement The placement for the tooltip.
156 | * @return {Object} An object with the top,left position values.
157 | */
158 | function getSvgPlacement(element, placement) {
159 | var svgElement = element.closest('svg')[0],
160 | domElement = element[0],
161 | point = svgElement.createSVGPoint(),
162 | boundingBox = domElement.getBBox(),
163 | matrix = domElement.getScreenCTM(),
164 | halfWidth = boundingBox.width / 2,
165 | halfHeight = boundingBox.height / 2,
166 | placements = [],
167 | placementKeys = [ 'nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w' ],
168 | coords,
169 | rotation,
170 | steps,
171 | x;
172 |
173 | /**
174 | * Transform and append the current points to the placements list.
175 | * @private
176 | */
177 | function pushPlacement() {
178 | placements.push(point.matrixTransform(matrix));
179 | }
180 |
181 | // get bounding box corners and midpoints
182 | point.x = boundingBox.x;
183 | point.y = boundingBox.y;
184 | pushPlacement();
185 | point.x += halfWidth;
186 | pushPlacement();
187 | point.x += halfWidth;
188 | pushPlacement();
189 | point.y += halfHeight;
190 | pushPlacement();
191 | point.y += halfHeight;
192 | pushPlacement();
193 | point.x -= halfWidth;
194 | pushPlacement();
195 | point.x -= halfWidth;
196 | pushPlacement();
197 | point.y -= halfHeight;
198 | pushPlacement();
199 |
200 | // determine rotation
201 | if (placements[0].y !== placements[1].y || placements[0].x !== placements[7].x) {
202 | rotation = Math.atan2(matrix.b, matrix.a) * RAD2DEG;
203 | steps = Math.ceil(((rotation % 360) - 22.5) / 45);
204 | if (steps < 1) {
205 | steps += 8;
206 | }
207 | while (steps--) {
208 | placementKeys.push(placementKeys.shift());
209 | }
210 | }
211 |
212 | // find placement
213 | for (x = 0; x < placements.length; x++) {
214 | if (placementKeys[x] === placement) {
215 | coords = placements[x];
216 | break;
217 | }
218 | }
219 |
220 | return {
221 | top: coords.y + session.scrollTop,
222 | left: coords.x + session.scrollLeft
223 | };
224 | }
225 |
226 | // expose methods
227 | this.compute = computePlacementCoords;
228 | }
229 |
--------------------------------------------------------------------------------
/src/tooltipcontroller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PowerTip TooltipController
3 | *
4 | * @fileoverview TooltipController object that manages tips for an instance.
5 | * @link https://stevenbenner.github.io/jquery-powertip/
6 | * @author Steven Benner (https://stevenbenner.com/)
7 | * @requires jQuery 1.7+
8 | */
9 |
10 | /**
11 | * Creates a new tooltip controller.
12 | * @private
13 | * @constructor
14 | * @param {Object} options Options object containing settings.
15 | */
16 | function TooltipController(options) {
17 | var placementCalculator = new PlacementCalculator(),
18 | tipElement = $('#' + options.popupId);
19 |
20 | // build and append tooltip div if it does not already exist
21 | if (tipElement.length === 0) {
22 | tipElement = $('', { id: options.popupId });
23 | // grab body element if it was not populated when the script loaded
24 | // note: this hack exists solely for jsfiddle support
25 | if ($body.length === 0) {
26 | $body = $('body');
27 | }
28 | $body.append(tipElement);
29 | // remember the tooltip elements that the plugin has created
30 | session.tooltips = session.tooltips ? session.tooltips.add(tipElement) : tipElement;
31 | }
32 |
33 | // hook mousemove for cursor follow tooltips
34 | if (options.followMouse) {
35 | // only one positionTipOnCursor hook per tooltip element, please
36 | if (!tipElement.data(DATA_HASMOUSEMOVE)) {
37 | $document.on('mousemove' + EVENT_NAMESPACE, positionTipOnCursor);
38 | $window.on('scroll' + EVENT_NAMESPACE, positionTipOnCursor);
39 | tipElement.data(DATA_HASMOUSEMOVE, true);
40 | }
41 | }
42 |
43 | /**
44 | * Gives the specified element the active-hover state and queues up the
45 | * showTip function.
46 | * @private
47 | * @param {jQuery} element The element that the tooltip should target.
48 | */
49 | function beginShowTip(element) {
50 | element.data(DATA_HASACTIVEHOVER, true);
51 | // show tooltip, asap
52 | tipElement.queue(function queueTipInit(next) {
53 | showTip(element);
54 | next();
55 | });
56 | }
57 |
58 | /**
59 | * Shows the tooltip, as soon as possible.
60 | * @private
61 | * @param {jQuery} element The element that the tooltip should target.
62 | */
63 | function showTip(element) {
64 | var tipContent;
65 |
66 | // it is possible, especially with keyboard navigation, to move on to
67 | // another element with a tooltip during the queue to get to this point
68 | // in the code. if that happens then we need to not proceed or we may
69 | // have the fadeout callback for the last tooltip execute immediately
70 | // after this code runs, causing bugs.
71 | if (!element.data(DATA_HASACTIVEHOVER)) {
72 | return;
73 | }
74 |
75 | // if the tooltip is open and we got asked to open another one then the
76 | // old one is still in its fadeOut cycle, so wait and try again
77 | if (session.isTipOpen) {
78 | if (!session.isClosing) {
79 | hideTip(session.activeHover);
80 | }
81 | tipElement.delay(100).queue(function queueTipAgain(next) {
82 | showTip(element);
83 | next();
84 | });
85 | return;
86 | }
87 |
88 | // trigger powerTipPreRender event
89 | element.trigger('powerTipPreRender');
90 |
91 | // set tooltip content
92 | tipContent = getTooltipContent(element);
93 | if (tipContent) {
94 | tipElement.empty().append(tipContent);
95 | } else {
96 | // we have no content to display, give up
97 | return;
98 | }
99 |
100 | // trigger powerTipRender event
101 | element.trigger('powerTipRender');
102 |
103 | session.activeHover = element;
104 | session.isTipOpen = true;
105 |
106 | tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup);
107 |
108 | // add custom class to tooltip element
109 | tipElement.addClass(options.popupClass);
110 |
111 | // set tooltip position
112 | // revert to static placement when the "force open" flag was set because
113 | // that flag means that we do not have accurate mouse position info
114 | if (!options.followMouse || element.data(DATA_FORCEDOPEN)) {
115 | positionTipOnElement(element);
116 | session.isFixedTipOpen = true;
117 | } else {
118 | positionTipOnCursor();
119 | }
120 |
121 | // close tooltip when clicking anywhere on the page, with the exception
122 | // of the tooltip's trigger element and any elements that are within a
123 | // tooltip that has 'mouseOnToPopup' option enabled
124 | // always enable this feature when the "force open" flag is set on a
125 | // followMouse tooltip because we reverted to static placement above
126 | if (!element.data(DATA_FORCEDOPEN) && !options.followMouse) {
127 | $document.on('click' + EVENT_NAMESPACE, function documentClick(event) {
128 | var target = event.target;
129 | if (target !== element[0]) {
130 | if (options.mouseOnToPopup) {
131 | if (target !== tipElement[0] && !$.contains(tipElement[0], target)) {
132 | $.powerTip.hide();
133 | }
134 | } else {
135 | $.powerTip.hide();
136 | }
137 | }
138 | });
139 | }
140 |
141 | // if we want to be able to mouse on to the tooltip then we need to
142 | // attach hover events to the tooltip that will cancel a close request
143 | // on mouseenter and start a new close request on mouseleave
144 | // only hook these listeners if we're not in manual mode
145 | if (options.mouseOnToPopup && !options.manual && $.inArray('mouseleave', options.closeEvents) > -1) {
146 | tipElement.on('mouseenter' + EVENT_NAMESPACE, function tipMouseEnter() {
147 | // check activeHover in case the mouse cursor entered the
148 | // tooltip during the fadeOut and close cycle
149 | if (session.activeHover) {
150 | session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
151 | }
152 | });
153 | tipElement.on('mouseleave' + EVENT_NAMESPACE, function tipMouseLeave() {
154 | // check activeHover in case the mouse cursor left the tooltip
155 | // during the fadeOut and close cycle
156 | if (session.activeHover) {
157 | session.activeHover.data(DATA_DISPLAYCONTROLLER).hide();
158 | }
159 | });
160 | }
161 |
162 | // fadein
163 | tipElement.fadeIn(options.fadeInTime, function fadeInCallback() {
164 | // start desync polling
165 | if (!session.desyncTimeout) {
166 | session.desyncTimeout = setInterval(closeDesyncedTip, 500);
167 | }
168 |
169 | // trigger powerTipOpen event
170 | element.trigger('powerTipOpen');
171 | });
172 | }
173 |
174 | /**
175 | * Hides the tooltip.
176 | * @private
177 | * @param {jQuery} element The element that the tooltip should target.
178 | */
179 | function hideTip(element) {
180 | // reset session
181 | session.isClosing = true;
182 | session.isTipOpen = false;
183 |
184 | // stop desync polling
185 | session.desyncTimeout = clearInterval(session.desyncTimeout);
186 |
187 | // reset element state
188 | element.data(DATA_HASACTIVEHOVER, false);
189 | element.data(DATA_FORCEDOPEN, false);
190 |
191 | // remove document click handler
192 | $document.off('click' + EVENT_NAMESPACE);
193 |
194 | // unbind the mouseOnToPopup events if they were set
195 | tipElement.off(EVENT_NAMESPACE);
196 |
197 | // fade out
198 | tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() {
199 | var coords = new CSSCoordinates();
200 |
201 | // reset session and tooltip element
202 | session.activeHover = null;
203 | session.isClosing = false;
204 | session.isFixedTipOpen = false;
205 | tipElement.removeClass();
206 |
207 | // support mouse-follow and fixed position tips at the same time by
208 | // moving the tooltip to the last cursor location after it is hidden
209 | coords.set('top', session.currentY + options.offset);
210 | coords.set('left', session.currentX + options.offset);
211 | tipElement.css(coords);
212 |
213 | // trigger powerTipClose event
214 | element.trigger('powerTipClose');
215 | });
216 | }
217 |
218 | /**
219 | * Moves the tooltip to the user's mouse cursor.
220 | * @private
221 | */
222 | function positionTipOnCursor() {
223 | var tipWidth,
224 | tipHeight,
225 | coords,
226 | collisions,
227 | collisionCount;
228 |
229 | // to support having fixed tooltips on the same page as cursor tooltips,
230 | // where both instances are referencing the same tooltip element, we
231 | // need to keep track of the mouse position constantly, but we should
232 | // only set the tip location if a fixed tip is not currently open, a tip
233 | // open is imminent or active, and the tooltip element in question does
234 | // have a mouse-follow using it.
235 | if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) {
236 | // grab measurements
237 | tipWidth = tipElement.outerWidth();
238 | tipHeight = tipElement.outerHeight();
239 | coords = new CSSCoordinates();
240 |
241 | // grab collisions
242 | coords.set('top', session.currentY + options.offset);
243 | coords.set('left', session.currentX + options.offset);
244 | collisions = getViewportCollisions(
245 | coords,
246 | tipWidth,
247 | tipHeight
248 | );
249 |
250 | // handle tooltip view port collisions
251 | if (collisions !== Collision.none) {
252 | collisionCount = countFlags(collisions);
253 | if (collisionCount === 1) {
254 | // if there is only one collision (bottom or right) then
255 | // simply constrain the tooltip to the view port
256 | if (collisions === Collision.right) {
257 | coords.set('left', session.scrollLeft + session.windowWidth - tipWidth);
258 | } else if (collisions === Collision.bottom) {
259 | coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
260 | }
261 | } else {
262 | // if the tooltip has more than one collision then it is
263 | // trapped in the corner and should be flipped to get it out
264 | // of the user's way
265 | coords.set('left', session.currentX - tipWidth - options.offset);
266 | coords.set('top', session.currentY - tipHeight - options.offset);
267 | }
268 | }
269 |
270 | // position the tooltip
271 | tipElement.css(coords);
272 | }
273 | }
274 |
275 | /**
276 | * Sets the tooltip to the correct position relative to the specified target
277 | * element. Based on options settings.
278 | * @private
279 | * @param {jQuery} element The element that the tooltip should target.
280 | */
281 | function positionTipOnElement(element) {
282 | var priorityList,
283 | finalPlacement;
284 |
285 | // when the followMouse option is enabled and the "force open" flag is
286 | // set we revert to static positioning. since the developer may not have
287 | // considered this scenario we should use smart placement
288 | if (options.smartPlacement || (options.followMouse && element.data(DATA_FORCEDOPEN))) {
289 | priorityList = $.fn.powerTip.smartPlacementLists[options.placement];
290 |
291 | // iterate over the priority list and use the first placement option
292 | // that does not collide with the view port. if they all collide
293 | // then the last placement in the list will be used.
294 | $.each(priorityList, function(idx, pos) {
295 | // place tooltip and find collisions
296 | var collisions = getViewportCollisions(
297 | placeTooltip(element, pos),
298 | tipElement.outerWidth(),
299 | tipElement.outerHeight()
300 | );
301 |
302 | // update the final placement variable
303 | finalPlacement = pos;
304 |
305 | // break if there were no collisions
306 | return collisions !== Collision.none;
307 | });
308 | } else {
309 | // if we're not going to use the smart placement feature then just
310 | // compute the coordinates and do it
311 | placeTooltip(element, options.placement);
312 | finalPlacement = options.placement;
313 | }
314 |
315 | // add placement as class for CSS arrows
316 | tipElement.removeClass('w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt');
317 | tipElement.addClass(finalPlacement);
318 | }
319 |
320 | /**
321 | * Sets the tooltip position to the appropriate values to show the tip at
322 | * the specified placement. This function will iterate and test the tooltip
323 | * to support elastic tooltips.
324 | * @private
325 | * @param {jQuery} element The element that the tooltip should target.
326 | * @param {string} placement The placement for the tooltip.
327 | * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and
328 | * right position values.
329 | */
330 | function placeTooltip(element, placement) {
331 | var iterationCount = 0,
332 | tipWidth,
333 | tipHeight,
334 | coords = new CSSCoordinates();
335 |
336 | // set the tip to 0,0 to get the full expanded width
337 | coords.set('top', 0);
338 | coords.set('left', 0);
339 | tipElement.css(coords);
340 |
341 | // to support elastic tooltips we need to check for a change in the
342 | // rendered dimensions after the tooltip has been positioned
343 | do {
344 | // grab the current tip dimensions
345 | tipWidth = tipElement.outerWidth();
346 | tipHeight = tipElement.outerHeight();
347 |
348 | // get placement coordinates
349 | coords = placementCalculator.compute(
350 | element,
351 | placement,
352 | tipWidth,
353 | tipHeight,
354 | options.offset
355 | );
356 |
357 | // place the tooltip
358 | tipElement.css(coords);
359 | } while (
360 | // sanity check: limit to 5 iterations, and...
361 | ++iterationCount <= 5 &&
362 | // try again if the dimensions changed after placement
363 | (tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight())
364 | );
365 |
366 | return coords;
367 | }
368 |
369 | /**
370 | * Checks for a tooltip desync and closes the tooltip if one occurs.
371 | * @private
372 | */
373 | function closeDesyncedTip() {
374 | var isDesynced = false,
375 | hasDesyncableCloseEvent = $.grep(
376 | [ 'mouseleave', 'mouseout', 'blur', 'focusout' ],
377 | function(eventType) {
378 | return $.inArray(eventType, options.closeEvents) !== -1;
379 | }
380 | ).length > 0;
381 |
382 | // It is possible for the mouse cursor to leave an element without
383 | // firing the mouseleave or blur event. This most commonly happens when
384 | // the element is disabled under mouse cursor. If this happens it will
385 | // result in a desynced tooltip because the tooltip was never asked to
386 | // close. So we should periodically check for a desync situation and
387 | // close the tip if such a situation arises.
388 | if (session.isTipOpen && !session.isClosing && !session.delayInProgress && hasDesyncableCloseEvent) {
389 | if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) {
390 | // user moused onto another tip or active hover is disabled
391 | isDesynced = true;
392 | } else if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) {
393 | // hanging tip - have to test if mouse position is not over the
394 | // active hover and not over a tooltip set to let the user
395 | // interact with it.
396 | // for keyboard navigation: this only counts if the element does
397 | // not have focus.
398 | // for tooltips opened via the api: we need to check if it has
399 | // the forcedOpen flag.
400 | if (tipElement.data(DATA_MOUSEONTOTIP)) {
401 | if (!isMouseOver(tipElement)) {
402 | isDesynced = true;
403 | }
404 | } else {
405 | isDesynced = true;
406 | }
407 | }
408 |
409 | if (isDesynced) {
410 | // close the desynced tip
411 | hideTip(session.activeHover);
412 | }
413 | }
414 | }
415 |
416 | // expose methods
417 | this.showTip = beginShowTip;
418 | this.hideTip = hideTip;
419 | this.resetPosition = positionTipOnElement;
420 | }
421 |
--------------------------------------------------------------------------------
/src/utility.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PowerTip Utility Functions
3 | *
4 | * @fileoverview Private helper functions.
5 | * @link https://stevenbenner.github.io/jquery-powertip/
6 | * @author Steven Benner (https://stevenbenner.com/)
7 | * @requires jQuery 1.7+
8 | */
9 |
10 | /**
11 | * Determine whether a jQuery object is an SVG element
12 | * @private
13 | * @param {jQuery} element The element to check
14 | * @return {boolean} Whether this is an SVG element
15 | */
16 | function isSvgElement(element) {
17 | return Boolean(window.SVGElement && element[0] instanceof SVGElement);
18 | }
19 |
20 | /**
21 | * Determines if the specified jQuery.Event object has mouse data.
22 | * @private
23 | * @param {jQuery.Event=} event The jQuery.Event object to test.
24 | * @return {boolean} True if there is mouse data, otherwise false.
25 | */
26 | function isMouseEvent(event) {
27 | return Boolean(event && $.inArray(event.type, MOUSE_EVENTS) > -1 &&
28 | typeof event.pageX === 'number');
29 | }
30 |
31 | /**
32 | * Initializes the viewport dimension cache and hooks up the mouse position
33 | * tracking and viewport dimension tracking events.
34 | * Prevents attaching the events more than once.
35 | * @private
36 | */
37 | function initTracking() {
38 | if (!session.mouseTrackingActive) {
39 | session.mouseTrackingActive = true;
40 |
41 | // grab the current viewport dimensions on load
42 | getViewportDimensions();
43 | $(getViewportDimensions);
44 |
45 | // hook mouse move tracking
46 | $document.on('mousemove' + EVENT_NAMESPACE, trackMouse);
47 |
48 | // hook viewport dimensions tracking
49 | $window.on('resize' + EVENT_NAMESPACE, trackResize);
50 | $window.on('scroll' + EVENT_NAMESPACE, trackScroll);
51 | }
52 | }
53 |
54 | /**
55 | * Updates the viewport dimensions cache.
56 | * @private
57 | */
58 | function getViewportDimensions() {
59 | session.scrollLeft = $window.scrollLeft();
60 | session.scrollTop = $window.scrollTop();
61 | session.windowWidth = $window.width();
62 | session.windowHeight = $window.height();
63 | }
64 |
65 | /**
66 | * Updates the window size info in the viewport dimensions cache.
67 | * @private
68 | */
69 | function trackResize() {
70 | session.windowWidth = $window.width();
71 | session.windowHeight = $window.height();
72 | }
73 |
74 | /**
75 | * Updates the scroll offset info in the viewport dimensions cache.
76 | * @private
77 | */
78 | function trackScroll() {
79 | var x = $window.scrollLeft(),
80 | y = $window.scrollTop();
81 | if (x !== session.scrollLeft) {
82 | session.currentX += x - session.scrollLeft;
83 | session.scrollLeft = x;
84 | }
85 | if (y !== session.scrollTop) {
86 | session.currentY += y - session.scrollTop;
87 | session.scrollTop = y;
88 | }
89 | }
90 |
91 | /**
92 | * Saves the current mouse coordinates to the session object.
93 | * @private
94 | * @param {jQuery.Event} event The mousemove event for the document.
95 | */
96 | function trackMouse(event) {
97 | session.currentX = event.pageX;
98 | session.currentY = event.pageY;
99 | }
100 |
101 | /**
102 | * Tests if the mouse is currently over the specified element.
103 | * @private
104 | * @param {jQuery} element The element to check for hover.
105 | * @return {boolean} True if the mouse is over the element, otherwise false.
106 | */
107 | function isMouseOver(element) {
108 | // use getBoundingClientRect() because jQuery's width() and height()
109 | // methods do not work with SVG elements
110 | // compute width/height because those properties do not exist on the object
111 | // returned by getBoundingClientRect() in older versions of IE
112 | var elementPosition = element.offset(),
113 | elementBox = element[0].getBoundingClientRect(),
114 | elementWidth = elementBox.right - elementBox.left,
115 | elementHeight = elementBox.bottom - elementBox.top;
116 |
117 | return session.currentX >= elementPosition.left &&
118 | session.currentX <= elementPosition.left + elementWidth &&
119 | session.currentY >= elementPosition.top &&
120 | session.currentY <= elementPosition.top + elementHeight;
121 | }
122 |
123 | /**
124 | * Fetches the tooltip content from the specified element's data attributes.
125 | * @private
126 | * @param {jQuery} element The element to get the tooltip content for.
127 | * @return {(string|jQuery|undefined)} The text/HTML string, jQuery object, or
128 | * undefined if there was no tooltip content for the element.
129 | */
130 | function getTooltipContent(element) {
131 | var tipText = element.data(DATA_POWERTIP),
132 | tipObject = element.data(DATA_POWERTIPJQ),
133 | tipTarget = element.data(DATA_POWERTIPTARGET),
134 | targetElement,
135 | content;
136 |
137 | if (tipText) {
138 | if (typeof tipText === 'function') {
139 | tipText = tipText.call(element[0]);
140 | }
141 | content = tipText;
142 | } else if (tipObject) {
143 | if (typeof tipObject === 'function') {
144 | tipObject = tipObject.call(element[0]);
145 | }
146 | if (tipObject.length > 0) {
147 | content = tipObject.clone(true, true);
148 | }
149 | } else if (tipTarget) {
150 | targetElement = $('#' + tipTarget);
151 | if (targetElement.length > 0) {
152 | content = targetElement.html();
153 | }
154 | }
155 |
156 | return content;
157 | }
158 |
159 | /**
160 | * Finds any viewport collisions that an element (the tooltip) would have if it
161 | * were absolutely positioned at the specified coordinates.
162 | * @private
163 | * @param {CSSCoordinates} coords Coordinates for the element.
164 | * @param {number} elementWidth Width of the element in pixels.
165 | * @param {number} elementHeight Height of the element in pixels.
166 | * @return {number} Value with the collision flags.
167 | */
168 | function getViewportCollisions(coords, elementWidth, elementHeight) {
169 | var viewportTop = session.scrollTop,
170 | viewportLeft = session.scrollLeft,
171 | viewportBottom = viewportTop + session.windowHeight,
172 | viewportRight = viewportLeft + session.windowWidth,
173 | collisions = Collision.none;
174 |
175 | if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) {
176 | collisions |= Collision.top;
177 | }
178 | if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) {
179 | collisions |= Collision.bottom;
180 | }
181 | if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) {
182 | collisions |= Collision.left;
183 | }
184 | if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) {
185 | collisions |= Collision.right;
186 | }
187 |
188 | return collisions;
189 | }
190 |
191 | /**
192 | * Counts the number of bits set on a flags value.
193 | * @param {number} value The flags value.
194 | * @return {number} The number of bits that have been set.
195 | */
196 | function countFlags(value) {
197 | var count = 0;
198 | while (value) {
199 | value &= value - 1;
200 | count++;
201 | }
202 | return count;
203 | }
204 |
--------------------------------------------------------------------------------
/src/wrapper.js:
--------------------------------------------------------------------------------
1 | /*!
2 | <%= pkg.title %> v<%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd") %>)
3 | <%= pkg.homepage %>
4 | Copyright (c) 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %> (<%= pkg.author.url %>).
5 | Released under <%= pkg.license %> license.
6 | https://github.com/stevenbenner/jquery-powertip/blob/master/<%= files.license %>
7 | */
8 | (function(root, factory) {
9 | // support loading the plugin via common patterns
10 | if (typeof define === 'function' && define.amd) {
11 | // load the plugin as an amd module
12 | define([ 'jquery' ], factory);
13 | } else if (typeof module === 'object' && module.exports) {
14 | // load the plugin as a commonjs module
15 | module.exports = factory(require('jquery'));
16 | } else {
17 | // load the plugin as a global
18 | factory(root.jQuery);
19 | }
20 | }(this, function($) {
21 | /* [POWERTIP CODE] */
22 | // return api for commonjs and amd environments
23 | return $.powerTip;
24 | }));
25 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 3
4 | },
5 | "env": {
6 | "browser": true,
7 | "commonjs": true,
8 | "jquery": true,
9 | "qunit": true,
10 | "node": false
11 | },
12 | "rules": {
13 | "no-prototype-builtins": "off",
14 | "no-var": "off",
15 | "prefer-template": "off"
16 | },
17 | "globals": {
18 | "DATA_DISPLAYCONTROLLER": false,
19 | "DATA_HASACTIVEHOVER": false,
20 | "DATA_POWERTIP": false,
21 | "DATA_POWERTIPJQ": false,
22 | "DATA_POWERTIPTARGET": false,
23 | "DATA_FORCEDOPEN": false,
24 | "session": true,
25 | "Collision": false,
26 |
27 | "CSSCoordinates": false,
28 | "DisplayController": false,
29 | "PlacementCalculator": false,
30 | "TooltipController": false,
31 |
32 | "isSvgElement": false,
33 | "isMouseEvent": false,
34 | "initTracking": false,
35 | "trackMouse": false,
36 | "isMouseOver": false,
37 | "getTooltipContent": false,
38 | "getViewportCollisions": false,
39 | "countFlags": false
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/amd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PowerTip AMD Test Suite
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/test/amd.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require.config({
4 | paths: {
5 | jquery: 'https://code.jquery.com/jquery-3.7.1',
6 | qunit: 'https://code.jquery.com/qunit/qunit-2.20.0',
7 | 'jquery.powertip': '../dist/jquery.powertip'
8 | }
9 | });
10 |
11 | require([ 'jquery', 'qunit', 'jquery.powertip' ], function($, QUnit, powerTip) {
12 | QUnit.start();
13 |
14 | QUnit.module('AMD');
15 |
16 | QUnit.test('powerTip is loaded and available via AMD', function(assert) {
17 | var element = $('');
18 | assert.strictEqual(typeof element.powerTip, 'function', 'powerTip is defined');
19 | });
20 |
21 | QUnit.test('expose API via jQuery', function(assert) {
22 | assert.strictEqual(typeof $.powerTip.show, 'function', 'show is defined');
23 | assert.strictEqual(typeof $.powerTip.reposition, 'function', 'reposition is defined');
24 | assert.strictEqual(typeof $.powerTip.hide, 'function', 'hide is defined');
25 | assert.strictEqual(typeof $.powerTip.toggle, 'function', 'toggle is defined');
26 | assert.strictEqual(typeof $.powerTip.destroy, 'function', 'destroy is defined');
27 | });
28 |
29 | QUnit.test('expose API via AMD parameter', function(assert) {
30 | assert.strictEqual(typeof powerTip.show, 'function', 'show is defined');
31 | assert.strictEqual(typeof powerTip.reposition, 'function', 'reposition is defined');
32 | assert.strictEqual(typeof powerTip.hide, 'function', 'hide is defined');
33 | assert.strictEqual(typeof powerTip.toggle, 'function', 'toggle is defined');
34 | assert.strictEqual(typeof powerTip.destroy, 'function', 'destroy is defined');
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/browserify.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PowerTip Browserify Test Suite
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/browserify.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | (function runTests() {
4 | var QUnit = require('qunit'),
5 | $ = require('jquery'),
6 | powerTip = require('./../dist/jquery.powertip.js');
7 |
8 | QUnit.module('Browserify');
9 |
10 | QUnit.test('powerTip is loaded into $.fn', function(assert) {
11 | var element = $('');
12 | assert.strictEqual(typeof element.powerTip, 'function', 'powerTip is defined');
13 | });
14 |
15 | QUnit.test('expose API via jQuery', function(assert) {
16 | assert.strictEqual(typeof $.powerTip.show, 'function', 'show is defined');
17 | assert.strictEqual(typeof $.powerTip.reposition, 'function', 'reposition is defined');
18 | assert.strictEqual(typeof $.powerTip.hide, 'function', 'hide is defined');
19 | assert.strictEqual(typeof $.powerTip.toggle, 'function', 'toggle is defined');
20 | assert.strictEqual(typeof $.powerTip.destroy, 'function', 'destroy is defined');
21 | });
22 |
23 | QUnit.test('expose API is returned from require()', function(assert) {
24 | assert.strictEqual(typeof powerTip.show, 'function', 'show is defined');
25 | assert.strictEqual(typeof powerTip.reposition, 'function', 'reposition is defined');
26 | assert.strictEqual(typeof powerTip.hide, 'function', 'hide is defined');
27 | assert.strictEqual(typeof powerTip.toggle, 'function', 'toggle is defined');
28 | assert.strictEqual(typeof powerTip.destroy, 'function', 'destroy is defined');
29 | });
30 | })();
31 |
--------------------------------------------------------------------------------
/test/edge-cases.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PowerTip Edge Case Tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
55 |
56 |
57 |
68 |
69 |
70 |
71 |
PowerTip Edge Case Tests
72 |
73 | Tooltip CSS Theme:
74 |
85 |
86 |
87 |
88 |
Open on load
89 |
The button below has a tooltip that will open when the document loads. The tooltip should be properly positioned.
90 |
91 |
92 |
93 |
Tab change
94 |
The button below has a tooltip that is set to follow the mouse. Focus the element and change to another browser tab, then change back to this tab. The tooltip should revert to a static placement when the browser fires the focus event.
95 |
96 |
97 |
98 |
Click toggle
99 |
The button below has a tooltip that will toggle when clicked.
100 |
101 |
102 |
103 |
Remote target
104 |
The link below has a tooltip that will open when the button is clicked. It should open normally.
The button below will disable itself when used with the mouse or keyboard. The tooltip should close.
111 |
112 |
113 |
114 |
Auto-disabling button
115 |
The button below will disable itself 2 seconds after you hover or focus on it. The tooltip should close.
116 |
117 |
118 |
119 |
Long delay
120 |
The two buttons below have tooltips with long delays. Mousing from one to the other should open tooltips normally.
121 |
122 |
123 |
124 |
125 |
Manual and interactive
126 |
The buttons below have tooltips, one with manual enabled, the other with mouseOnToPopup enabled. The manual tooltip should not close when you mouse off of the tooltip element.
127 |
128 |
129 |
130 |
131 |
Huge Text
132 |
The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.
The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.