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 |
148 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/test/unit/utility.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | $(function() {
4 | QUnit.module('Utility Functions');
5 |
6 | QUnit.test('isSvgElement', function(assert) {
7 | var div = $(''),
8 | rect = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect'));
9 |
10 | assert.strictEqual(isSvgElement(rect), true, 'rect is an SVG element');
11 | assert.strictEqual(isSvgElement(div), false, 'div is not an SVG element');
12 | });
13 |
14 | QUnit.test('isMouseEvent', function(assert) {
15 | var mouseEnterEvent = new $.Event('mouseenter', { pageX: 2, pageY: 3 }),
16 | mouseEventWithoutData = new $.Event('mouseenter'),
17 | focusEvent = new $.Event('focus', { pageX: 0, pageY: 0 }),
18 | emptyEvent = new $.Event();
19 |
20 | assert.strictEqual(isMouseEvent(mouseEnterEvent), true, 'The mouseenter event with coordinates is a mouse event');
21 | assert.strictEqual(isMouseEvent(mouseEventWithoutData), false, 'The mouseenter event without coordinates is not a mouse event');
22 | assert.strictEqual(isMouseEvent(focusEvent), false, 'The focus event is not a mouse event, even with mouse coordinates');
23 | assert.strictEqual(isMouseEvent(emptyEvent), false, 'Empty event is not a mouse event');
24 | assert.strictEqual(isMouseEvent(), false, 'Undefined argument is not a mouse event');
25 | });
26 |
27 | QUnit.test('initTracking', function(assert) {
28 | session.currentX = 1;
29 | session.currentY = 1;
30 |
31 | initTracking();
32 |
33 | $(document).trigger(new $.Event('mousemove', { pageX: 2, pageY: 3 }));
34 |
35 | assert.strictEqual(session.currentX, 2, 'currentX updated with correct value on mousemove');
36 | assert.strictEqual(session.currentY, 3, 'currentY updated with correct value on mousemove');
37 | });
38 |
39 | QUnit.test('trackMouse', function(assert) {
40 | session.currentX = 1;
41 | session.currentY = 1;
42 |
43 | trackMouse(new $.Event('mousemove', { pageX: 4, pageY: 5 }));
44 |
45 | assert.strictEqual(session.currentX, 4, 'currentX updated with correct value on mousemove');
46 | assert.strictEqual(session.currentY, 5, 'currentY updated with correct value on mousemove');
47 | });
48 |
49 | QUnit.test('isMouseOver', function(assert) {
50 | var div = $('')
51 | .css({
52 | position: 'absolute',
53 | top: '10px',
54 | left: '30px',
55 | width: '50px',
56 | height: '20px'
57 | })
58 | .appendTo('body');
59 |
60 | session.currentX = 30;
61 | session.currentY = 10;
62 | assert.ok(isMouseOver(div), 'top/left hover detected');
63 |
64 | session.currentX = 55;
65 | session.currentY = 15;
66 | assert.ok(isMouseOver(div), 'center hover detected');
67 |
68 | session.currentX = 80;
69 | session.currentY = 30;
70 | assert.ok(isMouseOver(div), 'bottom/right hover detected');
71 |
72 | session.currentX = 9;
73 | session.currentY = 29;
74 | assert.notOk(isMouseOver(div), 'no hover detected');
75 |
76 | session.currentX = 81;
77 | session.currentY = 31;
78 | assert.notOk(isMouseOver(div), 'no hover detected');
79 |
80 | div.remove();
81 | });
82 |
83 | QUnit.test('getTooltipContent', function(assert) {
84 | var powertip = $('').data(DATA_POWERTIP, 'powertip'),
85 | powertipFunc = $('').data(DATA_POWERTIP, function() {
86 | return 'powertipFunc';
87 | }),
88 | jqObject = $('
powertipjq
'),
89 | powertipjq = $('').data(DATA_POWERTIPJQ, jqObject),
90 | powertipjqFunc = $('').data(DATA_POWERTIPJQ, function() {
91 | return jqObject;
92 | }),
93 | powertiptarget = $('').data(DATA_POWERTIPTARGET, 'tiptargettest'),
94 | targetDiv = $('').attr('id', 'tiptargettest').text('tiptargettest');
95 |
96 | // add powertiptarget to body
97 | targetDiv.appendTo($('body'));
98 |
99 | assert.strictEqual(getTooltipContent(powertip), 'powertip', 'data-powertip text parsed');
100 | assert.strictEqual(getTooltipContent(powertipFunc), 'powertipFunc', 'data-powertip function parsed');
101 | assert.strictEqual(getTooltipContent(powertipjq).find('b').text(), 'powertipjq', 'data-powertipjq object parsed');
102 | assert.strictEqual(getTooltipContent(powertipjqFunc).find('b').text(), 'powertipjq', 'data-powertipjq function parsed');
103 | assert.strictEqual(getTooltipContent(powertiptarget), 'tiptargettest', 'data-powertiptarget reference parsed');
104 |
105 | // remove target test div
106 | targetDiv.remove();
107 | });
108 |
109 | QUnit.test('countFlags', function(assert) {
110 | var zero = Collision.none,
111 | one = Collision.top,
112 | two = Collision.top | Collision.left,
113 | three = Collision.top | Collision.left | Collision.right,
114 | four = Collision.top | Collision.left | Collision.right | Collision.bottom;
115 |
116 | assert.strictEqual(countFlags(zero), 0, 'Found zero flags.');
117 | assert.strictEqual(countFlags(one), 1, 'Found one flag.');
118 | assert.strictEqual(countFlags(two), 2, 'Found two flags.');
119 | assert.strictEqual(countFlags(three), 3, 'Found three flags.');
120 | assert.strictEqual(countFlags(four), 4, 'Found four flags.');
121 | });
122 |
123 | QUnit.test('getViewportCollisions', function(assert) {
124 | var windowWidth = $(window).width(),
125 | windowHeight = $(window).height(),
126 | none, right, bottom, bottomRight, top, left, topLeft;
127 |
128 | function doTests() {
129 | assert.strictEqual(none, Collision.none, 'no collisions detected');
130 | assert.strictEqual(right & Collision.right, Collision.right, 'right collision detected for right test');
131 | assert.strictEqual(countFlags(right), 1, 'exactly one collision detected for right test');
132 | assert.strictEqual(bottom & Collision.bottom, Collision.bottom, 'bottom collision detected for bottom test');
133 | assert.strictEqual(countFlags(bottom), 1, 'exactly one collision detected for bottom test');
134 | assert.strictEqual(bottomRight & Collision.bottom, Collision.bottom, 'bottom collision detected for bottom-right test');
135 | assert.strictEqual(bottomRight & Collision.right, Collision.right, 'right collision detected for bottom-right test');
136 | assert.strictEqual(countFlags(bottomRight), 2, 'exactly two collisions detected for bottom-right test');
137 | assert.strictEqual(top & Collision.top, Collision.top, 'top collision detected for top test');
138 | assert.strictEqual(countFlags(top), 1, 'exactly one collision detected for top test');
139 | assert.strictEqual(left & Collision.left, Collision.left, 'left collision detected for left test');
140 | assert.strictEqual(countFlags(left), 1, 'exactly one collision detected for left test');
141 | assert.strictEqual(topLeft & Collision.top, Collision.top, 'top collision detected for top-left test');
142 | assert.strictEqual(topLeft & Collision.left, Collision.left, 'left collision detected for top-left test');
143 | assert.strictEqual(countFlags(topLeft), 2, 'exactly two collisions detected for top-left test');
144 | }
145 |
146 | // need to make sure initTracking() has been invoked to populate the
147 | // viewport dimensions cache
148 | initTracking();
149 |
150 | // top/left placement
151 | none = getViewportCollisions({ top: 0, left: 0 }, 200, 100);
152 | right = getViewportCollisions({ top: 0, left: windowWidth - 199 }, 200, 100);
153 | bottom = getViewportCollisions({ top: windowHeight - 99, left: 0 }, 200, 100);
154 | bottomRight = getViewportCollisions({ top: windowHeight - 99, left: windowWidth - 199 }, 200, 100);
155 | top = getViewportCollisions({ top: -1, left: 0 }, 200, 100);
156 | left = getViewportCollisions({ top: 0, left: -1 }, 200, 100);
157 | topLeft = getViewportCollisions({ top: -1, left: -1 }, 200, 100);
158 |
159 | doTests();
160 |
161 | // bottom/right placement
162 | none = getViewportCollisions({ bottom: 0, right: 0 }, 200, 100);
163 | right = getViewportCollisions({ bottom: 0, right: -1 }, 200, 100);
164 | bottom = getViewportCollisions({ bottom: -1, right: 0 }, 200, 100);
165 | bottomRight = getViewportCollisions({ bottom: -1, right: -1 }, 200, 100);
166 | top = getViewportCollisions({ bottom: windowHeight - 99, right: 0 }, 200, 100);
167 | left = getViewportCollisions({ bottom: 0, right: windowWidth - 199 }, 200, 100);
168 | topLeft = getViewportCollisions({ bottom: windowHeight - 99, right: windowWidth - 199 }, 200, 100);
169 |
170 | doTests();
171 | });
172 | });
173 |
--------------------------------------------------------------------------------
/CHANGELOG.yml:
--------------------------------------------------------------------------------
1 | v1.4.0:
2 | date: TBA
3 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.3.2...master
4 | description: TBA
5 | changes:
6 | - section: Features & Improvements
7 | changes:
8 | - Added support for jQuery version 4.
9 | v1.3.2:
10 | date: 2022-03-06
11 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.3.1...v1.3.2
12 | description: Maintenance release with a couple of bug fixes
13 | changes:
14 | - section: Bug Fixes
15 | changes:
16 | - Fixed mouse close event being set when mouseOnToPopup is enabled but closeEvents option doesn't include mouseleave.
17 | - Fixed performance regression when setting up a very large number of tooltips with repeated powerTip() calls.
18 | v1.3.1:
19 | date: 2018-04-15
20 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.3.0...v1.3.1
21 | description: Minor bug fixing release with a couple functionality improvements
22 | changes:
23 | - section: Features & Improvements
24 | changes:
25 | - Mouse-follow tooltips will now fall back to static placement when opened via a non-mouse event.
26 | - CSS border color for tooltip arrows are now set to inherit, making it easier to override colors.
27 | - section: Bug Fixes
28 | changes:
29 | - Apply popupClass before tooltip positioning.
30 | - Fixed non-functional tooltips on even number repeated powerTip() calls on the same element(s).
31 | - Fixed issue with non-mouse events tracking invalid coordinates on Firefox with jQuery 3.
32 | - Fixed destroy() API method not cleaning up a currently open tooltip.
33 | - Fixed mouse follow tooltip placement when corner trapped on a horizontally scrolled page.
34 | - Fixed CSS arrows not rendering on Internet Explorer 8.
35 | v1.3.0:
36 | date: 2017-01-15
37 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.2.0...v1.3.0
38 | description: API enhancements, new options, and several bug fixes
39 | changes:
40 | - section: Features & Improvements
41 | changes:
42 | - Added openEvents and closeEvents options.
43 | - Added popupClass option for custom tooltip classes.
44 | - Added CommonJS/Browserify support.
45 | - section: API
46 | changes:
47 | - The destroy() API method elements argument is now optional. When omitted all instances will be destroyed.
48 | - Added toggle() method to the API.
49 | - section: Bug Fixes
50 | changes:
51 | - The closeDelay timer is now correctly shared between all tooltips.
52 | - Browser dimensions cache is now initialized as soon as PowerTip loads.
53 | - Fixed queuing issue when the API hide() method is called immediately after show().
54 | - Fixed error when an element with an open tooltip is deleted.
55 | - The mouseOnToPopup option will now be ignored (forced false) when the manual option is enabled.
56 | - Fixed possible repeated event hooks when mouseOnToPopup is enabled.
57 | - Fixed mouseOnToPopup events being applied to other instances where manual is enabled.
58 | - Fixed old placement classes remaining on tip element when using reposition API and smart placement.
59 | - section: Miscellaneous
60 | changes:
61 | - Fixed script url in the examples HTML file incuded in the release.
62 | - Documented the caching quirks for changing tooltip content.
63 | - PowerTip is now officially available on npm (as "jquery-powertip").
64 | v1.2.0:
65 | date: 2013-04-03
66 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.1.0...v1.2.0
67 | description: Major release with lots of improvements and a significant code rewrite
68 | changes:
69 | - section: Features & Improvements
70 | changes:
71 | - Mouse-follow tooltips will now flip out of the way if they become trapped in the bottom-right corner.
72 | - Escape key will now close tooltip for selected element.
73 | - Added support for elastic tooltips.
74 | - Added manual option to disable the built-in event listeners.
75 | - Added nw-alt, ne-alt, sw-alt, and se-alt placement options.
76 | - Added support for SVG elements.
77 | - PowerTip will now use right position for right aligned tooltips, and bottom position for nothern tooltips.
78 | - Data attributes powertip and powertipjq now accept a function.
79 | - powerTip() will now overwrite any previous powerTip() calls on an element.
80 | - Added support for AMD loading of PowerTip.
81 | - section: API
82 | changes:
83 | - Added show() and hide() methods to the API.
84 | - Added reposition() method to the API.
85 | - Added destroy() method to the API.
86 | - You can now pass API method names as strings to the powerTip() function.
87 | - showTip and hideTip API methods are now deprecated in favor of the new show and hide API methods (but they will continue to work until 2.0).
88 | - section: CSS
89 | changes:
90 | - Added 8 new tooltip CSS themes.
91 | - Changed default z-index in CSS themes to int max.
92 | - Added RGB color fallbacks for tooltip arrows (meaning arrows arrows now work in IE8).
93 | - section: Bug Fixes
94 | changes:
95 | - Fixed bug that would cause the CSS position to be updated even when the tooltip is closed.
96 | - Fixed issue that could cause tooltips to close prematurely during the closeDelay period.
97 | - section: Miscellaneous
98 | changes:
99 | - Project now has a fully automated build process.
100 | - Added a complete test suite and hooked up Travis CI.
101 | - Significant rewrite of the code.
102 | v1.1.0:
103 | date: 2012-08-08
104 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.0.4...v1.1.0
105 | description: Major release with several significant improvements
106 | changes:
107 | - section: Features & Improvements
108 | changes:
109 | - Added smart placement feature.
110 | - Added custom events.
111 | - Added support for keyboard navigation.
112 | - Added support for jsFiddle.
113 | - section: API
114 | changes:
115 | - Added API with showTip() and closeTip() methods.
116 | - section: Bug Fixes
117 | changes:
118 | - Fixed mouse-follow constraint
119 | v1.0.4:
120 | date: 2012-07-31
121 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.0.3...v1.0.4
122 | description: Minor release to address issues with IE8
123 | changes:
124 | - section: CSS
125 | changes:
126 | - Added RBG background color fallback for browsers that do not support RGBA.
127 | - section: Bug Fixes
128 | changes:
129 | - Fixed positioning problems with Internet Explorer 8.
130 | v1.0.3:
131 | date: 2012-07-31
132 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.0.2...v1.0.3
133 | description: Minor release to address a couple issues
134 | changes:
135 | - section: Features & Improvements
136 | changes:
137 | - Added mouse position tracking to scroll events.
138 | - section: Bug Fixes
139 | changes:
140 | - Fixed rare issue that would make fixed placement tooltips follow the mouse.
141 | v1.0.2:
142 | date: 2012-07-26
143 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.0.1...v1.0.2
144 | description: Minor release to make a couple small improvements and bug fixes
145 | changes:
146 | - section: Features & Improvements
147 | changes:
148 | - Added placement class to tooltip element.
149 | - Added CSS arrows to tooltips.
150 | - Add nw, ne, sw, and sw placement options.
151 | - Changed default closeDelay to 100ms.
152 | - Changed default fadeOutTime to 100ms.
153 | - Changed default placement to north.
154 | - section: Bug Fixes
155 | changes:
156 | - Fixed error when there is no tooltip content.
157 | - Fixed rare error when moused entered a tooltip during its fadeOut cycle.
158 | v1.0.1:
159 | date: 2012-07-11
160 | diff: https://github.com/stevenbenner/jquery-powertip/compare/v1.0.0...v1.0.1
161 | description: Minor release to fix a tip tracking issue
162 | changes:
163 | - section: Bug Fixes
164 | changes:
165 | - Fixed rare issue that caused tooltips to become desynced.
166 | v1.0.0:
167 | date: 2012-07-01
168 | description: Initial public release
169 | changes:
170 | - section: Initial public release
171 |
--------------------------------------------------------------------------------
/test/unit/core.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | $(function() {
4 | QUnit.module('PowerTip Core', {
5 | afterEach: function() {
6 | $.powerTip.destroy();
7 | }
8 | });
9 |
10 | QUnit.test('powerTip defined', function(assert) {
11 | var element = $('');
12 | assert.strictEqual(typeof element.powerTip, 'function', 'powerTip is defined');
13 | });
14 |
15 | QUnit.test('expose default settings', function(assert) {
16 | assert.ok($.fn.powerTip.defaults, 'defaults is defined');
17 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('fadeInTime'), 'fadeInTime exists');
18 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('fadeOutTime'), 'fadeOutTime exists');
19 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('followMouse'), 'followMouse exists');
20 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('popupId'), 'popupId exists');
21 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('intentSensitivity'), 'intentSensitivity exists');
22 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('intentPollInterval'), 'intentPollInterval exists');
23 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('closeDelay'), 'closeDelay exists');
24 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('placement'), 'placement exists');
25 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('smartPlacement'), 'smartPlacement exists');
26 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('offset'), 'offset exists');
27 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('mouseOnToPopup'), 'mouseOnToPopup exists');
28 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('manual'), 'manual exists');
29 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('openEvents'), 'openEvents exists');
30 | assert.ok($.fn.powerTip.defaults.hasOwnProperty('closeEvents'), 'closeEvents exists');
31 | });
32 |
33 | QUnit.test('expose smart placement lists', function(assert) {
34 | assert.ok($.fn.powerTip.smartPlacementLists, 'smartPlacementLists is defined');
35 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('n'), 'n exists');
36 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('e'), 'e exists');
37 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('s'), 's exists');
38 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('w'), 'w exists');
39 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('ne'), 'ne exists');
40 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('nw'), 'nw exists');
41 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('se'), 'se exists');
42 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('sw'), 'sw exists');
43 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('ne-alt'), 'ne-alt exists');
44 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('nw-alt'), 'nw-alt exists');
45 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('se-alt'), 'se-alt exists');
46 | assert.ok($.fn.powerTip.smartPlacementLists.hasOwnProperty('sw-alt'), 'sw-alt exists');
47 | });
48 |
49 | QUnit.test('powerTip', function(assert) {
50 | var div = $(''),
51 | empty = $('#thisDoesntExist'),
52 | element = $('').powerTip();
53 |
54 | assert.deepEqual(div.powerTip(), div, 'original jQuery object returned for matched selector');
55 | assert.deepEqual(empty.powerTip(), empty, 'original jQuery object returned for empty selector');
56 | assert.deepEqual(div.powerTip('show'), div, 'original jQuery object returned for show');
57 | assert.deepEqual(div.powerTip('hide', true), div, 'original jQuery object returned for hide');
58 | assert.deepEqual(div.powerTip('toggle'), div, 'original jQuery object returned for toggle');
59 | assert.deepEqual(div.powerTip('resetPosition'), div, 'original jQuery object returned for resetPosition');
60 | assert.deepEqual(div.powerTip('destroy'), div, 'original jQuery object returned for destroy');
61 | assert.notOk(element.attr('title'), 'title attribute was removed');
62 | assert.ok(element.data(DATA_DISPLAYCONTROLLER), 'new DisplayController created and added to data');
63 | });
64 |
65 | QUnit.test('powerTip hooks events', function(assert) {
66 | var openEvents = {
67 | mouseenter: { pageX: 14, pageY: 14 },
68 | focus: null,
69 | customOpenEvent: null
70 | },
71 | closeEvents = {
72 | mouseleave: { pageX: 14, pageY: 14 },
73 | blur: null,
74 | customCloseEvent: null
75 | },
76 | element = $('TEXT').powerTip({
77 | openEvents: Object.keys(openEvents),
78 | closeEvents: Object.keys(closeEvents)
79 | }),
80 | showTriggered = false,
81 | hideTriggered = false;
82 |
83 | element.data(
84 | DATA_DISPLAYCONTROLLER,
85 | new MockDisplayController(
86 | function() {
87 | showTriggered = true;
88 | },
89 | function() {
90 | hideTriggered = true;
91 | }
92 | )
93 | );
94 |
95 | // jquery 1.9 will not trigger a focus event on an element that cannot
96 | // be focused, so we have to append the test element to the document
97 | // before the focus test will work
98 | $('body').prepend(element);
99 |
100 | // test open events
101 | $.each(openEvents, function(eventName, eventData) {
102 | showTriggered = false;
103 | element.triggerHandler(new $.Event(eventName, eventData));
104 | assert.strictEqual(showTriggered, true, eventName + ' event calls DisplayController.show');
105 | });
106 |
107 | // test close events
108 | $.each(closeEvents, function(eventName, eventData) {
109 | hideTriggered = false;
110 | element.triggerHandler(new $.Event(eventName, eventData));
111 | assert.strictEqual(hideTriggered, true, eventName + ' event calls DisplayController.hide');
112 | });
113 |
114 | // test escape key
115 | hideTriggered = false;
116 | element.trigger(new $.Event('keydown', { keyCode: 27 }));
117 | assert.strictEqual(hideTriggered, true, 'keydown event for key code 27 calls DisplayController.hide');
118 |
119 | // cleanup test element
120 | element.detach();
121 | });
122 |
123 | QUnit.test('expose API', function(assert) {
124 | assert.strictEqual(typeof $.powerTip.show, 'function', 'show is defined');
125 | assert.strictEqual(typeof $.powerTip.reposition, 'function', 'reposition is defined');
126 | assert.strictEqual(typeof $.powerTip.hide, 'function', 'hide is defined');
127 | assert.strictEqual(typeof $.powerTip.toggle, 'function', 'toggle is defined');
128 | assert.strictEqual(typeof $.powerTip.destroy, 'function', 'destroy is defined');
129 | // deprecated
130 | assert.strictEqual(typeof $.powerTip.showTip, 'function', 'showTip is defined');
131 | assert.strictEqual(typeof $.powerTip.closeTip, 'function', 'closeTip is defined');
132 | });
133 |
134 | QUnit.test('API show method should call DisplayController.show', function(assert) {
135 | var showCalled = false,
136 | element = $('')
137 | .data(DATA_DISPLAYCONTROLLER, new MockDisplayController(
138 | function() {
139 | showCalled = true;
140 | }
141 | ));
142 |
143 | $.powerTip.show(element);
144 |
145 | assert.ok(showCalled, 'show method was called');
146 | });
147 |
148 | QUnit.test('API reposition method should call DisplayController.resetPosition', function(assert) {
149 | var resetCalled = false,
150 | element = $('')
151 | .data(DATA_DISPLAYCONTROLLER, new MockDisplayController(
152 | null,
153 | null,
154 | null,
155 | function() {
156 | resetCalled = true;
157 | }
158 | ));
159 |
160 | $.powerTip.reposition(element);
161 |
162 | assert.ok(resetCalled, 'reposition method was called');
163 | });
164 |
165 | QUnit.test('API hide method should call DisplayController.hide', function(assert) {
166 | var hideCalled = false,
167 | element = $('')
168 | .data(DATA_DISPLAYCONTROLLER, new MockDisplayController(
169 | null,
170 | function() {
171 | hideCalled = true;
172 | }
173 | ));
174 |
175 | $.powerTip.hide(element);
176 |
177 | assert.ok(hideCalled, 'hide method was called');
178 | });
179 |
180 | QUnit.test('API toggle method should call DisplayController.show to open and DisplayController.hide to close', function(assert) {
181 | var showCalled = false,
182 | hideCalled = false,
183 | element = $('')
184 | .data(DATA_DISPLAYCONTROLLER, new MockDisplayController(
185 | function() {
186 | showCalled = true;
187 | // toggle checks activeHover to determine action
188 | session.activeHover = element;
189 | },
190 | function() {
191 | hideCalled = true;
192 | }
193 | ));
194 |
195 | $.powerTip.toggle(element); // simulate show
196 | $.powerTip.toggle(element); // simulate hide
197 |
198 | assert.ok(showCalled, 'show method was called');
199 | assert.ok(hideCalled, 'hide method was called');
200 |
201 | // reset activeHover
202 | session.activeHover = null;
203 | });
204 |
205 | QUnit.test('API destroy method rolls back PowerTip changes', function(assert) {
206 | var element = $('').powerTip(),
207 | elementDataAttr = $('').powerTip(),
208 | showTriggered = false,
209 | hideTriggered = false;
210 |
211 | element.data(
212 | DATA_DISPLAYCONTROLLER,
213 | new MockDisplayController(
214 | function() {
215 | showTriggered = true;
216 | },
217 | function() {
218 | hideTriggered = true;
219 | }
220 | )
221 | );
222 |
223 | element.powerTip('destroy');
224 | elementDataAttr.powerTip('destroy');
225 |
226 | // attributes
227 | assert.strictEqual(element.attr('title'), 'This is the tooltip text', 'destory method rolled back the title attribute');
228 | assert.notOk(element.data(DATA_POWERTIP), 'destroy method removed powertip data attribute');
229 | assert.strictEqual(elementDataAttr.data(DATA_POWERTIP), 'This is the tooltip text', 'destroy method did not remove manually set powertip data attribute');
230 |
231 | // events
232 | element.trigger(new $.Event('mouseenter', { pageX: 10, pageY: 10 }));
233 | assert.notOk(showTriggered, 'mouseenter event was unhooked after destroy');
234 | showTriggered = false;
235 |
236 | element.trigger('mouseleave');
237 | assert.notOk(hideTriggered, 'mouseleave event was unhooked after destroy');
238 | hideTriggered = false;
239 |
240 | element.trigger('focus');
241 | assert.notOk(showTriggered, 'focus event was unhooked after destroy');
242 | showTriggered = false;
243 |
244 | element.trigger('blur');
245 | assert.notOk(hideTriggered, 'blur event was unhooked after destroy');
246 | hideTriggered = false;
247 |
248 | element.trigger(new $.Event('keydown', { keyCode: 27 }));
249 | assert.notOk(hideTriggered, 'keydown event was unhooked after destroy');
250 | hideTriggered = false;
251 | });
252 |
253 | QUnit.test('API destroy method with no arguments rolls back all PowerTip changes', function(assert) {
254 | // run PowerTip
255 | $('').powerTip();
256 |
257 | // destroy everything
258 | $.powerTip.destroy();
259 |
260 | // tooltip element
261 | assert.strictEqual($('#' + $.fn.powerTip.defaults.popupId).length, 0, 'tooltip element removed');
262 |
263 | // document event (mouse tracking)
264 | session.currentX = 1;
265 | $(document).trigger(new $.Event('mousemove', { pageX: 2, pageY: 3 }));
266 | assert.strictEqual(session.currentX, 1, 'document event removed');
267 | });
268 |
269 | QUnit.test('API destroy method with no arguments destroys multiple PowerTip instances', function(assert) {
270 | // run PowerTip
271 | $('').powerTip();
272 | $('').powerTip();
273 | $('').powerTip();
274 |
275 | // destroy everything
276 | $.powerTip.destroy();
277 |
278 | // tooltip element
279 | assert.strictEqual($('#' + $.fn.powerTip.defaults.popupId).length, 0, 'tooltip element removed');
280 |
281 | // document event (mouse tracking)
282 | session.currentX = 1;
283 | $(document).trigger(new $.Event('mousemove', { pageX: 2, pageY: 3 }));
284 | assert.strictEqual(session.currentX, 1, 'document event removed');
285 | });
286 |
287 | QUnit.test('API destroy method with no arguments rolls back removed elements', function(assert) {
288 | var element = $('');
289 | // run PowerTip
290 | element.powerTip();
291 |
292 | // remove element
293 | element.remove();
294 |
295 | // destroy everything
296 | $.powerTip.destroy();
297 |
298 | // tooltip element
299 | assert.strictEqual($('#' + $.fn.powerTip.defaults.popupId).length, 0, 'tooltip element removed');
300 |
301 | // document event (mouse tracking)
302 | session.currentX = 1;
303 | $(document).trigger(new $.Event('mousemove', { pageX: 2, pageY: 3 }));
304 | assert.strictEqual(session.currentX, 1, 'document event removed');
305 | });
306 |
307 | QUnit.test('API destroy hides a tooltip that is currently open', function(assert) {
308 | var done = assert.async(),
309 | element = $('').powerTip();
310 |
311 | element.on('powerTipOpen', function() {
312 | // destroy the tooltip
313 | $.powerTip.destroy(element);
314 |
315 | assert.notOk(session.isTipOpen, 'session.isTipOpen is false');
316 | assert.notOk(session.desyncTimeout, 'session.desyncTimeout is not active');
317 |
318 | done();
319 | });
320 |
321 | // open the tooltip
322 | $.powerTip.show(element);
323 | });
324 |
325 | QUnit.test('API destroy method with no arguments succeeds when there are no bound elements', function(assert) {
326 | // destroy everything, or in this case, nothing
327 | $.powerTip.destroy();
328 |
329 | assert.ok(true, 'no error');
330 | });
331 |
332 | function MockDisplayController(show, hide, cancel, resetPosition) {
333 | this.show = show || $.noop;
334 | this.hide = hide || $.noop;
335 | this.cancel = cancel || $.noop;
336 | this.resetPosition = resetPosition || $.noop;
337 | }
338 | });
339 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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.
The following glyphs are SVG elements. Tooltips should be placed correctly on all SVG elements.
169 |
183 |
184 |
185 |
Complex SVG Elements
186 |
The following shapes are created using SVG. Tooltips should be placed correctly on all SVG elements.
187 |
203 |
204 |
205 |
Rotated SVG Elements
206 |
The following SVG shapes have been rotated. Tooltips should be placed correctly on all SVG elements.
207 |
218 |
219 |
220 |
Trapped mouse following tooltip
221 |
This box has a mouse following tooltip.
222 |
Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.
223 |
224 |
225 |
226 |
227 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 | ## Overview
2 |
3 | ### Unique features
4 |
5 | * **Checks for hover intent**
6 |
7 | Testing for hover intent makes it so that tooltips don't open the moment your mouse happens to cross an element with a tooltip. Users must hover over the element for a moment before the tooltip will open. This provides a much smoother user experience.
8 |
9 | * **Tooltip queuing**
10 |
11 | The tooltip queue makes it a fundamental rule of the system that there will only ever be one tooltip visible on the screen. When the user moves their cursor from one element with a tooltip to another element with a tooltip, the previous tooltip will close gracefully before the new tooltip opens.
12 |
13 | ### Features
14 |
15 | * Straightforward implementation
16 | * Simple configuration
17 | * Supports static tooltips as well as tooltips that follow the mouse
18 | * Ability to let users mouse on to the tooltips and interact with their content
19 | * Mouse follow tooltips are constrained to the browser viewport
20 | * Easy customization
21 | * Works with keyboard navigation
22 | * Smooth fade-ins and fade-outs
23 | * Smart placement that (when enabled) will try to keep tooltips inside of the viewport
24 | * Multiple instances
25 | * Works on any type of element
26 | * Supports complex content (markup with behavior & events)
27 | * Supports custom open and close event handlers
28 |
29 | ### Requirements
30 |
31 | * jQuery 1.7 or later
32 |
33 | **Important note:** The `` 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 |
--------------------------------------------------------------------------------