├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.yml ├── Gruntfile.js ├── LICENSE.txt ├── README.md ├── css ├── jquery.powertip-blue.css ├── jquery.powertip-dark.css ├── jquery.powertip-green.css ├── jquery.powertip-light.css ├── jquery.powertip-orange.css ├── jquery.powertip-purple.css ├── jquery.powertip-red.css ├── jquery.powertip-yellow.css └── jquery.powertip.css ├── doc ├── README.md ├── gh-pages.template.md └── release-process.md ├── examples └── examples.html ├── package.json ├── src ├── .eslintrc.json ├── core.js ├── csscoordinates.js ├── displaycontroller.js ├── placementcalculator.js ├── tooltipcontroller.js ├── utility.js └── wrapper.js └── test ├── .eslintrc.json ├── amd.html ├── amd.js ├── browserify.html ├── browserify.js ├── edge-cases.html ├── index.html ├── tests-edge.js └── unit ├── core.js ├── csscoordinates.js ├── displaycontroller.js ├── placementcalculator.js ├── tooltipcontroller.js └── utility.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "@stevenbenner/eslint-config" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 6 9 | }, 10 | "env": { 11 | "node": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | test: 8 | name: Node ${{matrix.node}} on ${{matrix.os}} 9 | 10 | strategy: 11 | matrix: 12 | os: [ ubuntu-latest ] 13 | node: [ 20.x ] 14 | 15 | runs-on: ${{matrix.os}} 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js ${{matrix.node}} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{matrix.node}} 25 | 26 | - name: Install npm dependencies 27 | run: npm install 28 | 29 | - name: Test 30 | run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | npm-debug.log 4 | package-lock.json 5 | *.tgz 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /css 3 | /doc 4 | /examples 5 | /src 6 | /test 7 | .editorconfig 8 | .eslintrc.json 9 | .gitattributes 10 | .npmignore 11 | Gruntfile.js 12 | *.tgz 13 | *.zip 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip Grunt Config 3 | */ 4 | 5 | 'use strict'; 6 | 7 | module.exports = function gruntConfig(grunt) { 8 | // include grunt enhancements 9 | require('time-grunt')(grunt); 10 | require('jit-grunt')(grunt); 11 | 12 | // configure grunt 13 | grunt.initConfig({ 14 | pkg: grunt.file.readJSON('package.json'), 15 | paths: { 16 | build: 'dist', 17 | temp: '<%= paths.build %>/temp' 18 | }, 19 | files: { 20 | cat: 'jquery.powertip.js', 21 | min: 'jquery.powertip.min.js', 22 | zip: 'jquery.powertip-<%= pkg.version %>.zip', 23 | license: 'LICENSE.txt', 24 | changelog: 'CHANGELOG.yml' 25 | }, 26 | clean: { 27 | dist: [ '<%= paths.build %>' ], 28 | temp: [ '<%= paths.temp %>' ] 29 | }, 30 | eslint: { 31 | grunt: { 32 | src: [ 'Gruntfile.js' ] 33 | }, 34 | tests: { 35 | src: [ 'test/**/*.js' ] 36 | }, 37 | js: { 38 | options: { 39 | overrideConfigFile: 'src/.eslintrc.json' 40 | }, 41 | src: [ '<%= paths.build %>/<%= files.cat %>' ] 42 | } 43 | }, 44 | jsonlint: { 45 | project: { 46 | src: [ 47 | 'package.json', 48 | '.eslintrc.json', 49 | '{src,test}/.eslintrc.json' 50 | ] 51 | } 52 | }, 53 | concat: { 54 | options: { 55 | stripBanners: true 56 | }, 57 | core: { 58 | src: [ 59 | 'src/core.js', 60 | 'src/csscoordinates.js', 61 | 'src/displaycontroller.js', 62 | 'src/placementcalculator.js', 63 | 'src/tooltipcontroller.js', 64 | 'src/utility.js' 65 | ], 66 | dest: '<%= paths.temp %>/core.js', 67 | nonull: true 68 | } 69 | }, 70 | indent: { 71 | js: { 72 | src: [ '<%= concat.core.dest %>' ], 73 | dest: '<%= concat.core.dest %>', 74 | options: { 75 | change: 1 76 | } 77 | } 78 | }, 79 | browserify: { 80 | test: { 81 | src: [ 'test/browserify.js' ], 82 | dest: '<%= paths.temp %>/bundle.js' 83 | } 84 | }, 85 | qunit: { 86 | tests: [ 87 | 'test/index.html', 88 | 'test/amd.html' 89 | ], 90 | browserify: [ '<%= copy.browserify.dest %>' ], 91 | options: { 92 | puppeteer: { 93 | args: [ '--no-sandbox' ] 94 | } 95 | } 96 | }, 97 | uglify: { 98 | dist: { 99 | src: [ '<%= paths.build %>/<%= files.cat %>' ], 100 | dest: '<%= paths.build %>/<%= files.min %>', 101 | options: { 102 | output: { 103 | comments: /^!/u 104 | }, 105 | report: 'gzip', 106 | ie8: true 107 | } 108 | } 109 | }, 110 | copy: { 111 | dist: { 112 | src: [ 'src/wrapper.js' ], 113 | dest: '<%= paths.build %>/<%= files.cat %>', 114 | options: { 115 | process: (content) => { 116 | const replaceRegex = /\s\/\* \[POWERTIP CODE\] \*\//u; 117 | const coreFile = grunt.file.read(grunt.template.process('<%= concat.core.dest %>')); 118 | return grunt.template.process(content).replace(replaceRegex, coreFile); 119 | } 120 | } 121 | }, 122 | css: { 123 | src: [ 'css/*.css' ], 124 | dest: '<%= paths.build %>/' 125 | }, 126 | examples: { 127 | src: [ 'examples/*' ], 128 | dest: '<%= paths.build %>/', 129 | options: { 130 | process: (content) => { 131 | const scriptsRegex = /(?:.*\r?\n\s)*/u; 132 | const builtScriptTag = ''; 133 | return content.replace(scriptsRegex, grunt.template.process(builtScriptTag)); 134 | } 135 | } 136 | }, 137 | browserify: { 138 | src: [ 'test/browserify.html' ], 139 | dest: '<%= paths.temp %>/browserify.html', 140 | nonull: true 141 | }, 142 | license: { 143 | src: [ '<%= files.license %>' ], 144 | dest: '<%= paths.build %>/<%= files.license %>', 145 | nonull: true 146 | }, 147 | changelog: { 148 | src: [ '<%= files.changelog %>' ], 149 | dest: '<%= paths.build %>/<%= files.changelog %>', 150 | nonull: true 151 | }, 152 | index: { 153 | src: [ '<%= paths.build %>/index.md' ], 154 | dest: 'index.md', 155 | nonull: true 156 | }, 157 | jsassets: { 158 | src: [ '<%= paths.build %>/<%= files.cat %>' ], 159 | dest: 'scripts/<%= files.cat %>', 160 | nonull: true 161 | }, 162 | cssassets: { 163 | files: [ 164 | { 165 | expand: true, 166 | cwd: '<%= paths.build %>/css/', 167 | src: [ '*.css', '!*.min.css' ], 168 | dest: 'styles/' 169 | } 170 | ] 171 | } 172 | }, 173 | csslint: { 174 | themes: { 175 | src: [ 'css/*.css' ], 176 | options: { 177 | ids: false, 178 | 'order-alphabetical': false 179 | } 180 | } 181 | }, 182 | cssmin: { 183 | compress: { 184 | files: [ 185 | { 186 | expand: true, 187 | cwd: 'css/', 188 | src: [ '*.css' ], 189 | dest: '<%= paths.build %>/css/', 190 | rename: (dest, matchedSrcPath) => (dest + matchedSrcPath.replace('.css', '.min.css')) 191 | } 192 | ], 193 | options: { 194 | report: 'gzip' 195 | } 196 | } 197 | }, 198 | compress: { 199 | zip: { 200 | options: { 201 | archive: '<%= paths.build %>/<%= files.zip %>' 202 | }, 203 | files: [ 204 | { 205 | expand: true, 206 | cwd: '<%= paths.build %>/', 207 | src: [ '**/*' ] 208 | } 209 | ] 210 | } 211 | }, 212 | shell: { 213 | checkoutpages: { 214 | command: 'git checkout gh-pages' 215 | }, 216 | addindex: { 217 | command: [ 218 | 'git add index.md', 219 | 'git commit -m "Publishing docs"' 220 | ].join(' && ') 221 | }, 222 | addassets: { 223 | command: [ 224 | 'git add scripts/<%= files.cat %>', 225 | 'git add styles/jquery.powertip*.css', 226 | 'git commit -m "Publishing assets"' 227 | ].join(' && ') 228 | }, 229 | checkoutmaster: { 230 | command: 'git checkout master' 231 | } 232 | } 233 | }); 234 | 235 | // custom task to build the gh-pages index.md file 236 | grunt.registerTask('build:gh-pages', 'Create the gh-pages markdown.', () => { 237 | const template = grunt.file.read('doc/gh-pages.template.md'); 238 | const data = { 239 | pkg: grunt.file.readJSON('package.json'), 240 | doc: grunt.file.read('doc/README.md'), 241 | changelog: grunt.file.readYAML(grunt.template.process('<%= files.changelog %>')) 242 | }; 243 | const page = grunt.template.process(template, { data: data }); 244 | grunt.file.write('dist/index.md', page); 245 | grunt.log.ok('gh-pages markdown created'); 246 | }); 247 | 248 | // force unix style line endings 249 | grunt.util.linefeed = '\n'; 250 | 251 | // register grunt tasks 252 | grunt.registerTask('default', [ 'test' ]); 253 | grunt.registerTask('test', [ 'jsonlint', 'concat:core', 'indent', 'copy:dist', 'eslint', 'qunit:tests', 'test:browserify', 'csslint', 'clean:temp' ]); 254 | grunt.registerTask('test:browserify', [ 'copy:browserify', 'browserify', 'qunit:browserify' ]); 255 | grunt.registerTask('build', [ 'jsonlint', 'build:js', 'build:css' ]); 256 | grunt.registerTask('build:js', [ 'concat:core', 'indent', 'copy:dist', 'eslint', 'qunit:tests', 'test:browserify', 'uglify', 'clean:temp' ]); 257 | grunt.registerTask('build:css', [ 'csslint', 'copy:css', 'cssmin' ]); 258 | grunt.registerTask('build:docs', [ 'copy:examples', 'copy:license', 'copy:changelog' ]); 259 | grunt.registerTask('build:release', [ 'clean:dist', 'build', 'build:docs', 'compress' ]); 260 | grunt.registerTask('build:npm', [ 'clean:dist', 'build' ]); 261 | grunt.registerTask('deploy', [ 'deploy:docs', 'deploy:assets' ]); 262 | grunt.registerTask('deploy:docs', [ 'build:gh-pages', 'shell:checkoutpages', 'copy:index', 'shell:addindex', 'shell:checkoutmaster' ]); 263 | grunt.registerTask('deploy:assets', [ 'build:release', 'shell:checkoutpages', 'copy:jsassets', 'copy:cssassets', 'shell:addassets', 'shell:checkoutmaster' ]); 264 | }; 265 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024 Steven Benner (https://stevenbenner.com/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerTip 2 | 3 | PowerTip is a jQuery tooltip plugin with some advanced features like **hover intent testing**, **tooltip queueing**, and **support for complex data**. 4 | 5 | This software is licensed under the [MIT license][license]. 6 | 7 | [![Release Version][releasebadge]][releases] [![NPM Version][npmbadge]][npmpage] [![Test Status][testbadge]][teststatus] 8 | 9 | [license]: LICENSE.txt 10 | [releases]: https://github.com/stevenbenner/jquery-powertip/releases 11 | [teststatus]: https://github.com/stevenbenner/jquery-powertip/actions/workflows/run-tests.yml 12 | [releasebadge]: https://img.shields.io/github/release/stevenbenner/jquery-powertip.svg?style=flat-square 13 | [npmbadge]: https://img.shields.io/npm/v/jquery-powertip.svg?style=flat-square 14 | [testbadge]: https://img.shields.io/github/actions/workflow/status/stevenbenner/jquery-powertip/run-tests.yml?style=flat-square 15 | 16 | ## Getting Started 17 | 18 | * Download the latest stable release from the [PowerTip web site][projectpage] or install [jquery-powertip][npmpage] from npm. 19 | * Add the JavaScript and CSS file references to your web site. 20 | * Add a title or data-powertip attribute to the elements you want to show tooltips for. 21 | * Run the `powerTip()` method on those elements. 22 | 23 | [npmpage]: https://www.npmjs.com/package/jquery-powertip 24 | 25 | ## Documentation 26 | 27 | You can find the documentation for the **latest release version** on the [PowerTip web site][projectpage]. You will find the documentation for the **latest in-development version** in the [doc folder][docs] in the GitHub repository. 28 | 29 | [projectpage]: https://stevenbenner.github.io/jquery-powertip/ 30 | [docs]: https://github.com/stevenbenner/jquery-powertip/tree/master/doc 31 | 32 | ## Reporting Bugs 33 | 34 | For bug reports, questions, feature requests, or other suggestions the best way to contact me is to [create an issue][newissue] on GitHub. 35 | 36 | [newissue]: https://github.com/stevenbenner/jquery-powertip/issues/new 37 | 38 | ## Contributor Guide 39 | 40 | Make PowerTip better! Join the [league of awesome][contributors] today by submitting a patch! The best way to submit patches is to [fork this project][fork] on GitHub and submit a pull request. But if you are unwilling or unable to use GitHub I will accept patches in any way you can get them to me (JSFiddle, pastebin, text file, whatever). 41 | 42 | [contributors]: https://github.com/stevenbenner/jquery-powertip/graphs/contributors 43 | [fork]: https://github.com/stevenbenner/jquery-powertip/fork 44 | 45 | ### Style Guide 46 | 47 | These are general guidelines, not rules. I won't refuse a pull request just because it isn't the style that I use. 48 | 49 | * Style guide: In general, follow the [Google JavaScript Style Guide][styleguide]. 50 | * Line wrap: Soft-wrap at 80 characters (go further if wrapping makes code less readable). 51 | * Indentation: Use tabs for indentation. 52 | * JSDoc comments: Use [closure compiler annotations][jsdoc]. 53 | * Method chaining: Avoid long chained method statements, two or three max. 54 | 55 | [styleguide]: https://google.github.io/styleguide/javascriptguide.xml 56 | [jsdoc]: https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler 57 | -------------------------------------------------------------------------------- /css/jquery.powertip-blue.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the blue theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #d2e6fa; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #a5d2fa inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #a5d2fa inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #a5d2fa inset; 14 | border: 1px solid #4b91d2; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the dark monochrome theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #424242; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.2) inset, 0 -2px 2px #323232 inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.2) inset, 0 -2px 2px #323232 inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.2) inset, 0 -2px 2px #323232 inset; 14 | border: 1px solid #000000; 15 | border-radius: 6px; 16 | color: #ffffff; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-green.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the green theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #f0ffb9; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.8) inset, 0 -2px 2px #dcf582 inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.8) inset, 0 -2px 2px #dcf582 inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.8) inset, 0 -2px 2px #dcf582 inset; 14 | border: 1px solid #9bc800; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-light.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the light monochrome theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #f2f2f2; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #dcdcdc inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #dcdcdc inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #dcdcdc inset; 14 | border: 1px solid #acacac; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-orange.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the orange theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #ffcdaf; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #fab482 inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #fab482 inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #fab482 inset; 14 | border: 1px solid #f5a550; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-purple.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the purple theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #ebd2fa; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #d796ff inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #d796ff inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #d796ff inset; 14 | border: 1px solid #914bd2; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-red.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the red theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #ffc8c3; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #f5aa9b inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #f5aa9b inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.5) inset, 0 -2px 2px #f5aa9b inset; 14 | border: 1px solid #eb5037; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip-yellow.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the yellow theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #ffffb4; 11 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.8) inset, 0 -2px 2px #fafa6e inset; 12 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.8) inset, 0 -2px 2px #fafa6e inset; 13 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 1px rgba(255, 255, 255, 0.8) inset, 0 -2px 2px #fafa6e inset; 14 | border: 1px solid #fafa50; 15 | border-radius: 6px; 16 | color: #000000; 17 | display: none; 18 | padding: 10px; 19 | position: absolute; 20 | white-space: nowrap; 21 | z-index: 2147483647; 22 | } 23 | #powerTip:before { 24 | content: attr(class) " "; 25 | position: absolute; 26 | height: 0; 27 | width: 0; 28 | text-indent: 100%; 29 | overflow: hidden; 30 | } 31 | #powerTip.n:before, #powerTip.s:before { 32 | border-right: 5px solid transparent; 33 | border-left: 5px solid transparent; 34 | left: 50%; 35 | margin-left: -5px; 36 | } 37 | #powerTip.e:before, #powerTip.w:before { 38 | border-bottom: 5px solid transparent; 39 | border-top: 5px solid transparent; 40 | margin-top: -5px; 41 | top: 50%; 42 | } 43 | #powerTip.n:before, 44 | #powerTip.ne:before, #powerTip.nw:before { 45 | bottom: -10px; 46 | } 47 | #powerTip.n:before, 48 | #powerTip.ne:before, #powerTip.nw:before , 49 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 50 | border-top-color: inherit; 51 | border-top-style: solid; 52 | border-top-width: 10px; 53 | } 54 | #powerTip.e:before { 55 | border-right-color: inherit; 56 | border-right-style: solid; 57 | border-right-width: 10px; 58 | left: -10px; 59 | } 60 | #powerTip.s:before, 61 | #powerTip.se:before, #powerTip.sw:before { 62 | top: -10px; 63 | } 64 | #powerTip.s:before, 65 | #powerTip.se:before, #powerTip.sw:before , 66 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 67 | border-bottom-color: inherit; 68 | border-bottom-style: solid; 69 | border-bottom-width: 10px; 70 | } 71 | #powerTip.w:before { 72 | border-left-color: inherit; 73 | border-left-style: solid; 74 | border-left-width: 10px; 75 | right: -10px; 76 | } 77 | #powerTip.ne:before, #powerTip.se:before { 78 | border-right: 10px solid transparent; 79 | border-left: 0; 80 | left: 10px; 81 | } 82 | #powerTip.nw:before, #powerTip.sw:before { 83 | border-left: 10px solid transparent; 84 | border-right: 0; 85 | right: 10px; 86 | } 87 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 88 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 89 | bottom: -10px; 90 | border-left: 5px solid transparent; 91 | border-right: 5px solid transparent; 92 | left: 10px; 93 | } 94 | #powerTip.ne-alt:before { 95 | left: auto; 96 | right: 10px; 97 | } 98 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 99 | border-top: none; 100 | bottom: auto; 101 | top: -10px; 102 | } 103 | #powerTip.se-alt:before { 104 | left: auto; 105 | right: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /css/jquery.powertip.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip 3 | * https://stevenbenner.github.io/jquery-powertip/ 4 | * 5 | * Stylesheet for the monochrome (default) theme. 6 | */ 7 | 8 | #powerTip { 9 | cursor: default; 10 | background-color: #333; 11 | background-color: rgba(0, 0, 0, 0.8); 12 | border-color: #333; 13 | border-color: rgba(0, 0, 0, 0.8); 14 | border-radius: 6px; 15 | color: #fff; 16 | display: none; 17 | padding: 10px; 18 | position: absolute; 19 | white-space: nowrap; 20 | z-index: 2147483647; 21 | } 22 | #powerTip:before { 23 | content: attr(class) " "; 24 | position: absolute; 25 | height: 0; 26 | width: 0; 27 | text-indent: 100%; 28 | overflow: hidden; 29 | } 30 | #powerTip.n:before, #powerTip.s:before { 31 | border-right: 5px solid transparent; 32 | border-left: 5px solid transparent; 33 | left: 50%; 34 | margin-left: -5px; 35 | } 36 | #powerTip.e:before, #powerTip.w:before { 37 | border-bottom: 5px solid transparent; 38 | border-top: 5px solid transparent; 39 | margin-top: -5px; 40 | top: 50%; 41 | } 42 | #powerTip.n:before, 43 | #powerTip.ne:before, #powerTip.nw:before { 44 | bottom: -10px; 45 | } 46 | #powerTip.n:before, 47 | #powerTip.ne:before, #powerTip.nw:before , 48 | #powerTip.nw-alt:before, #powerTip.ne-alt:before { 49 | border-top-color: inherit; 50 | border-top-style: solid; 51 | border-top-width: 10px; 52 | } 53 | #powerTip.e:before { 54 | border-right-color: inherit; 55 | border-right-style: solid; 56 | border-right-width: 10px; 57 | left: -10px; 58 | } 59 | #powerTip.s:before, 60 | #powerTip.se:before, #powerTip.sw:before { 61 | top: -10px; 62 | } 63 | #powerTip.s:before, 64 | #powerTip.se:before, #powerTip.sw:before , 65 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 66 | border-bottom-color: inherit; 67 | border-bottom-style: solid; 68 | border-bottom-width: 10px; 69 | } 70 | #powerTip.w:before { 71 | border-left-color: inherit; 72 | border-left-style: solid; 73 | border-left-width: 10px; 74 | right: -10px; 75 | } 76 | #powerTip.ne:before, #powerTip.se:before { 77 | border-right: 10px solid transparent; 78 | border-left: 0; 79 | left: 10px; 80 | } 81 | #powerTip.nw:before, #powerTip.sw:before { 82 | border-left: 10px solid transparent; 83 | border-right: 0; 84 | right: 10px; 85 | } 86 | #powerTip.nw-alt:before, #powerTip.ne-alt:before, 87 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 88 | bottom: -10px; 89 | border-left: 5px solid transparent; 90 | border-right: 5px solid transparent; 91 | left: 10px; 92 | } 93 | #powerTip.ne-alt:before { 94 | left: auto; 95 | right: 10px; 96 | } 97 | #powerTip.sw-alt:before, #powerTip.se-alt:before { 98 | border-top: none; 99 | bottom: auto; 100 | top: -10px; 101 | } 102 | #powerTip.se-alt:before { 103 | left: auto; 104 | right: 10px; 105 | } 106 | -------------------------------------------------------------------------------- /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!
'); 182 | tooltip.on('click', function() { /* ... */ }); 183 | 184 | $('#element').data('powertipjq', tooltip); 185 | ``` 186 | 187 | or 188 | 189 | ```javascript 190 | $('#element').data('powertipjq', function() { 191 | var tooltip = $('
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 |
203 |

Some Title

204 |

This will be the tooltip text.

205 |

This link will be in the tooltip as well.

206 |
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 | ![Example without smart placement](https://stevenbenner.github.io/jquery-powertip/images/without-smart-placement.png) 431 | 432 | **With smart placement:** 433 | 434 | ![Example with smart placement](https://stevenbenner.github.io/jquery-powertip/images/with-smart-placement.png) 435 | 436 | It does this by detecting that a tooltip would appear outside of the viewport, then trying a series of other placement options until it finds one that isn't going to be outside of the viewport. You can define the placement fall backs and priorities yourself by overriding them in the `$.fn.powerTip.smartPlacementLists` object. 437 | 438 | These are the default smart placement priority lists: 439 | 440 | ```javascript 441 | $.fn.powerTip.smartPlacementLists = { 442 | n: [ 'n', 'ne', 'nw', 's' ], 443 | e: [ 'e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e' ], 444 | s: [ 's', 'se', 'sw', 'n' ], 445 | w: [ 'w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w' ], 446 | nw: [ 'nw', 'w', 'sw', 'n', 's', 'se', 'nw' ], 447 | ne: [ 'ne', 'e', 'se', 'n', 's', 'sw', 'ne' ], 448 | sw: [ 'sw', 'w', 'nw', 's', 'n', 'ne', 'sw' ], 449 | se: [ 'se', 'e', 'ne', 's', 'n', 'nw', 'se' ], 450 | 'nw-alt': [ 'nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e' ], 451 | 'ne-alt': [ 'ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w' ], 452 | 'sw-alt': [ 'sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e' ], 453 | 'se-alt': [ 'se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w' ] 454 | }; 455 | ``` 456 | 457 | As you can see, each placement option has an array of placement options that it can fall back on. The first item in the array is the highest priority placement, the last is the lowest priority. The last item in the array is also the default. If none of the placement options can be fully displayed within the viewport then the last item in the array is the placement used to show the tooltip. 458 | 459 | You can override these default placement priority lists before you call `powerTip()` and define your own smart placement fall back order. Like so: 460 | 461 | ```javascript 462 | // define custom smart placement order 463 | $.fn.powerTip.smartPlacementLists.n = [ 'n', 's', 'e', 'w' ]; 464 | 465 | // these tips will use the custom 'north' smart placement list 466 | $('.tips').powerTip({ 467 | placement: 'n', 468 | smartPlacement: true 469 | }); 470 | ``` 471 | 472 | Smart placement is **disabled** by default because I believe that the world would be a better place if features that override explicit configuration values were disabled by default. 473 | 474 | ## Custom PowerTip Integration 475 | 476 | If you need to use PowerTip in a non-standard way, that is to say, if you need tooltips to open and close in some way other than the default mouse-on/mouse-off behavior then you can create your own event handlers and tell PowerTip when it should open and close tooltips. 477 | 478 | This is actually quite easy, you just tell PowerTip not to hook the default mouse and keyboard events when you run the plugin by setting the `manual` option to `true`, then use the API to open and close tooltips. While this is a bit more technical then just using the default behavior, it works just as well. In fact, PowerTip uses this same public API internally. 479 | 480 | ### Disable event binding 481 | 482 | To disable binding of the events that are normally attached when you run `powerTip()` just set the `manual` option to `true`. 483 | 484 | ```javascript 485 | $('.tooltips').powerTip({ manual: true }); 486 | ``` 487 | 488 | Now PowerTip has initialized and set up the `.tooltips` elements, but it will not open tooltips for those elements automatically. You must manually open the tooltips using the API. 489 | 490 | ### Building your own event handlers 491 | 492 | Here is an example of a manually implemented click-to-open tooltip to show you how it's done: 493 | 494 | ```javascript 495 | // run PowerTip - but disable the default event hooks 496 | $('.tooltips').powerTip({ manual: true }); 497 | 498 | // hook custom onclick function 499 | $('.tooltips').on('click', function() { 500 | // toggle the tooltip for the element that received the click event 501 | $.powerTip.toggle(this); 502 | }); 503 | 504 | // Note: this is just for example - for click-to-open you should probably just 505 | // use the openEvents/closeEvents options, like this: 506 | // $('.tooltips').powerTip({ openEvents: [ 'click' ], closeEvents: [ 'click' ] }); 507 | ``` 508 | 509 | This code will open a tooltip when the element is clicked and close it when the element is clicked again, or when another one of the `.tooltips` elements gets clicked. 510 | 511 | Now it's worth noting that this example doesn't take advantage of the hover intent feature or the tooltip delays because the mouse position was not passed to the `toggle()` method. 512 | 513 | So, let's look at a more complex situation. In the following example we hook up mouse events just like PowerTip would internally (open on mouse enter, close on mouse leave). 514 | 515 | ```javascript 516 | // run PowerTip - but disable the default event hooks 517 | $('.tooltips').powerTip({ manual: true }); 518 | 519 | // hook custom mouse events 520 | $('.tooltips').on({ 521 | mouseenter: function(event) { 522 | // note that we pass the jQuery mouse event to the show() method 523 | // this lets PowerTip do the hover intent testing 524 | $.powerTip.show(this, event); 525 | }, 526 | mouseleave: function() { 527 | // note that we pass the element to the hide() method 528 | // this lets PowerTip wait before closing the tooltip, if the user's 529 | // mouse cursor returns to this element before the tooltip closes then 530 | // the close will be canceled 531 | $.powerTip.hide(this); 532 | } 533 | }); 534 | ``` 535 | 536 | And there you have it. If you want to enable the hover intent testing then you will need to pass the mouse event to the `show()` method and if you want to enable the close delay feature then you have to pass that element to the `hide()` method. 537 | 538 | ### Additional notes 539 | 540 | * Only mouse events (`mouseenter`, `mouseleave`, `hover`, `mousemove`) have the required properties (`pageX`, and `pageY`) to do hover intent testing. Click events and keyboard events will not work. 541 | * You should not use the `destroy()` method while your custom handlers are hooked up, it may cause unexpected things to happen (like mouse position tracking not working). 542 | * In most cases you should probably be using the `openEvents` and `closeEvents` options to bind tooltips to non-default events. 543 | -------------------------------------------------------------------------------- /doc/gh-pages.template.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | description: "PowerTip is a jQuery plugin for creating smooth, modern tooltips." 4 | --- 5 | 6 | PowerTip is a jQuery tooltip plugin with a smooth user experience that features a very flexible design which is easy to customize, gives you a variety of different ways to create tooltips, supports adding complex data to tooltips, and has a robust API for developers seeking greater integration with their web applications. 7 | 8 |

9 | Download v<%= pkg.version %> 10 | Zip file with examples, CSS, and script. 11 | For older versions, see the releases page on GitHub. 12 |

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 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | ### Mouse follow example 38 | 39 |
40 |
41 | The PowerTip for this box will follow the mouse. 42 |
43 |
44 | 45 | ### Mouse on to popup example 46 | 47 |
48 |
49 | The PowerTip for this box will appear on the right and you will be able to interact with its content. 50 |
51 |
52 | 53 | <%= 54 | doc.replace( 55 | /```(\w+)((?:.*\r?\n)*?)```/g, 56 | '{% highlight $1 %}$2{% endhighlight %}' 57 | ) 58 | %> 59 | 60 | ## Change Log 61 | <% 62 | _.each(changelog, function(details, version) { 63 | var date = details.date; 64 | 65 | if (date instanceof Date) { 66 | date = grunt.template.date(new Date(date.getTime() + date.getTimezoneOffset() * 60000), 'longDate'); 67 | } 68 | 69 | if (details.diff) { 70 | print('\n\n### [' + version + '](' + details.diff + ')'); 71 | } else { 72 | print('\n\n### ' + version); 73 | } 74 | print(' - ' + details.description + ' (' + date + ')\n'); 75 | 76 | _.each(details.changes, function(value, key, list) { 77 | print('\n* **' + value.section + '**'); 78 | _.each(value.changes, function(value, key, list) { 79 | print('\n\t* ' + value); 80 | }); 81 | }); 82 | }); 83 | %> 84 | 85 | ## Contributors 86 | 87 | Special thanks to the [contributors](https://github.com/stevenbenner/jquery-powertip/graphs/contributors) who have helped build PowerTip. 88 | -------------------------------------------------------------------------------- /doc/release-process.md: -------------------------------------------------------------------------------- 1 | # PowerTip Release Process 2 | 3 | **THIS DOCUMENT IS FOR INTERNAL REFERENCE ONLY** - I am documenting the release process so that I have a nice checklist to go over when releasing a new version of PowerTip. You probably don't care how PowerTip is built and released unless you plan on maintaining your own fork. 4 | 5 | ## Version Format 6 | 7 | PowerTip uses [Semantic Versioning](https://semver.org/) and the version is in the format of [MAJOR].[MINOR].[PATCH]. Versioning is dictated by the exposed API that PowerTip users consume when using the plugin. 8 | 9 | This includes anything in the following namespaces: 10 | 11 | * `$.fn.powerTip` 12 | * `$.fn.powerTip.defaults` 13 | * `$.fn.powerTip.smartPlacementLists` 14 | * `$.powerTip` 15 | 16 | The events that fire during the tooltip life cycle are also considered to be part of the API for versioning purposes. 17 | 18 | ### Semantic Versioning Requirements 19 | 20 | > * MAJOR version when you make incompatible API changes, 21 | > * MINOR version when you add functionality in a backwards-compatible manner, and 22 | > * PATCH version when you make backwards-compatible bug fixes. 23 | 24 | ## The Release Process 25 | 26 | 1. **Update the date and diff for the release in CHANGELOG.yml** 27 | 28 | The CHANGELOG.yml file is used to generate the release notes seen on the project page. 29 | 30 | 2. **Bump the version in package.json** 31 | 32 | The package.json is used when building the project and when publishing to npm. 33 | 34 | 3. **Run `grunt build:release`** 35 | 36 | This will build and test all of the code and generate the zip archive for release in the dist folder. 37 | 38 | 4. **Tag the version** 39 | 40 | Make sure the changes from steps 1 and 2 have been committed. 41 | 42 | `git tag -a vX.X.X -m "version X.X.X"` 43 | 44 | 5. **Run `grunt deploy`** 45 | 46 | This will build the project page content, commit it to the gh-pages branch, and return to the master branch. It does not push any changes to the repo. 47 | 48 | 6. **Review commits and push them** 49 | 50 | **POINT OF NO RETURN** 51 | 52 | This is the fail-safe step. Make sure the build looks right. Make sure the commits from steps 1 and 2 are correct. Make sure the commits added to the gh-pages branch look right. If everything looks good then push the commits and the tag. 53 | 54 | 7. **Publish to npm** 55 | 56 | *Prefer npm version 4.0.0 or greater for prepublishOnly script* 57 | 58 | First, verify that the package to be release to npm contains the expected files in the expected structure. Run `grunt build:npm && npm pack`. This will generate the appropriate dist folder contents and create the tgz package. Look over the tgz package to make sure everything looks good. 59 | 60 | Now publish the new release to the npm repository by running the `npm publish` command. 61 | 62 | 7. **Add new release to GitHub repo** 63 | 64 | Add a release for the tag you just created, copy and paste the release notes, and add the zip archive to the release. 65 | 66 | 8. **Update JSFiddle if needed** (it usually will not be needed) 67 | 68 | The [PowerTip JSFiddle](https://jsfiddle.net/stevenbenner/2baqv/) is used by people wanting to quickly play with the plugin before really digging into it. If there were any breaking changes or significant new features then they should be added to the JSFiddle. 69 | -------------------------------------------------------------------------------- /examples/examples.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PowerTip Examples 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

PowerTip Examples

29 | 30 | 31 |
32 |

Placement examples

33 |
34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 | 65 | 66 | 67 |
68 |

Mouse follow example

69 |
70 | The PowerTip for this box will follow the mouse. 71 |
72 |
73 | 79 | 80 | 81 |
82 |

Mouse on to tooltip example

83 |
84 | The PowerTip for this box will appear on the right and you will be able to interact with its content. 85 |
86 |
87 | 102 | 103 | 104 |
105 |

Custom show and hide event examples

106 | 107 | 108 | 109 | 110 |
111 | 135 | 136 | 137 |
138 |

API examples

139 | 140 | 141 | 142 |
143 | Delegated manual mouseover, with lazy-loaded tooltip: 144 | 145 | 146 |
147 |
148 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-powertip", 3 | "title": "PowerTip", 4 | "description": "A jQuery plugin that creates hover tooltips.", 5 | "version": "1.3.2", 6 | "main": "dist/jquery.powertip.js", 7 | "homepage": "https://stevenbenner.github.io/jquery-powertip/", 8 | "author": { 9 | "name": "Steven Benner", 10 | "url": "https://stevenbenner.com/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/stevenbenner/jquery-powertip.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/stevenbenner/jquery-powertip/issues" 18 | }, 19 | "license": "MIT", 20 | "keywords": [ 21 | "jquery-plugin", 22 | "ecosystem:jquery", 23 | "powertip", 24 | "jquery", 25 | "tooltip", 26 | "tooltips", 27 | "ui", 28 | "browser" 29 | ], 30 | "dependencies": { 31 | "jquery": "1.7 - 4" 32 | }, 33 | "devDependencies": { 34 | "@stevenbenner/eslint-config": "~3.0", 35 | "grunt": "~1.6", 36 | "grunt-browserify": "~6.0", 37 | "grunt-contrib-clean": "~2.0", 38 | "grunt-contrib-compress": "~2.0", 39 | "grunt-contrib-concat": "~2.1", 40 | "grunt-contrib-copy": "~1.0", 41 | "grunt-contrib-csslint": "~2.0", 42 | "grunt-contrib-cssmin": "~5.0", 43 | "grunt-contrib-qunit": "~8.0", 44 | "grunt-contrib-uglify": "~5.2", 45 | "grunt-eslint": "~24.3", 46 | "grunt-indent": "~1.0", 47 | "grunt-jsonlint": "~2.1", 48 | "grunt-shell": "~4.0", 49 | "jit-grunt": "~0.10", 50 | "qunit": "~2.20", 51 | "time-grunt": "~2.0" 52 | }, 53 | "scripts": { 54 | "grunt": "grunt", 55 | "test": "grunt test --verbose --stack", 56 | "prepublishOnly": "grunt build:npm" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 3 4 | }, 5 | "env": { 6 | "amd": true, 7 | "browser": true, 8 | "commonjs": true, 9 | "jquery": true, 10 | "node": false 11 | }, 12 | "rules": { 13 | "strict": [ 14 | "error", 15 | "never" 16 | ], 17 | "no-var": "off", 18 | "prefer-template": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip Core 3 | * 4 | * @fileoverview Core variables, plugin object, and API. 5 | * @link https://stevenbenner.github.io/jquery-powertip/ 6 | * @author Steven Benner (https://stevenbenner.com/) 7 | * @requires jQuery 1.7+ 8 | */ 9 | 10 | // useful private variables 11 | var $document = $(document), 12 | $window = $(window), 13 | $body = $('body'); 14 | 15 | // constants 16 | var DATA_DISPLAYCONTROLLER = 'displayController', 17 | DATA_HASACTIVEHOVER = 'hasActiveHover', 18 | DATA_FORCEDOPEN = 'forcedOpen', 19 | DATA_HASMOUSEMOVE = 'hasMouseMove', 20 | DATA_MOUSEONTOTIP = 'mouseOnToPopup', 21 | DATA_ORIGINALTITLE = 'originalTitle', 22 | DATA_POWERTIP = 'powertip', 23 | DATA_POWERTIPJQ = 'powertipjq', 24 | DATA_POWERTIPTARGET = 'powertiptarget', 25 | EVENT_NAMESPACE = '.powertip', 26 | RAD2DEG = 180 / Math.PI, 27 | MOUSE_EVENTS = [ 28 | 'click', 29 | 'dblclick', 30 | 'mousedown', 31 | 'mouseup', 32 | 'mousemove', 33 | 'mouseover', 34 | 'mouseout', 35 | 'mouseenter', 36 | 'mouseleave', 37 | 'contextmenu' 38 | ]; 39 | 40 | /** 41 | * Session data 42 | * Private properties global to all powerTip instances 43 | */ 44 | var session = { 45 | elements: [], 46 | tooltips: null, 47 | isTipOpen: false, 48 | isFixedTipOpen: false, 49 | isClosing: false, 50 | tipOpenImminent: false, 51 | activeHover: null, 52 | currentX: 0, 53 | currentY: 0, 54 | previousX: 0, 55 | previousY: 0, 56 | desyncTimeout: null, 57 | closeDelayTimeout: null, 58 | mouseTrackingActive: false, 59 | delayInProgress: false, 60 | windowWidth: 0, 61 | windowHeight: 0, 62 | scrollTop: 0, 63 | scrollLeft: 0 64 | }; 65 | 66 | /** 67 | * Collision enumeration 68 | * @enum {number} 69 | */ 70 | var Collision = { 71 | none: 0, 72 | top: 1, 73 | bottom: 2, 74 | left: 4, 75 | right: 8 76 | }; 77 | 78 | /** 79 | * Display hover tooltips on the matched elements. 80 | * @param {(Object|string)=} opts The options object to use for the plugin, or 81 | * the name of a method to invoke on the first matched element. 82 | * @param {*=} [arg] Argument for an invoked method (optional). 83 | * @return {jQuery} jQuery object for the matched selectors. 84 | */ 85 | $.fn.powerTip = function(opts, arg) { 86 | var targetElements = this, 87 | options, 88 | tipController; 89 | 90 | // don't do any work if there were no matched elements 91 | if (!targetElements.length) { 92 | return targetElements; 93 | } 94 | 95 | // handle api method calls on the plugin, e.g. powerTip('hide') 96 | if (typeof opts === 'string' && $.powerTip[opts]) { 97 | return $.powerTip[opts].call(targetElements, targetElements, arg); 98 | } 99 | 100 | // extend options 101 | options = $.extend({}, $.fn.powerTip.defaults, opts); 102 | 103 | // handle repeated powerTip calls on the same element by destroying any 104 | // original instance hooked to it and replacing it with this call 105 | $.powerTip.destroy(targetElements); 106 | 107 | // instantiate the TooltipController for this instance 108 | tipController = new TooltipController(options); 109 | 110 | // hook mouse and viewport dimension tracking 111 | initTracking(); 112 | 113 | // setup the elements 114 | targetElements.each(function elementSetup() { 115 | var $this = $(this), 116 | dataPowertip = $this.data(DATA_POWERTIP), 117 | dataElem = $this.data(DATA_POWERTIPJQ), 118 | dataTarget = $this.data(DATA_POWERTIPTARGET), 119 | title = $this.attr('title'); 120 | 121 | // attempt to use title attribute text if there is no data-powertip, 122 | // data-powertipjq or data-powertiptarget. If we do use the title 123 | // attribute, delete the attribute so the browser will not show it 124 | if (!dataPowertip && !dataTarget && !dataElem && title) { 125 | $this.data(DATA_POWERTIP, title); 126 | $this.data(DATA_ORIGINALTITLE, title); 127 | $this.removeAttr('title'); 128 | } 129 | 130 | // create hover controllers for each element 131 | $this.data( 132 | DATA_DISPLAYCONTROLLER, 133 | new DisplayController($this, options, tipController) 134 | ); 135 | }); 136 | 137 | // attach events to matched elements if the manual option is not enabled 138 | if (!options.manual) { 139 | // attach open events 140 | $.each(options.openEvents, function(idx, evt) { 141 | if ($.inArray(evt, options.closeEvents) > -1) { 142 | // event is in both openEvents and closeEvents, so toggle it 143 | targetElements.on(evt + EVENT_NAMESPACE, function elementToggle(event) { 144 | $.powerTip.toggle(this, event); 145 | }); 146 | } else { 147 | targetElements.on(evt + EVENT_NAMESPACE, function elementOpen(event) { 148 | $.powerTip.show(this, event); 149 | }); 150 | } 151 | }); 152 | 153 | // attach close events 154 | $.each(options.closeEvents, function(idx, evt) { 155 | if ($.inArray(evt, options.openEvents) < 0) { 156 | targetElements.on(evt + EVENT_NAMESPACE, function elementClose(event) { 157 | // set immediate to true for any event without mouse info 158 | $.powerTip.hide(this, !isMouseEvent(event)); 159 | }); 160 | } 161 | }); 162 | 163 | // attach escape key close event 164 | targetElements.on('keydown' + EVENT_NAMESPACE, function elementKeyDown(event) { 165 | // always close tooltip when the escape key is pressed 166 | if (event.keyCode === 27) { 167 | $.powerTip.hide(this, true); 168 | } 169 | }); 170 | } 171 | 172 | // remember elements that the plugin is attached to 173 | session.elements.push(targetElements); 174 | 175 | return targetElements; 176 | }; 177 | 178 | /** 179 | * Default options for the powerTip plugin. 180 | */ 181 | $.fn.powerTip.defaults = { 182 | fadeInTime: 200, 183 | fadeOutTime: 100, 184 | followMouse: false, 185 | popupId: 'powerTip', 186 | popupClass: null, 187 | intentSensitivity: 7, 188 | intentPollInterval: 100, 189 | closeDelay: 100, 190 | placement: 'n', 191 | smartPlacement: false, 192 | offset: 10, 193 | mouseOnToPopup: false, 194 | manual: false, 195 | openEvents: [ 'mouseenter', 'focus' ], 196 | closeEvents: [ 'mouseleave', 'blur' ] 197 | }; 198 | 199 | /** 200 | * Default smart placement priority lists. 201 | * The first item in the array is the highest priority, the last is the lowest. 202 | * The last item is also the default, which will be used if all previous options 203 | * do not fit. 204 | */ 205 | $.fn.powerTip.smartPlacementLists = { 206 | n: [ 'n', 'ne', 'nw', 's' ], 207 | e: [ 'e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e' ], 208 | s: [ 's', 'se', 'sw', 'n' ], 209 | w: [ 'w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w' ], 210 | nw: [ 'nw', 'w', 'sw', 'n', 's', 'se', 'nw' ], 211 | ne: [ 'ne', 'e', 'se', 'n', 's', 'sw', 'ne' ], 212 | sw: [ 'sw', 'w', 'nw', 's', 'n', 'ne', 'sw' ], 213 | se: [ 'se', 'e', 'ne', 's', 'n', 'nw', 'se' ], 214 | 'nw-alt': [ 'nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e' ], 215 | 'ne-alt': [ 'ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w' ], 216 | 'sw-alt': [ 'sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e' ], 217 | 'se-alt': [ 'se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w' ] 218 | }; 219 | 220 | /** 221 | * Public API 222 | */ 223 | $.powerTip = { 224 | /** 225 | * Attempts to show the tooltip for the specified element. 226 | * @param {jQuery|Element} element The element to open the tooltip for. 227 | * @param {jQuery.Event=} event jQuery event for hover intent and mouse 228 | * tracking (optional). 229 | * @return {jQuery|Element} The original jQuery object or DOM Element. 230 | */ 231 | show: function apiShowTip(element, event) { 232 | // if we were given a mouse event then run the hover intent testing, 233 | // otherwise, simply show the tooltip asap 234 | if (isMouseEvent(event)) { 235 | trackMouse(event); 236 | session.previousX = event.pageX; 237 | session.previousY = event.pageY; 238 | $(element).data(DATA_DISPLAYCONTROLLER).show(); 239 | } else { 240 | $(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true); 241 | } 242 | return element; 243 | }, 244 | 245 | /** 246 | * Repositions the tooltip on the element. 247 | * @param {jQuery|Element} element The element the tooltip is shown for. 248 | * @return {jQuery|Element} The original jQuery object or DOM Element. 249 | */ 250 | reposition: function apiResetPosition(element) { 251 | $(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition(); 252 | return element; 253 | }, 254 | 255 | /** 256 | * Attempts to close any open tooltips. 257 | * @param {(jQuery|Element)=} element The element with the tooltip that 258 | * should be closed (optional). 259 | * @param {boolean=} immediate Disable close delay (optional). 260 | * @return {jQuery|Element|undefined} The original jQuery object or DOM 261 | * Element, if one was specified. 262 | */ 263 | hide: function apiCloseTip(element, immediate) { 264 | var displayController; 265 | 266 | // set immediate to true when no element is specified 267 | immediate = element ? immediate : true; 268 | 269 | // find the relevant display controller 270 | if (element) { 271 | displayController = $(element).first().data(DATA_DISPLAYCONTROLLER); 272 | } else if (session.activeHover) { 273 | displayController = session.activeHover.data(DATA_DISPLAYCONTROLLER); 274 | } 275 | 276 | // if found, hide the tip 277 | if (displayController) { 278 | displayController.hide(immediate); 279 | } 280 | 281 | return element; 282 | }, 283 | 284 | /** 285 | * Toggles the tooltip for the specified element. This will open a closed 286 | * tooltip, or close an open tooltip. 287 | * @param {jQuery|Element} element The element with the tooltip that 288 | * should be toggled. 289 | * @param {jQuery.Event=} event jQuery event for hover intent and mouse 290 | * tracking (optional). 291 | * @return {jQuery|Element} The original jQuery object or DOM Element. 292 | */ 293 | toggle: function apiToggle(element, event) { 294 | if (session.activeHover && session.activeHover.is(element)) { 295 | // tooltip for element is active, so close it 296 | $.powerTip.hide(element, !isMouseEvent(event)); 297 | } else { 298 | // tooltip for element is not active, so open it 299 | $.powerTip.show(element, event); 300 | } 301 | return element; 302 | }, 303 | 304 | /** 305 | * Destroy and roll back any powerTip() instance on the specified elements. 306 | * If no elements are specified then all elements that the plugin is 307 | * currently attached to will be rolled back. 308 | * @param {(jQuery|Element)=} element The element with the powerTip instance. 309 | * @return {jQuery|Element|undefined} The original jQuery object or DOM 310 | * Element, if one was specified. 311 | */ 312 | destroy: function apiDestroy(element) { 313 | var $element, 314 | foundPowerTip = false, 315 | runTipCheck = true, 316 | i; 317 | 318 | // if the plugin is not hooked to any elements then there is no point 319 | // trying to destroy anything, or dealing with the possible errors 320 | if (session.elements.length === 0) { 321 | return element; 322 | } 323 | 324 | if (element) { 325 | // make sure we're working with a jQuery object 326 | $element = $(element); 327 | } else { 328 | // if we are being asked to destroy all instances, then iterate the 329 | // array of jQuery objects that we've been tracking and call destroy 330 | // for each group 331 | $.each(session.elements, function cleanElsTracking(idx, els) { 332 | $.powerTip.destroy(els); 333 | }); 334 | 335 | // reset elements list 336 | // if a dev calls .remove() on an element before calling this 337 | // destroy() method then jQuery will have deleted all of the .data() 338 | // information, so it will not be recognized as a PowerTip element, 339 | // which could leave dangling references in this array - causing 340 | // future destroy() (no param) invocations to not fully clean up - 341 | // so make sure the array is empty and set a flag to skip the 342 | // element check before proceeding 343 | session.elements = []; 344 | runTipCheck = false; 345 | 346 | // set $element to an empty jQuery object to proceed 347 | $element = $(); 348 | } 349 | 350 | // check if PowerTip has been set on any of the elements - if PowerTip 351 | // has not been found then return early to skip the slow .not() 352 | // operation below - only if we did not reset the elements list above 353 | if (runTipCheck) { 354 | $element.each(function checkForPowerTip() { 355 | var $this = $(this); 356 | if ($this.data(DATA_DISPLAYCONTROLLER)) { 357 | foundPowerTip = true; 358 | return false; 359 | } 360 | return true; 361 | }); 362 | if (!foundPowerTip) { 363 | return element; 364 | } 365 | } 366 | 367 | // if a tooltip is currently open for an element we are being asked to 368 | // destroy then it should be forced to close 369 | if (session.isTipOpen && !session.isClosing && $element.filter(session.activeHover).length > 0) { 370 | // if the tooltip is waiting to close then cancel that delay timer 371 | if (session.delayInProgress) { 372 | session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel(); 373 | } 374 | // hide the tooltip, immediately 375 | $.powerTip.hide(session.activeHover, true); 376 | } 377 | 378 | // unhook events and destroy plugin changes to each element 379 | $element.off(EVENT_NAMESPACE).each(function destroy() { 380 | var $this = $(this), 381 | dataAttributes = [ 382 | DATA_ORIGINALTITLE, 383 | DATA_DISPLAYCONTROLLER, 384 | DATA_HASACTIVEHOVER, 385 | DATA_FORCEDOPEN 386 | ]; 387 | 388 | // revert title attribute 389 | if ($this.data(DATA_ORIGINALTITLE)) { 390 | $this.attr('title', $this.data(DATA_ORIGINALTITLE)); 391 | dataAttributes.push(DATA_POWERTIP); 392 | } 393 | 394 | // remove data attributes 395 | $this.removeData(dataAttributes); 396 | }); 397 | 398 | // remove destroyed element from active elements collection 399 | for (i = session.elements.length - 1; i >= 0; i--) { 400 | session.elements[i] = session.elements[i].not($element); 401 | 402 | // check if there are any more elements left in this collection, if 403 | // there is not then remove it from the elements array 404 | if (session.elements[i].length === 0) { 405 | session.elements.splice(i, 1); 406 | } 407 | } 408 | 409 | // if there are no active elements left then we will unhook all of the 410 | // events that we've bound code to and remove the tooltip elements 411 | if (session.elements.length === 0) { 412 | $window.off(EVENT_NAMESPACE); 413 | $document.off(EVENT_NAMESPACE); 414 | session.mouseTrackingActive = false; 415 | if (session.tooltips) { 416 | session.tooltips.remove(); 417 | session.tooltips = null; 418 | } 419 | } 420 | 421 | return element; 422 | } 423 | }; 424 | 425 | // API aliasing 426 | // for backwards compatibility with versions earlier than 1.2.0 427 | $.powerTip.showTip = $.powerTip.show; 428 | $.powerTip.closeTip = $.powerTip.hide; 429 | -------------------------------------------------------------------------------- /src/csscoordinates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip CSSCoordinates 3 | * 4 | * @fileoverview CSSCoordinates object for describing CSS positions. 5 | * @link https://stevenbenner.github.io/jquery-powertip/ 6 | * @author Steven Benner (https://stevenbenner.com/) 7 | * @requires jQuery 1.7+ 8 | */ 9 | 10 | /** 11 | * Creates a new CSSCoordinates object. 12 | * @private 13 | * @constructor 14 | */ 15 | function CSSCoordinates() { 16 | var me = this; 17 | 18 | // initialize object properties 19 | me.top = 'auto'; 20 | me.left = 'auto'; 21 | me.right = 'auto'; 22 | me.bottom = 'auto'; 23 | 24 | /** 25 | * Set a property to a value. 26 | * @private 27 | * @param {string} property The name of the property. 28 | * @param {number} value The value of the property. 29 | */ 30 | me.set = function(property, value) { 31 | if (typeof value === 'number') { 32 | me[property] = Math.round(value); 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/displaycontroller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip DisplayController 3 | * 4 | * @fileoverview DisplayController object used to manage tooltips for elements. 5 | * @link https://stevenbenner.github.io/jquery-powertip/ 6 | * @author Steven Benner (https://stevenbenner.com/) 7 | * @requires jQuery 1.7+ 8 | */ 9 | 10 | /** 11 | * Creates a new tooltip display controller. 12 | * @private 13 | * @constructor 14 | * @param {jQuery} element The element that this controller will handle. 15 | * @param {Object} options Options object containing settings. 16 | * @param {TooltipController} tipController The TooltipController object for 17 | * this instance. 18 | */ 19 | function DisplayController(element, options, tipController) { 20 | var hoverTimer = null, 21 | myCloseDelay = null; 22 | 23 | /** 24 | * Begins the process of showing a tooltip. 25 | * @private 26 | * @param {boolean=} immediate Skip intent testing (optional). 27 | * @param {boolean=} forceOpen Ignore cursor position and force tooltip to 28 | * open (optional). 29 | */ 30 | function openTooltip(immediate, forceOpen) { 31 | cancelTimer(); 32 | if (!element.data(DATA_HASACTIVEHOVER)) { 33 | if (!immediate) { 34 | session.tipOpenImminent = true; 35 | hoverTimer = setTimeout( 36 | function intentDelay() { 37 | hoverTimer = null; 38 | checkForIntent(); 39 | }, 40 | options.intentPollInterval 41 | ); 42 | } else { 43 | if (forceOpen) { 44 | element.data(DATA_FORCEDOPEN, true); 45 | } 46 | closeAnyDelayed(); 47 | tipController.showTip(element); 48 | } 49 | } else { 50 | // cursor left and returned to this element, cancel close 51 | cancelClose(); 52 | } 53 | } 54 | 55 | /** 56 | * Begins the process of closing a tooltip. 57 | * @private 58 | * @param {boolean=} disableDelay Disable close delay (optional). 59 | */ 60 | function closeTooltip(disableDelay) { 61 | // if this instance already has a close delay in progress then halt it 62 | if (myCloseDelay) { 63 | session.closeDelayTimeout = clearTimeout(myCloseDelay); 64 | myCloseDelay = session.closeDelayTimeout; 65 | session.delayInProgress = false; 66 | } 67 | cancelTimer(); 68 | session.tipOpenImminent = false; 69 | if (element.data(DATA_HASACTIVEHOVER)) { 70 | element.data(DATA_FORCEDOPEN, false); 71 | if (!disableDelay) { 72 | session.delayInProgress = true; 73 | session.closeDelayTimeout = setTimeout( 74 | function closeDelay() { 75 | session.closeDelayTimeout = null; 76 | tipController.hideTip(element); 77 | session.delayInProgress = false; 78 | myCloseDelay = null; 79 | }, 80 | options.closeDelay 81 | ); 82 | // save internal reference close delay id so we can check if the 83 | // active close delay belongs to this instance 84 | myCloseDelay = session.closeDelayTimeout; 85 | } else { 86 | tipController.hideTip(element); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Checks mouse position to make sure that the user intended to hover on the 93 | * specified element before showing the tooltip. 94 | * @private 95 | */ 96 | function checkForIntent() { 97 | // calculate mouse position difference 98 | var xDifference = Math.abs(session.previousX - session.currentX), 99 | yDifference = Math.abs(session.previousY - session.currentY), 100 | totalDifference = xDifference + yDifference; 101 | 102 | // check if difference has passed the sensitivity threshold 103 | if (totalDifference < options.intentSensitivity) { 104 | cancelClose(); 105 | closeAnyDelayed(); 106 | tipController.showTip(element); 107 | } else { 108 | // try again 109 | session.previousX = session.currentX; 110 | session.previousY = session.currentY; 111 | openTooltip(); 112 | } 113 | } 114 | 115 | /** 116 | * Cancels active hover timer. 117 | * @private 118 | * @param {boolean=} stopClose Cancel any active close delay timer. 119 | */ 120 | function cancelTimer(stopClose) { 121 | hoverTimer = clearTimeout(hoverTimer); 122 | // cancel the current close delay if the active close delay is for this 123 | // element or the stopClose argument is true 124 | if (session.closeDelayTimeout && myCloseDelay === session.closeDelayTimeout || stopClose) { 125 | cancelClose(); 126 | } 127 | } 128 | 129 | /** 130 | * Cancels any active close delay timer. 131 | * @private 132 | */ 133 | function cancelClose() { 134 | session.closeDelayTimeout = clearTimeout(session.closeDelayTimeout); 135 | session.delayInProgress = false; 136 | } 137 | 138 | /** 139 | * Asks any tooltips waiting on their close delay to close now. 140 | * @private 141 | */ 142 | function closeAnyDelayed() { 143 | // if another element is waiting for its close delay then we should ask 144 | // it to close immediately so we can proceed without unexpected timeout 145 | // code being run during this tooltip's lifecycle 146 | if (session.delayInProgress && session.activeHover && !session.activeHover.is(element)) { 147 | session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true); 148 | } 149 | } 150 | 151 | /** 152 | * Repositions the tooltip on this element. 153 | * @private 154 | */ 155 | function repositionTooltip() { 156 | tipController.resetPosition(element); 157 | } 158 | 159 | // expose the methods 160 | this.show = openTooltip; 161 | this.hide = closeTooltip; 162 | this.cancel = cancelTimer; 163 | this.resetPosition = repositionTooltip; 164 | } 165 | -------------------------------------------------------------------------------- /src/placementcalculator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip PlacementCalculator 3 | * 4 | * @fileoverview PlacementCalculator object that computes tooltip position. 5 | * @link https://stevenbenner.github.io/jquery-powertip/ 6 | * @author Steven Benner (https://stevenbenner.com/) 7 | * @requires jQuery 1.7+ 8 | */ 9 | 10 | /** 11 | * Creates a new Placement Calculator. 12 | * @private 13 | * @constructor 14 | */ 15 | function PlacementCalculator() { 16 | /** 17 | * Compute the CSS position to display a tooltip at the specified placement 18 | * relative to the specified element. 19 | * @private 20 | * @param {jQuery} element The element that the tooltip should target. 21 | * @param {string} placement The placement for the tooltip. 22 | * @param {number} tipWidth Width of the tooltip element in pixels. 23 | * @param {number} tipHeight Height of the tooltip element in pixels. 24 | * @param {number} offset Distance to offset tooltips in pixels. 25 | * @return {CSSCoordinates} A CSSCoordinates object with the position. 26 | */ 27 | function computePlacementCoords(element, placement, tipWidth, tipHeight, offset) { 28 | var placementBase = placement.split('-')[0], // ignore 'alt' for corners 29 | coords = new CSSCoordinates(), 30 | position; 31 | 32 | if (isSvgElement(element)) { 33 | position = getSvgPlacement(element, placementBase); 34 | } else { 35 | position = getHtmlPlacement(element, placementBase); 36 | } 37 | 38 | // calculate the appropriate x and y position in the document 39 | switch (placement) { 40 | case 'n': 41 | coords.set('left', position.left - (tipWidth / 2)); 42 | coords.set('bottom', session.windowHeight - position.top + offset); 43 | break; 44 | case 'e': 45 | coords.set('left', position.left + offset); 46 | coords.set('top', position.top - (tipHeight / 2)); 47 | break; 48 | case 's': 49 | coords.set('left', position.left - (tipWidth / 2)); 50 | coords.set('top', position.top + offset); 51 | break; 52 | case 'w': 53 | coords.set('top', position.top - (tipHeight / 2)); 54 | coords.set('right', session.windowWidth - position.left + offset); 55 | break; 56 | case 'nw': 57 | coords.set('bottom', session.windowHeight - position.top + offset); 58 | coords.set('right', session.windowWidth - position.left - 20); 59 | break; 60 | case 'nw-alt': 61 | coords.set('left', position.left); 62 | coords.set('bottom', session.windowHeight - position.top + offset); 63 | break; 64 | case 'ne': 65 | coords.set('left', position.left - 20); 66 | coords.set('bottom', session.windowHeight - position.top + offset); 67 | break; 68 | case 'ne-alt': 69 | coords.set('bottom', session.windowHeight - position.top + offset); 70 | coords.set('right', session.windowWidth - position.left); 71 | break; 72 | case 'sw': 73 | coords.set('top', position.top + offset); 74 | coords.set('right', session.windowWidth - position.left - 20); 75 | break; 76 | case 'sw-alt': 77 | coords.set('left', position.left); 78 | coords.set('top', position.top + offset); 79 | break; 80 | case 'se': 81 | coords.set('left', position.left - 20); 82 | coords.set('top', position.top + offset); 83 | break; 84 | case 'se-alt': 85 | coords.set('top', position.top + offset); 86 | coords.set('right', session.windowWidth - position.left); 87 | break; 88 | } 89 | 90 | return coords; 91 | } 92 | 93 | /** 94 | * Finds the tooltip attachment point in the document for a HTML DOM element 95 | * for the specified placement. 96 | * @private 97 | * @param {jQuery} element The element that the tooltip should target. 98 | * @param {string} placement The placement for the tooltip. 99 | * @return {Object} An object with the top,left position values. 100 | */ 101 | function getHtmlPlacement(element, placement) { 102 | var objectOffset = element.offset(), 103 | objectWidth = element.outerWidth(), 104 | objectHeight = element.outerHeight(), 105 | left, 106 | top; 107 | 108 | // calculate the appropriate x and y position in the document 109 | switch (placement) { 110 | case 'n': 111 | left = objectOffset.left + objectWidth / 2; 112 | top = objectOffset.top; 113 | break; 114 | case 'e': 115 | left = objectOffset.left + objectWidth; 116 | top = objectOffset.top + objectHeight / 2; 117 | break; 118 | case 's': 119 | left = objectOffset.left + objectWidth / 2; 120 | top = objectOffset.top + objectHeight; 121 | break; 122 | case 'w': 123 | left = objectOffset.left; 124 | top = objectOffset.top + objectHeight / 2; 125 | break; 126 | case 'nw': 127 | left = objectOffset.left; 128 | top = objectOffset.top; 129 | break; 130 | case 'ne': 131 | left = objectOffset.left + objectWidth; 132 | top = objectOffset.top; 133 | break; 134 | case 'sw': 135 | left = objectOffset.left; 136 | top = objectOffset.top + objectHeight; 137 | break; 138 | case 'se': 139 | left = objectOffset.left + objectWidth; 140 | top = objectOffset.top + objectHeight; 141 | break; 142 | } 143 | 144 | return { 145 | top: top, 146 | left: left 147 | }; 148 | } 149 | 150 | /** 151 | * Finds the tooltip attachment point in the document for a SVG element for 152 | * the specified placement. 153 | * @private 154 | * @param {jQuery} element The element that the tooltip should target. 155 | * @param {string} placement The placement for the tooltip. 156 | * @return {Object} An object with the top,left position values. 157 | */ 158 | function getSvgPlacement(element, placement) { 159 | var svgElement = element.closest('svg')[0], 160 | domElement = element[0], 161 | point = svgElement.createSVGPoint(), 162 | boundingBox = domElement.getBBox(), 163 | matrix = domElement.getScreenCTM(), 164 | halfWidth = boundingBox.width / 2, 165 | halfHeight = boundingBox.height / 2, 166 | placements = [], 167 | placementKeys = [ 'nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w' ], 168 | coords, 169 | rotation, 170 | steps, 171 | x; 172 | 173 | /** 174 | * Transform and append the current points to the placements list. 175 | * @private 176 | */ 177 | function pushPlacement() { 178 | placements.push(point.matrixTransform(matrix)); 179 | } 180 | 181 | // get bounding box corners and midpoints 182 | point.x = boundingBox.x; 183 | point.y = boundingBox.y; 184 | pushPlacement(); 185 | point.x += halfWidth; 186 | pushPlacement(); 187 | point.x += halfWidth; 188 | pushPlacement(); 189 | point.y += halfHeight; 190 | pushPlacement(); 191 | point.y += halfHeight; 192 | pushPlacement(); 193 | point.x -= halfWidth; 194 | pushPlacement(); 195 | point.x -= halfWidth; 196 | pushPlacement(); 197 | point.y -= halfHeight; 198 | pushPlacement(); 199 | 200 | // determine rotation 201 | if (placements[0].y !== placements[1].y || placements[0].x !== placements[7].x) { 202 | rotation = Math.atan2(matrix.b, matrix.a) * RAD2DEG; 203 | steps = Math.ceil(((rotation % 360) - 22.5) / 45); 204 | if (steps < 1) { 205 | steps += 8; 206 | } 207 | while (steps--) { 208 | placementKeys.push(placementKeys.shift()); 209 | } 210 | } 211 | 212 | // find placement 213 | for (x = 0; x < placements.length; x++) { 214 | if (placementKeys[x] === placement) { 215 | coords = placements[x]; 216 | break; 217 | } 218 | } 219 | 220 | return { 221 | top: coords.y + session.scrollTop, 222 | left: coords.x + session.scrollLeft 223 | }; 224 | } 225 | 226 | // expose methods 227 | this.compute = computePlacementCoords; 228 | } 229 | -------------------------------------------------------------------------------- /src/tooltipcontroller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip TooltipController 3 | * 4 | * @fileoverview TooltipController object that manages tips for an instance. 5 | * @link https://stevenbenner.github.io/jquery-powertip/ 6 | * @author Steven Benner (https://stevenbenner.com/) 7 | * @requires jQuery 1.7+ 8 | */ 9 | 10 | /** 11 | * Creates a new tooltip controller. 12 | * @private 13 | * @constructor 14 | * @param {Object} options Options object containing settings. 15 | */ 16 | function TooltipController(options) { 17 | var placementCalculator = new PlacementCalculator(), 18 | tipElement = $('#' + options.popupId); 19 | 20 | // build and append tooltip div if it does not already exist 21 | if (tipElement.length === 0) { 22 | tipElement = $('
', { id: options.popupId }); 23 | // grab body element if it was not populated when the script loaded 24 | // note: this hack exists solely for jsfiddle support 25 | if ($body.length === 0) { 26 | $body = $('body'); 27 | } 28 | $body.append(tipElement); 29 | // remember the tooltip elements that the plugin has created 30 | session.tooltips = session.tooltips ? session.tooltips.add(tipElement) : tipElement; 31 | } 32 | 33 | // hook mousemove for cursor follow tooltips 34 | if (options.followMouse) { 35 | // only one positionTipOnCursor hook per tooltip element, please 36 | if (!tipElement.data(DATA_HASMOUSEMOVE)) { 37 | $document.on('mousemove' + EVENT_NAMESPACE, positionTipOnCursor); 38 | $window.on('scroll' + EVENT_NAMESPACE, positionTipOnCursor); 39 | tipElement.data(DATA_HASMOUSEMOVE, true); 40 | } 41 | } 42 | 43 | /** 44 | * Gives the specified element the active-hover state and queues up the 45 | * showTip function. 46 | * @private 47 | * @param {jQuery} element The element that the tooltip should target. 48 | */ 49 | function beginShowTip(element) { 50 | element.data(DATA_HASACTIVEHOVER, true); 51 | // show tooltip, asap 52 | tipElement.queue(function queueTipInit(next) { 53 | showTip(element); 54 | next(); 55 | }); 56 | } 57 | 58 | /** 59 | * Shows the tooltip, as soon as possible. 60 | * @private 61 | * @param {jQuery} element The element that the tooltip should target. 62 | */ 63 | function showTip(element) { 64 | var tipContent; 65 | 66 | // it is possible, especially with keyboard navigation, to move on to 67 | // another element with a tooltip during the queue to get to this point 68 | // in the code. if that happens then we need to not proceed or we may 69 | // have the fadeout callback for the last tooltip execute immediately 70 | // after this code runs, causing bugs. 71 | if (!element.data(DATA_HASACTIVEHOVER)) { 72 | return; 73 | } 74 | 75 | // if the tooltip is open and we got asked to open another one then the 76 | // old one is still in its fadeOut cycle, so wait and try again 77 | if (session.isTipOpen) { 78 | if (!session.isClosing) { 79 | hideTip(session.activeHover); 80 | } 81 | tipElement.delay(100).queue(function queueTipAgain(next) { 82 | showTip(element); 83 | next(); 84 | }); 85 | return; 86 | } 87 | 88 | // trigger powerTipPreRender event 89 | element.trigger('powerTipPreRender'); 90 | 91 | // set tooltip content 92 | tipContent = getTooltipContent(element); 93 | if (tipContent) { 94 | tipElement.empty().append(tipContent); 95 | } else { 96 | // we have no content to display, give up 97 | return; 98 | } 99 | 100 | // trigger powerTipRender event 101 | element.trigger('powerTipRender'); 102 | 103 | session.activeHover = element; 104 | session.isTipOpen = true; 105 | 106 | tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup); 107 | 108 | // add custom class to tooltip element 109 | tipElement.addClass(options.popupClass); 110 | 111 | // set tooltip position 112 | // revert to static placement when the "force open" flag was set because 113 | // that flag means that we do not have accurate mouse position info 114 | if (!options.followMouse || element.data(DATA_FORCEDOPEN)) { 115 | positionTipOnElement(element); 116 | session.isFixedTipOpen = true; 117 | } else { 118 | positionTipOnCursor(); 119 | } 120 | 121 | // close tooltip when clicking anywhere on the page, with the exception 122 | // of the tooltip's trigger element and any elements that are within a 123 | // tooltip that has 'mouseOnToPopup' option enabled 124 | // always enable this feature when the "force open" flag is set on a 125 | // followMouse tooltip because we reverted to static placement above 126 | if (!element.data(DATA_FORCEDOPEN) && !options.followMouse) { 127 | $document.on('click' + EVENT_NAMESPACE, function documentClick(event) { 128 | var target = event.target; 129 | if (target !== element[0]) { 130 | if (options.mouseOnToPopup) { 131 | if (target !== tipElement[0] && !$.contains(tipElement[0], target)) { 132 | $.powerTip.hide(); 133 | } 134 | } else { 135 | $.powerTip.hide(); 136 | } 137 | } 138 | }); 139 | } 140 | 141 | // if we want to be able to mouse on to the tooltip then we need to 142 | // attach hover events to the tooltip that will cancel a close request 143 | // on mouseenter and start a new close request on mouseleave 144 | // only hook these listeners if we're not in manual mode 145 | if (options.mouseOnToPopup && !options.manual && $.inArray('mouseleave', options.closeEvents) > -1) { 146 | tipElement.on('mouseenter' + EVENT_NAMESPACE, function tipMouseEnter() { 147 | // check activeHover in case the mouse cursor entered the 148 | // tooltip during the fadeOut and close cycle 149 | if (session.activeHover) { 150 | session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel(); 151 | } 152 | }); 153 | tipElement.on('mouseleave' + EVENT_NAMESPACE, function tipMouseLeave() { 154 | // check activeHover in case the mouse cursor left the tooltip 155 | // during the fadeOut and close cycle 156 | if (session.activeHover) { 157 | session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(); 158 | } 159 | }); 160 | } 161 | 162 | // fadein 163 | tipElement.fadeIn(options.fadeInTime, function fadeInCallback() { 164 | // start desync polling 165 | if (!session.desyncTimeout) { 166 | session.desyncTimeout = setInterval(closeDesyncedTip, 500); 167 | } 168 | 169 | // trigger powerTipOpen event 170 | element.trigger('powerTipOpen'); 171 | }); 172 | } 173 | 174 | /** 175 | * Hides the tooltip. 176 | * @private 177 | * @param {jQuery} element The element that the tooltip should target. 178 | */ 179 | function hideTip(element) { 180 | // reset session 181 | session.isClosing = true; 182 | session.isTipOpen = false; 183 | 184 | // stop desync polling 185 | session.desyncTimeout = clearInterval(session.desyncTimeout); 186 | 187 | // reset element state 188 | element.data(DATA_HASACTIVEHOVER, false); 189 | element.data(DATA_FORCEDOPEN, false); 190 | 191 | // remove document click handler 192 | $document.off('click' + EVENT_NAMESPACE); 193 | 194 | // unbind the mouseOnToPopup events if they were set 195 | tipElement.off(EVENT_NAMESPACE); 196 | 197 | // fade out 198 | tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() { 199 | var coords = new CSSCoordinates(); 200 | 201 | // reset session and tooltip element 202 | session.activeHover = null; 203 | session.isClosing = false; 204 | session.isFixedTipOpen = false; 205 | tipElement.removeClass(); 206 | 207 | // support mouse-follow and fixed position tips at the same time by 208 | // moving the tooltip to the last cursor location after it is hidden 209 | coords.set('top', session.currentY + options.offset); 210 | coords.set('left', session.currentX + options.offset); 211 | tipElement.css(coords); 212 | 213 | // trigger powerTipClose event 214 | element.trigger('powerTipClose'); 215 | }); 216 | } 217 | 218 | /** 219 | * Moves the tooltip to the user's mouse cursor. 220 | * @private 221 | */ 222 | function positionTipOnCursor() { 223 | var tipWidth, 224 | tipHeight, 225 | coords, 226 | collisions, 227 | collisionCount; 228 | 229 | // to support having fixed tooltips on the same page as cursor tooltips, 230 | // where both instances are referencing the same tooltip element, we 231 | // need to keep track of the mouse position constantly, but we should 232 | // only set the tip location if a fixed tip is not currently open, a tip 233 | // open is imminent or active, and the tooltip element in question does 234 | // have a mouse-follow using it. 235 | if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) { 236 | // grab measurements 237 | tipWidth = tipElement.outerWidth(); 238 | tipHeight = tipElement.outerHeight(); 239 | coords = new CSSCoordinates(); 240 | 241 | // grab collisions 242 | coords.set('top', session.currentY + options.offset); 243 | coords.set('left', session.currentX + options.offset); 244 | collisions = getViewportCollisions( 245 | coords, 246 | tipWidth, 247 | tipHeight 248 | ); 249 | 250 | // handle tooltip view port collisions 251 | if (collisions !== Collision.none) { 252 | collisionCount = countFlags(collisions); 253 | if (collisionCount === 1) { 254 | // if there is only one collision (bottom or right) then 255 | // simply constrain the tooltip to the view port 256 | if (collisions === Collision.right) { 257 | coords.set('left', session.scrollLeft + session.windowWidth - tipWidth); 258 | } else if (collisions === Collision.bottom) { 259 | coords.set('top', session.scrollTop + session.windowHeight - tipHeight); 260 | } 261 | } else { 262 | // if the tooltip has more than one collision then it is 263 | // trapped in the corner and should be flipped to get it out 264 | // of the user's way 265 | coords.set('left', session.currentX - tipWidth - options.offset); 266 | coords.set('top', session.currentY - tipHeight - options.offset); 267 | } 268 | } 269 | 270 | // position the tooltip 271 | tipElement.css(coords); 272 | } 273 | } 274 | 275 | /** 276 | * Sets the tooltip to the correct position relative to the specified target 277 | * element. Based on options settings. 278 | * @private 279 | * @param {jQuery} element The element that the tooltip should target. 280 | */ 281 | function positionTipOnElement(element) { 282 | var priorityList, 283 | finalPlacement; 284 | 285 | // when the followMouse option is enabled and the "force open" flag is 286 | // set we revert to static positioning. since the developer may not have 287 | // considered this scenario we should use smart placement 288 | if (options.smartPlacement || (options.followMouse && element.data(DATA_FORCEDOPEN))) { 289 | priorityList = $.fn.powerTip.smartPlacementLists[options.placement]; 290 | 291 | // iterate over the priority list and use the first placement option 292 | // that does not collide with the view port. if they all collide 293 | // then the last placement in the list will be used. 294 | $.each(priorityList, function(idx, pos) { 295 | // place tooltip and find collisions 296 | var collisions = getViewportCollisions( 297 | placeTooltip(element, pos), 298 | tipElement.outerWidth(), 299 | tipElement.outerHeight() 300 | ); 301 | 302 | // update the final placement variable 303 | finalPlacement = pos; 304 | 305 | // break if there were no collisions 306 | return collisions !== Collision.none; 307 | }); 308 | } else { 309 | // if we're not going to use the smart placement feature then just 310 | // compute the coordinates and do it 311 | placeTooltip(element, options.placement); 312 | finalPlacement = options.placement; 313 | } 314 | 315 | // add placement as class for CSS arrows 316 | tipElement.removeClass('w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt'); 317 | tipElement.addClass(finalPlacement); 318 | } 319 | 320 | /** 321 | * Sets the tooltip position to the appropriate values to show the tip at 322 | * the specified placement. This function will iterate and test the tooltip 323 | * to support elastic tooltips. 324 | * @private 325 | * @param {jQuery} element The element that the tooltip should target. 326 | * @param {string} placement The placement for the tooltip. 327 | * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and 328 | * right position values. 329 | */ 330 | function placeTooltip(element, placement) { 331 | var iterationCount = 0, 332 | tipWidth, 333 | tipHeight, 334 | coords = new CSSCoordinates(); 335 | 336 | // set the tip to 0,0 to get the full expanded width 337 | coords.set('top', 0); 338 | coords.set('left', 0); 339 | tipElement.css(coords); 340 | 341 | // to support elastic tooltips we need to check for a change in the 342 | // rendered dimensions after the tooltip has been positioned 343 | do { 344 | // grab the current tip dimensions 345 | tipWidth = tipElement.outerWidth(); 346 | tipHeight = tipElement.outerHeight(); 347 | 348 | // get placement coordinates 349 | coords = placementCalculator.compute( 350 | element, 351 | placement, 352 | tipWidth, 353 | tipHeight, 354 | options.offset 355 | ); 356 | 357 | // place the tooltip 358 | tipElement.css(coords); 359 | } while ( 360 | // sanity check: limit to 5 iterations, and... 361 | ++iterationCount <= 5 && 362 | // try again if the dimensions changed after placement 363 | (tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight()) 364 | ); 365 | 366 | return coords; 367 | } 368 | 369 | /** 370 | * Checks for a tooltip desync and closes the tooltip if one occurs. 371 | * @private 372 | */ 373 | function closeDesyncedTip() { 374 | var isDesynced = false, 375 | hasDesyncableCloseEvent = $.grep( 376 | [ 'mouseleave', 'mouseout', 'blur', 'focusout' ], 377 | function(eventType) { 378 | return $.inArray(eventType, options.closeEvents) !== -1; 379 | } 380 | ).length > 0; 381 | 382 | // It is possible for the mouse cursor to leave an element without 383 | // firing the mouseleave or blur event. This most commonly happens when 384 | // the element is disabled under mouse cursor. If this happens it will 385 | // result in a desynced tooltip because the tooltip was never asked to 386 | // close. So we should periodically check for a desync situation and 387 | // close the tip if such a situation arises. 388 | if (session.isTipOpen && !session.isClosing && !session.delayInProgress && hasDesyncableCloseEvent) { 389 | if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) { 390 | // user moused onto another tip or active hover is disabled 391 | isDesynced = true; 392 | } else if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) { 393 | // hanging tip - have to test if mouse position is not over the 394 | // active hover and not over a tooltip set to let the user 395 | // interact with it. 396 | // for keyboard navigation: this only counts if the element does 397 | // not have focus. 398 | // for tooltips opened via the api: we need to check if it has 399 | // the forcedOpen flag. 400 | if (tipElement.data(DATA_MOUSEONTOTIP)) { 401 | if (!isMouseOver(tipElement)) { 402 | isDesynced = true; 403 | } 404 | } else { 405 | isDesynced = true; 406 | } 407 | } 408 | 409 | if (isDesynced) { 410 | // close the desynced tip 411 | hideTip(session.activeHover); 412 | } 413 | } 414 | } 415 | 416 | // expose methods 417 | this.showTip = beginShowTip; 418 | this.hideTip = hideTip; 419 | this.resetPosition = positionTipOnElement; 420 | } 421 | -------------------------------------------------------------------------------- /src/utility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PowerTip Utility Functions 3 | * 4 | * @fileoverview Private helper functions. 5 | * @link https://stevenbenner.github.io/jquery-powertip/ 6 | * @author Steven Benner (https://stevenbenner.com/) 7 | * @requires jQuery 1.7+ 8 | */ 9 | 10 | /** 11 | * Determine whether a jQuery object is an SVG element 12 | * @private 13 | * @param {jQuery} element The element to check 14 | * @return {boolean} Whether this is an SVG element 15 | */ 16 | function isSvgElement(element) { 17 | return Boolean(window.SVGElement && element[0] instanceof SVGElement); 18 | } 19 | 20 | /** 21 | * Determines if the specified jQuery.Event object has mouse data. 22 | * @private 23 | * @param {jQuery.Event=} event The jQuery.Event object to test. 24 | * @return {boolean} True if there is mouse data, otherwise false. 25 | */ 26 | function isMouseEvent(event) { 27 | return Boolean(event && $.inArray(event.type, MOUSE_EVENTS) > -1 && 28 | typeof event.pageX === 'number'); 29 | } 30 | 31 | /** 32 | * Initializes the viewport dimension cache and hooks up the mouse position 33 | * tracking and viewport dimension tracking events. 34 | * Prevents attaching the events more than once. 35 | * @private 36 | */ 37 | function initTracking() { 38 | if (!session.mouseTrackingActive) { 39 | session.mouseTrackingActive = true; 40 | 41 | // grab the current viewport dimensions on load 42 | getViewportDimensions(); 43 | $(getViewportDimensions); 44 | 45 | // hook mouse move tracking 46 | $document.on('mousemove' + EVENT_NAMESPACE, trackMouse); 47 | 48 | // hook viewport dimensions tracking 49 | $window.on('resize' + EVENT_NAMESPACE, trackResize); 50 | $window.on('scroll' + EVENT_NAMESPACE, trackScroll); 51 | } 52 | } 53 | 54 | /** 55 | * Updates the viewport dimensions cache. 56 | * @private 57 | */ 58 | function getViewportDimensions() { 59 | session.scrollLeft = $window.scrollLeft(); 60 | session.scrollTop = $window.scrollTop(); 61 | session.windowWidth = $window.width(); 62 | session.windowHeight = $window.height(); 63 | } 64 | 65 | /** 66 | * Updates the window size info in the viewport dimensions cache. 67 | * @private 68 | */ 69 | function trackResize() { 70 | session.windowWidth = $window.width(); 71 | session.windowHeight = $window.height(); 72 | } 73 | 74 | /** 75 | * Updates the scroll offset info in the viewport dimensions cache. 76 | * @private 77 | */ 78 | function trackScroll() { 79 | var x = $window.scrollLeft(), 80 | y = $window.scrollTop(); 81 | if (x !== session.scrollLeft) { 82 | session.currentX += x - session.scrollLeft; 83 | session.scrollLeft = x; 84 | } 85 | if (y !== session.scrollTop) { 86 | session.currentY += y - session.scrollTop; 87 | session.scrollTop = y; 88 | } 89 | } 90 | 91 | /** 92 | * Saves the current mouse coordinates to the session object. 93 | * @private 94 | * @param {jQuery.Event} event The mousemove event for the document. 95 | */ 96 | function trackMouse(event) { 97 | session.currentX = event.pageX; 98 | session.currentY = event.pageY; 99 | } 100 | 101 | /** 102 | * Tests if the mouse is currently over the specified element. 103 | * @private 104 | * @param {jQuery} element The element to check for hover. 105 | * @return {boolean} True if the mouse is over the element, otherwise false. 106 | */ 107 | function isMouseOver(element) { 108 | // use getBoundingClientRect() because jQuery's width() and height() 109 | // methods do not work with SVG elements 110 | // compute width/height because those properties do not exist on the object 111 | // returned by getBoundingClientRect() in older versions of IE 112 | var elementPosition = element.offset(), 113 | elementBox = element[0].getBoundingClientRect(), 114 | elementWidth = elementBox.right - elementBox.left, 115 | elementHeight = elementBox.bottom - elementBox.top; 116 | 117 | return session.currentX >= elementPosition.left && 118 | session.currentX <= elementPosition.left + elementWidth && 119 | session.currentY >= elementPosition.top && 120 | session.currentY <= elementPosition.top + elementHeight; 121 | } 122 | 123 | /** 124 | * Fetches the tooltip content from the specified element's data attributes. 125 | * @private 126 | * @param {jQuery} element The element to get the tooltip content for. 127 | * @return {(string|jQuery|undefined)} The text/HTML string, jQuery object, or 128 | * undefined if there was no tooltip content for the element. 129 | */ 130 | function getTooltipContent(element) { 131 | var tipText = element.data(DATA_POWERTIP), 132 | tipObject = element.data(DATA_POWERTIPJQ), 133 | tipTarget = element.data(DATA_POWERTIPTARGET), 134 | targetElement, 135 | content; 136 | 137 | if (tipText) { 138 | if (typeof tipText === 'function') { 139 | tipText = tipText.call(element[0]); 140 | } 141 | content = tipText; 142 | } else if (tipObject) { 143 | if (typeof tipObject === 'function') { 144 | tipObject = tipObject.call(element[0]); 145 | } 146 | if (tipObject.length > 0) { 147 | content = tipObject.clone(true, true); 148 | } 149 | } else if (tipTarget) { 150 | targetElement = $('#' + tipTarget); 151 | if (targetElement.length > 0) { 152 | content = targetElement.html(); 153 | } 154 | } 155 | 156 | return content; 157 | } 158 | 159 | /** 160 | * Finds any viewport collisions that an element (the tooltip) would have if it 161 | * were absolutely positioned at the specified coordinates. 162 | * @private 163 | * @param {CSSCoordinates} coords Coordinates for the element. 164 | * @param {number} elementWidth Width of the element in pixels. 165 | * @param {number} elementHeight Height of the element in pixels. 166 | * @return {number} Value with the collision flags. 167 | */ 168 | function getViewportCollisions(coords, elementWidth, elementHeight) { 169 | var viewportTop = session.scrollTop, 170 | viewportLeft = session.scrollLeft, 171 | viewportBottom = viewportTop + session.windowHeight, 172 | viewportRight = viewportLeft + session.windowWidth, 173 | collisions = Collision.none; 174 | 175 | if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) { 176 | collisions |= Collision.top; 177 | } 178 | if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) { 179 | collisions |= Collision.bottom; 180 | } 181 | if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) { 182 | collisions |= Collision.left; 183 | } 184 | if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) { 185 | collisions |= Collision.right; 186 | } 187 | 188 | return collisions; 189 | } 190 | 191 | /** 192 | * Counts the number of bits set on a flags value. 193 | * @param {number} value The flags value. 194 | * @return {number} The number of bits that have been set. 195 | */ 196 | function countFlags(value) { 197 | var count = 0; 198 | while (value) { 199 | value &= value - 1; 200 | count++; 201 | } 202 | return count; 203 | } 204 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | /*! 2 | <%= pkg.title %> v<%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd") %>) 3 | <%= pkg.homepage %> 4 | Copyright (c) 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %> (<%= pkg.author.url %>). 5 | Released under <%= pkg.license %> license. 6 | https://github.com/stevenbenner/jquery-powertip/blob/master/<%= files.license %> 7 | */ 8 | (function(root, factory) { 9 | // support loading the plugin via common patterns 10 | if (typeof define === 'function' && define.amd) { 11 | // load the plugin as an amd module 12 | define([ 'jquery' ], factory); 13 | } else if (typeof module === 'object' && module.exports) { 14 | // load the plugin as a commonjs module 15 | module.exports = factory(require('jquery')); 16 | } else { 17 | // load the plugin as a global 18 | factory(root.jQuery); 19 | } 20 | }(this, function($) { 21 | /* [POWERTIP CODE] */ 22 | // return api for commonjs and amd environments 23 | return $.powerTip; 24 | })); 25 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 3 4 | }, 5 | "env": { 6 | "browser": true, 7 | "commonjs": true, 8 | "jquery": true, 9 | "qunit": true, 10 | "node": false 11 | }, 12 | "rules": { 13 | "no-prototype-builtins": "off", 14 | "no-var": "off", 15 | "prefer-template": "off" 16 | }, 17 | "globals": { 18 | "DATA_DISPLAYCONTROLLER": false, 19 | "DATA_HASACTIVEHOVER": false, 20 | "DATA_POWERTIP": false, 21 | "DATA_POWERTIPJQ": false, 22 | "DATA_POWERTIPTARGET": false, 23 | "DATA_FORCEDOPEN": false, 24 | "session": true, 25 | "Collision": false, 26 | 27 | "CSSCoordinates": false, 28 | "DisplayController": false, 29 | "PlacementCalculator": false, 30 | "TooltipController": false, 31 | 32 | "isSvgElement": false, 33 | "isMouseEvent": false, 34 | "initTracking": false, 35 | "trackMouse": false, 36 | "isMouseOver": false, 37 | "getTooltipContent": false, 38 | "getViewportCollisions": false, 39 | "countFlags": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/amd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PowerTip AMD Test Suite 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /test/amd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require.config({ 4 | paths: { 5 | jquery: 'https://code.jquery.com/jquery-3.7.1', 6 | qunit: 'https://code.jquery.com/qunit/qunit-2.20.0', 7 | 'jquery.powertip': '../dist/jquery.powertip' 8 | } 9 | }); 10 | 11 | require([ 'jquery', 'qunit', 'jquery.powertip' ], function($, QUnit, powerTip) { 12 | QUnit.start(); 13 | 14 | QUnit.module('AMD'); 15 | 16 | QUnit.test('powerTip is loaded and available via AMD', function(assert) { 17 | var element = $(''); 18 | assert.strictEqual(typeof element.powerTip, 'function', 'powerTip is defined'); 19 | }); 20 | 21 | QUnit.test('expose API via jQuery', function(assert) { 22 | assert.strictEqual(typeof $.powerTip.show, 'function', 'show is defined'); 23 | assert.strictEqual(typeof $.powerTip.reposition, 'function', 'reposition is defined'); 24 | assert.strictEqual(typeof $.powerTip.hide, 'function', 'hide is defined'); 25 | assert.strictEqual(typeof $.powerTip.toggle, 'function', 'toggle is defined'); 26 | assert.strictEqual(typeof $.powerTip.destroy, 'function', 'destroy is defined'); 27 | }); 28 | 29 | QUnit.test('expose API via AMD parameter', function(assert) { 30 | assert.strictEqual(typeof powerTip.show, 'function', 'show is defined'); 31 | assert.strictEqual(typeof powerTip.reposition, 'function', 'reposition is defined'); 32 | assert.strictEqual(typeof powerTip.hide, 'function', 'hide is defined'); 33 | assert.strictEqual(typeof powerTip.toggle, 'function', 'toggle is defined'); 34 | assert.strictEqual(typeof powerTip.destroy, 'function', 'destroy is defined'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/browserify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PowerTip Browserify Test Suite 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /test/browserify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function runTests() { 4 | var QUnit = require('qunit'), 5 | $ = require('jquery'), 6 | powerTip = require('./../dist/jquery.powertip.js'); 7 | 8 | QUnit.module('Browserify'); 9 | 10 | QUnit.test('powerTip is loaded into $.fn', function(assert) { 11 | var element = $(''); 12 | assert.strictEqual(typeof element.powerTip, 'function', 'powerTip is defined'); 13 | }); 14 | 15 | QUnit.test('expose API via jQuery', function(assert) { 16 | assert.strictEqual(typeof $.powerTip.show, 'function', 'show is defined'); 17 | assert.strictEqual(typeof $.powerTip.reposition, 'function', 'reposition is defined'); 18 | assert.strictEqual(typeof $.powerTip.hide, 'function', 'hide is defined'); 19 | assert.strictEqual(typeof $.powerTip.toggle, 'function', 'toggle is defined'); 20 | assert.strictEqual(typeof $.powerTip.destroy, 'function', 'destroy is defined'); 21 | }); 22 | 23 | QUnit.test('expose API is returned from require()', function(assert) { 24 | assert.strictEqual(typeof powerTip.show, 'function', 'show is defined'); 25 | assert.strictEqual(typeof powerTip.reposition, 'function', 'reposition is defined'); 26 | assert.strictEqual(typeof powerTip.hide, 'function', 'hide is defined'); 27 | assert.strictEqual(typeof powerTip.toggle, 'function', 'toggle is defined'); 28 | assert.strictEqual(typeof powerTip.destroy, 'function', 'destroy is defined'); 29 | }); 30 | })(); 31 | -------------------------------------------------------------------------------- /test/edge-cases.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PowerTip Edge Case Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 55 | 56 | 57 | 68 | 69 | 70 |
71 |

PowerTip Edge Case Tests

72 |

73 | Tooltip CSS Theme: 74 | 85 |

86 |
87 |
88 |

Open on load

89 |

The button below has a tooltip that will open when the document loads. The tooltip should be properly positioned.

90 | 91 |
92 |
93 |

Tab change

94 |

The button below has a tooltip that is set to follow the mouse. Focus the element and change to another browser tab, then change back to this tab. The tooltip should revert to a static placement when the browser fires the focus event.

95 | 96 |
97 |
98 |

Click toggle

99 |

The button below has a tooltip that will toggle when clicked.

100 | 101 |
102 |
103 |

Remote target

104 |

The link below has a tooltip that will open when the button is clicked. It should open normally.

105 |

This link has a tooltip

106 | 107 |
108 |
109 |

Self-disabling button

110 |

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.

133 |
134 | 135 | 136 | 137 | 138 |
139 | 140 |
141 | 142 | 143 | 144 | 145 | 146 |
147 |
148 |
149 |

Huge Text with Smart Placement

150 |

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.

151 |
152 | 153 | 154 | 155 | 156 |
157 | 158 |
159 | 160 | 161 | 162 | 163 | 164 |
165 |
166 |
167 |

SVG Elements

168 |

The following glyphs are SVG elements. Tooltips should be placed correctly on all SVG elements.

169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 |
184 |
185 |

Complex SVG Elements

186 |

The following shapes are created using SVG. Tooltips should be placed correctly on all SVG elements.

187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | Text 202 | 203 |
204 |
205 |

Rotated SVG Elements

206 |

The following SVG shapes have been rotated. Tooltips should be placed correctly on all SVG elements.

207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 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 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PowerTip Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /test/tests-edge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | // Open on load 5 | $('#open-on-load input').powerTip({ placement: 'ne' }).powerTip('show'); 6 | 7 | // Tab change 8 | $('#tab-change input').powerTip({ followMouse: true }); 9 | 10 | // Click toggle 11 | $('#click-toggle input').powerTip({ 12 | openEvents: [ 'click' ], 13 | closeEvents: [ 'click' ], 14 | placement: 'e' 15 | }); 16 | 17 | // Remote target 18 | $('#remote-target a').powerTip({ placement: 'ne' }); 19 | $('#remote-target input').on('click', function() { 20 | $('#remote-target a').powerTip('show'); 21 | }); 22 | 23 | // Self-disabling button 24 | $('#disable-button input').on('click', function() { 25 | var $this = $(this); 26 | $this.attr('disabled', true); 27 | // enable after 2 seconds 28 | setTimeout(function() { 29 | $this.attr('disabled', false); 30 | }, 2000); 31 | }); 32 | $('#disable-button input').powerTip({ placement: 'e' }); 33 | 34 | // Auto-disabling button 35 | $('#auto-disable-button input').on('mouseenter focus', function() { 36 | var button = $(this); 37 | setTimeout(function() { 38 | button.attr('disabled', true); 39 | // enable after 2 seconds 40 | setTimeout(function() { 41 | button.attr('disabled', false); 42 | }, 2000); 43 | }, 2000); 44 | }); 45 | $('#auto-disable-button input').powerTip({ placement: 'e' }); 46 | 47 | // Long delay tooltips 48 | $('#long-delay #first-button').powerTip({ closeDelay: 2000, mouseOnToPopup: true }); 49 | $('#long-delay #second-button').powerTip({ closeDelay: 2000 }); 50 | 51 | // Manual and interactive tooltips 52 | $('#manual-and-interactive #manual-button').on('click', function() { 53 | $(this).powerTip('toggle'); 54 | }).powerTip({ manual: true }); 55 | $('#manual-and-interactive #interactive-button').powerTip({ mouseOnToPopup: true }); 56 | 57 | // setup huge text tooltips 58 | $.each( 59 | [ 60 | 'north', 61 | 'east', 62 | 'south', 63 | 'west', 64 | 'north-west', 65 | 'north-east', 66 | 'south-west', 67 | 'south-east', 68 | 'north-west-alt', 69 | 'north-east-alt', 70 | 'south-west-alt', 71 | 'south-east-alt' 72 | ], 73 | function(i, val) { 74 | $('.' + val).data( 75 | 'powertip', 76 | [ 77 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 78 | 'Praesent sed volutpat tellus. Fusce mollis iaculis est at', 79 | 'sodales. Proin aliquam bibendum neque, nec blandit orci', 80 | 'porttitor non. Cras lacinia varius felis vel ultricies.', 81 | 'Nulla eu sapien arcu, dapibus tempor eros. Praesent', 82 | 'aliquet hendrerit commodo. Pellentesque habitant morbi', 83 | 'tristique senectus et netus et malesuada fames ac turpis', 84 | 'egestas. Proin gravida justo faucibus urna dictum id', 85 | 'egestas velit hendrerit. Praesent dapibus rutrum tempor.', 86 | 'Sed ultrices varius purus, eu rhoncus tortor scelerisque', 87 | 'sit amet. Sed vitae molestie diam. Pellentesque posuere', 88 | 'euismod venenatis. Proin ut ligula vel urna lacinia', 89 | 'accumsan. Quisque commodo ultrices orci ut cursus.', 90 | 'Aliquam in dolor orci. Nunc pretium euismod odio.' 91 | ].join(' ') 92 | ); 93 | } 94 | ); 95 | 96 | // Huge text 97 | $('#huge-text .north').powerTip({ placement: 'n' }); 98 | $('#huge-text .east').powerTip({ placement: 'e' }); 99 | $('#huge-text .south').powerTip({ placement: 's' }); 100 | $('#huge-text .west').powerTip({ placement: 'w' }); 101 | $('#huge-text .north-west').powerTip({ placement: 'nw' }); 102 | $('#huge-text .north-east').powerTip({ placement: 'ne' }); 103 | $('#huge-text .south-west').powerTip({ placement: 'sw' }); 104 | $('#huge-text .south-east').powerTip({ placement: 'se' }); 105 | $('#huge-text .north-west-alt').powerTip({ placement: 'nw-alt' }); 106 | $('#huge-text .north-east-alt').powerTip({ placement: 'ne-alt' }); 107 | $('#huge-text .south-west-alt').powerTip({ placement: 'sw-alt' }); 108 | $('#huge-text .south-east-alt').powerTip({ placement: 'se-alt' }); 109 | 110 | // Huge text with smart placement 111 | $('#huge-text-smart .north').powerTip({ placement: 'n', smartPlacement: true }); 112 | $('#huge-text-smart .east').powerTip({ placement: 'e', smartPlacement: true }); 113 | $('#huge-text-smart .south').powerTip({ placement: 's', smartPlacement: true }); 114 | $('#huge-text-smart .west').powerTip({ placement: 'w', smartPlacement: true }); 115 | $('#huge-text-smart .north-west').powerTip({ placement: 'nw', smartPlacement: true }); 116 | $('#huge-text-smart .north-east').powerTip({ placement: 'ne', smartPlacement: true }); 117 | $('#huge-text-smart .south-west').powerTip({ placement: 'sw', smartPlacement: true }); 118 | $('#huge-text-smart .south-east').powerTip({ placement: 'se', smartPlacement: true }); 119 | $('#huge-text-smart .north-west-alt').powerTip({ placement: 'nw-alt', smartPlacement: true }); 120 | $('#huge-text-smart .north-east-alt').powerTip({ placement: 'ne-alt', smartPlacement: true }); 121 | $('#huge-text-smart .south-west-alt').powerTip({ placement: 'sw-alt', smartPlacement: true }); 122 | $('#huge-text-smart .south-east-alt').powerTip({ placement: 'se-alt', smartPlacement: true }); 123 | 124 | // SVG elements 125 | $('#svg-elements #red-ellipse1').powerTip({ placement: 'n' }); 126 | $('#svg-elements #red-ellipse2').powerTip({ placement: 'e' }); 127 | $('#svg-elements #red-ellipse3').powerTip({ placement: 's' }); 128 | $('#svg-elements #red-ellipse4').powerTip({ placement: 'w' }); 129 | $('#svg-elements #red-ellipse5').powerTip({ placement: 'nw' }); 130 | $('#svg-elements #red-ellipse5-alt').powerTip({ placement: 'nw-alt' }); 131 | $('#svg-elements #red-ellipse6').powerTip({ placement: 'ne' }); 132 | $('#svg-elements #red-ellipse6-alt').powerTip({ placement: 'ne-alt' }); 133 | $('#svg-elements #red-ellipse7').powerTip({ placement: 'sw' }); 134 | $('#svg-elements #red-ellipse7-alt').powerTip({ placement: 'sw-alt' }); 135 | $('#svg-elements #red-ellipse8').powerTip({ placement: 'se' }); 136 | $('#svg-elements #red-ellipse8-alt').powerTip({ placement: 'se-alt' }); 137 | $('#svg-elements #red-ellipse9').powerTip({ placement: 'n' }); 138 | 139 | // Complex SVG elements 140 | $('#complex-svg-elements #black-star').powerTip({ placement: 'n' }); 141 | $('#complex-svg-elements #black-line').powerTip({ placement: 'e' }); 142 | $('#complex-svg-elements #black-mexico').powerTip({ placement: 's' }); 143 | $('#complex-svg-elements #black-group').powerTip({ placement: 'w' }); 144 | $('#complex-svg-elements #black-circle').powerTip({ placement: 'nw' }); 145 | $('#complex-svg-elements #black-rect').powerTip({ placement: 'ne' }); 146 | $('#complex-svg-elements #black-polygon').powerTip({ placement: 'sw' }); 147 | $('#complex-svg-elements #black-text').powerTip({ placement: 'se' }); 148 | 149 | // Rotated SVG elements 150 | $('#rotated-svg-elements #blue-ellipse1').powerTip({ placement: 'nw' }); 151 | $('#rotated-svg-elements #blue-ellipse2').powerTip({ placement: 'n' }); 152 | $('#rotated-svg-elements #blue-ellipse3').powerTip({ placement: 'ne' }); 153 | $('#rotated-svg-elements #blue-ellipse4').powerTip({ placement: 'w' }); 154 | $('#rotated-svg-elements #blue-ellipse5').powerTip({ followMouse: true }); 155 | $('#rotated-svg-elements #blue-ellipse6').powerTip({ placement: 'e' }); 156 | $('#rotated-svg-elements #blue-ellipse7').powerTip({ placement: 'sw' }); 157 | $('#rotated-svg-elements #blue-ellipse8').powerTip({ placement: 's' }); 158 | $('#rotated-svg-elements #blue-ellipse9').powerTip({ placement: 'se' }); 159 | 160 | // Trapped mouse following tooltip 161 | $('#trapped-mousefollow').powerTip({ followMouse: true }); 162 | }); 163 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/unit/csscoordinates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | QUnit.module('CSS Coordinates'); 5 | 6 | QUnit.test('expose properties', function(assert) { 7 | var coords = new CSSCoordinates(); 8 | assert.strictEqual(coords.top, 'auto', 'top property is defined'); 9 | assert.strictEqual(coords.left, 'auto', 'left property is defined'); 10 | assert.strictEqual(coords.right, 'auto', 'right property is defined'); 11 | assert.strictEqual(coords.bottom, 'auto', 'bottom property is defined'); 12 | }); 13 | 14 | QUnit.test('expose methods', function(assert) { 15 | var coords = new CSSCoordinates(); 16 | assert.strictEqual(typeof coords.set, 'function', 'set method is defined'); 17 | }); 18 | 19 | QUnit.test('decimal values are rounded', function(assert) { 20 | var coords = new CSSCoordinates(); 21 | 22 | coords.set('top', 10.5); 23 | coords.set('left', 10.4); 24 | coords.set('right', 10.499); 25 | coords.set('bottom', 10.50000000000001); 26 | 27 | assert.strictEqual(coords.top, 11, 'top property was rounded up'); 28 | assert.strictEqual(coords.left, 10, 'left property was rounded down'); 29 | assert.strictEqual(coords.right, 10, 'right property was rounded down'); 30 | assert.strictEqual(coords.bottom, 11, 'bottom property was rounded up'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/displaycontroller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | QUnit.module('Display Controller'); 5 | 6 | QUnit.test('expose methods', function(assert) { 7 | var dc = new DisplayController(); 8 | assert.strictEqual(typeof dc.show, 'function', 'show method is defined'); 9 | assert.strictEqual(typeof dc.hide, 'function', 'hide method is defined'); 10 | assert.strictEqual(typeof dc.cancel, 'function', 'cances method is defined'); 11 | assert.strictEqual(typeof dc.resetPosition, 'function', 'resetPosition method is defined'); 12 | }); 13 | 14 | QUnit.test('show method calls TooltipController.showTip', function(assert) { 15 | var done = assert.async(), 16 | element = $(''), 17 | dc; 18 | 19 | assert.expect(1); 20 | 21 | dc = new DisplayController( 22 | element, 23 | $.fn.powerTip.defaults, 24 | new MockTipController( 25 | function(el) { 26 | assert.deepEqual(el, element, 'original element passed'); 27 | done(); 28 | } 29 | ) 30 | ); 31 | 32 | dc.show(); 33 | }); 34 | 35 | QUnit.test('hide method calls TooltipController.hideTip', function(assert) { 36 | var done = assert.async(), 37 | element = $(''), 38 | dc; 39 | 40 | assert.expect(1); 41 | 42 | dc = new DisplayController( 43 | element, 44 | $.fn.powerTip.defaults, 45 | new MockTipController( 46 | null, 47 | function(el) { 48 | assert.deepEqual(el, element, 'original element passed'); 49 | done(); 50 | } 51 | ) 52 | ); 53 | 54 | element.data(DATA_HASACTIVEHOVER, true); // set active hover or hide wont do anything 55 | dc.hide(); 56 | }); 57 | 58 | QUnit.test('resetPosition method calls TooltipController.resetPosition', function(assert) { 59 | var element = $(''), 60 | resetCalled = false, 61 | dc; 62 | 63 | dc = new DisplayController( 64 | element, 65 | $.fn.powerTip.defaults, 66 | new MockTipController( 67 | null, 68 | null, 69 | function() { 70 | resetCalled = true; 71 | } 72 | ) 73 | ); 74 | 75 | dc.resetPosition(); 76 | 77 | assert.ok(resetCalled, 'resetPosition method called'); 78 | }); 79 | 80 | QUnit.test('show method does not delay when immediate is set to true', function(assert) { 81 | var element = $(''), 82 | showCalled = false, 83 | dc; 84 | 85 | dc = new DisplayController( 86 | element, 87 | $.fn.powerTip.defaults, 88 | new MockTipController( 89 | function(el) { 90 | showCalled = true; 91 | assert.deepEqual(el, element, 'original element passed'); 92 | } 93 | ) 94 | ); 95 | 96 | dc.show(true); 97 | 98 | assert.ok(showCalled, 'showTip called'); 99 | }); 100 | 101 | QUnit.test('show method adds the DATA_FORCEDOPEN flag when forceOpen is set to true', function(assert) { 102 | var element = $(''), 103 | dc; 104 | 105 | dc = new DisplayController( 106 | element, 107 | $.fn.powerTip.defaults, 108 | new MockTipController() 109 | ); 110 | 111 | dc.show(true, true); 112 | 113 | assert.strictEqual(element.data(DATA_FORCEDOPEN), true, 'DATA_FORCEDOPEN is true'); 114 | }); 115 | 116 | QUnit.test('hide method does not delay when disableDelay is set to true', function(assert) { 117 | var element = $(''), 118 | hideCalled = false, 119 | dc; 120 | 121 | dc = new DisplayController( 122 | element, 123 | $.fn.powerTip.defaults, 124 | new MockTipController( 125 | null, 126 | function(el) { 127 | hideCalled = true; 128 | assert.deepEqual(el, element, 'original element passed'); 129 | } 130 | ) 131 | ); 132 | 133 | element.data(DATA_HASACTIVEHOVER, true); // set active hover or hide wont do anything 134 | dc.hide(true); 135 | 136 | assert.ok(hideCalled, 'hideTip called'); 137 | }); 138 | 139 | QUnit.test('cancel method stops showTip from being called', function(assert) { 140 | var done = assert.async(), 141 | element = $(''), 142 | showCalled = false, 143 | dc; 144 | 145 | assert.expect(1); 146 | 147 | dc = new DisplayController( 148 | element, 149 | $.fn.powerTip.defaults, 150 | new MockTipController( 151 | function() { 152 | showCalled = true; 153 | } 154 | ) 155 | ); 156 | 157 | dc.show(); 158 | 159 | setTimeout(function() { 160 | dc.cancel(); 161 | 162 | setTimeout(function() { 163 | assert.notOk(showCalled, 'showTip was not called'); 164 | done(); 165 | }, $.fn.powerTip.defaults.intentPollInterval / 2 + 10); 166 | }, $.fn.powerTip.defaults.intentPollInterval / 2); 167 | }); 168 | 169 | QUnit.test('cancel method stops hideTip from being called', function(assert) { 170 | var done = assert.async(), 171 | element = $(''), 172 | hideCalled = false, 173 | dc; 174 | 175 | assert.expect(1); 176 | 177 | dc = new DisplayController( 178 | element, 179 | $.fn.powerTip.defaults, 180 | new MockTipController( 181 | null, 182 | function() { 183 | hideCalled = true; 184 | } 185 | ) 186 | ); 187 | 188 | element.data(DATA_HASACTIVEHOVER, true); // set active hover or hide wont do anything 189 | dc.hide(); 190 | 191 | setTimeout(function() { 192 | dc.cancel(); 193 | 194 | setTimeout(function() { 195 | assert.notOk(hideCalled, 'showTip was not called'); 196 | done(); 197 | }, $.fn.powerTip.defaults.closeDelay / 2 + 10); 198 | }, $.fn.powerTip.defaults.closeDelay / 2); 199 | }); 200 | 201 | QUnit.test('show method does not call showTip if hover intent is never satisfied', function(assert) { 202 | var done = assert.async(), 203 | element = $(''), 204 | showCalled = false, 205 | testCount = 5, 206 | dc, 207 | changeMousePosition; 208 | 209 | assert.expect(testCount); 210 | 211 | dc = new DisplayController( 212 | element, 213 | $.fn.powerTip.defaults, 214 | new MockTipController( 215 | function() { 216 | showCalled = true; 217 | } 218 | ) 219 | ); 220 | 221 | changeMousePosition = function() { 222 | if (testCount-- > 0) { 223 | // check value, move the mouse cursor, and run the test again 224 | assert.strictEqual(showCalled, false, 'showTip has not been called'); 225 | session.currentX += $.fn.powerTip.defaults.intentSensitivity; 226 | session.currentY += $.fn.powerTip.defaults.intentSensitivity; 227 | setTimeout(changeMousePosition, $.fn.powerTip.defaults.intentPollInterval); 228 | } else { 229 | // we're done testing 230 | done(); 231 | } 232 | }; 233 | 234 | dc.show(); 235 | changeMousePosition(); 236 | }); 237 | 238 | function MockTipController(show, hide, reset) { 239 | this.showTip = show || $.noop; 240 | this.hideTip = hide || $.noop; 241 | this.resetPosition = reset || $.noop; 242 | } 243 | }); 244 | -------------------------------------------------------------------------------- /test/unit/placementcalculator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | QUnit.module('Placement Calculator'); 5 | 6 | QUnit.test('expose methods', function(assert) { 7 | var pc = new PlacementCalculator(); 8 | assert.strictEqual(typeof pc.compute, 'function', 'compute method is defined'); 9 | }); 10 | 11 | QUnit.test('return CSSCordinates object', function(assert) { 12 | var pc = new PlacementCalculator(), 13 | element = $('
Some Content
'), 14 | retVal = pc.compute( 15 | element, 16 | $.fn.powerTip.defaults.placement, 17 | element.width(), 18 | element.height(), 19 | $.fn.powerTip.defaults.offset 20 | ); 21 | 22 | assert.ok(retVal instanceof CSSCoordinates); 23 | }); 24 | 25 | QUnit.test('return expected CSSCordinates properties', function(assert) { 26 | var pc = new PlacementCalculator(), 27 | element = $('
Some Content
'), 28 | coords; 29 | 30 | $.each($.fn.powerTip.smartPlacementLists, function(key) { 31 | coords = pc.compute( 32 | element, 33 | key, 34 | element.width(), 35 | element.height(), 36 | $.fn.powerTip.defaults.offset 37 | ); 38 | 39 | switch (key) { 40 | case 'n': 41 | case 'ne': 42 | case 'nw-alt': 43 | assert.strictEqual(coords.top, 'auto', key + ': top property is set to auto'); 44 | assert.strictEqual(typeof coords.left, 'number', key + ': left property is set to a number'); 45 | assert.strictEqual(coords.right, 'auto', key + ': right property is set to auto'); 46 | assert.strictEqual(typeof coords.bottom, 'number', key + ': bottom property is set to a number'); 47 | break; 48 | case 'e': 49 | case 's': 50 | case 'se': 51 | case 'sw-alt': 52 | assert.strictEqual(typeof coords.top, 'number', key + ': top property is set to a number'); 53 | assert.strictEqual(typeof coords.left, 'number', key + ': left property is set to a number'); 54 | assert.strictEqual(coords.right, 'auto', key + ': right property is set to auto'); 55 | assert.strictEqual(coords.bottom, 'auto', key + ': bottom property is set to auto'); 56 | break; 57 | case 'w': 58 | case 'sw': 59 | case 'se-alt': 60 | assert.strictEqual(typeof coords.top, 'number', key + ': top property is set to a number'); 61 | assert.strictEqual(coords.left, 'auto', key + ': left property is set to auto'); 62 | assert.strictEqual(typeof coords.right, 'number', key + ': right property is set to a number'); 63 | assert.strictEqual(coords.bottom, 'auto', key + ': bottom property is set to auto'); 64 | break; 65 | case 'nw': 66 | case 'ne-alt': 67 | assert.strictEqual(coords.top, 'auto', key + ': top property is set to auto'); 68 | assert.strictEqual(coords.left, 'auto', key + ': left property is set to auto'); 69 | assert.strictEqual(typeof coords.right, 'number', key + ': right property is set to a number'); 70 | assert.strictEqual(typeof coords.bottom, 'number', key + ': bottom property is set to a number'); 71 | break; 72 | } 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/unit/tooltipcontroller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | // set of default options with zero fade time for faster testing 5 | var zeroTimeOpts = $.extend({}, $.fn.powerTip.defaults, { fadeInTime: 0, fadeOutTime: 0 }); 6 | 7 | QUnit.module('Tooltip Controller'); 8 | 9 | QUnit.test('expose methods', function(assert) { 10 | var tc = new TooltipController($.fn.powerTip.defaults); 11 | assert.strictEqual(typeof tc.showTip, 'function', 'showTip method is defined'); 12 | assert.strictEqual(typeof tc.hideTip, 'function', 'hideTip method is defined'); 13 | assert.strictEqual(typeof tc.resetPosition, 'function', 'resetPosition method is defined'); 14 | }); 15 | 16 | QUnit.test('custom PowerTip events fire', function(assert) { 17 | var done = assert.async(), 18 | element = $('').data(DATA_POWERTIP, 'This is the tooltip text.'), 19 | tc = new TooltipController(zeroTimeOpts), 20 | tipElem = $('#' + $.fn.powerTip.defaults.popupId); 21 | 22 | assert.expect(14); 23 | 24 | element.on({ 25 | powerTipPreRender: function() { 26 | assert.step('powerTipPreRender'); 27 | assert.ok(true, 'powerTipPreRender fired'); 28 | assert.notStrictEqual(tipElem.text(), 'This is the tooltip text.', 'tooltip content has not been inserted yet'); 29 | }, 30 | powerTipRender: function() { 31 | assert.step('powerTipRender'); 32 | assert.ok(true, 'powerTipRender fired'); 33 | assert.strictEqual(tipElem.text(), 'This is the tooltip text.', 'tooltip content has been inserted'); 34 | }, 35 | powerTipOpen: function() { 36 | assert.step('powerTipOpen'); 37 | assert.ok(true, 'powerTipClose fired'); 38 | assert.strictEqual(tipElem.css('opacity'), '1', 'tooltip is faded in'); 39 | tc.hideTip(element); 40 | }, 41 | powerTipClose: function() { 42 | assert.step('powerTipClose'); 43 | assert.ok(true, 'powerTipClose fired'); 44 | assert.strictEqual(tipElem.text(), 'This is the tooltip text.', 'tooltip content still exists'); 45 | assert.strictEqual(tipElem.css('display'), 'none', 'display set to none'); 46 | assert.verifySteps([ 'powerTipPreRender', 'powerTipRender', 'powerTipOpen', 'powerTipClose' ]); 47 | done(); 48 | } 49 | }); 50 | 51 | tc.showTip(element); 52 | }); 53 | 54 | QUnit.test('showTip opens tooltip and hideTip closes it', function(assert) { 55 | var done = assert.async(), 56 | element = $('').data(DATA_POWERTIP, 'This is the tooltip text.'), 57 | tc = new TooltipController(zeroTimeOpts), 58 | tipElem = $('#' + $.fn.powerTip.defaults.popupId); 59 | 60 | assert.expect(2); 61 | 62 | element.on({ 63 | powerTipOpen: function() { 64 | assert.strictEqual(tipElem.css('opacity'), '1', 'tooltip is faded in'); 65 | tc.hideTip(element); 66 | }, 67 | powerTipClose: function() { 68 | assert.strictEqual(tipElem.css('display'), 'none', 'display set to none'); 69 | done(); 70 | } 71 | }); 72 | 73 | tc.showTip(element); 74 | }); 75 | 76 | QUnit.test('TooltipController uses custom id', function(assert) { 77 | var customPopupId = 'myPopupId', 78 | tc = new TooltipController($.extend({}, zeroTimeOpts, { popupId: customPopupId })); 79 | 80 | assert.strictEqual($('#' + customPopupId).length, 1, 'custom id element created'); 81 | 82 | // clean up 83 | $('#' + customPopupId).remove(); 84 | 85 | // this is solely to make the linter happy 86 | tc.foo = 0; 87 | }); 88 | 89 | QUnit.test('TooltipController adds placement classes', function(assert) { 90 | var done = assert.async(), 91 | placementList = [], 92 | tipElem; 93 | 94 | assert.expect(Object.keys($.fn.powerTip.smartPlacementLists).length); 95 | 96 | // function that actually runs a test 97 | function testPlacementClass(placement) { 98 | var element = $('').data(DATA_POWERTIP, 'This is the tooltip text.'), 99 | opts = $.extend({}, zeroTimeOpts, { placement: placement }), 100 | tc = new TooltipController(opts); 101 | 102 | // the element wont exist until it's built by a TooltipController, 103 | // so we must grab it here 104 | if (!tipElem) { 105 | tipElem = $('#' + $.fn.powerTip.defaults.popupId); 106 | } 107 | 108 | element.on({ 109 | powerTipOpen: function() { 110 | assert.strictEqual(tipElem.hasClass(placement), true, placement + ' placement class added'); 111 | tc.hideTip(element); 112 | }, 113 | powerTipClose: function() { 114 | runNextTest(); 115 | } 116 | }); 117 | 118 | tc.showTip(element); 119 | } 120 | 121 | // function to run the next test in the placementList queue 122 | function runNextTest() { 123 | var nextPlacement = placementList.shift(); 124 | if (nextPlacement) { 125 | testPlacementClass(nextPlacement); 126 | } else { 127 | done(); 128 | } 129 | } 130 | 131 | // populate placement list 132 | $.each($.fn.powerTip.smartPlacementLists, function(key) { 133 | placementList.push(key); 134 | }); 135 | 136 | // start the tests 137 | runNextTest(); 138 | }); 139 | 140 | QUnit.test('resetPosition removes old placement classes', function(assert) { 141 | var placement = 'se', 142 | element = $('').data(DATA_POWERTIP, 'This is the tooltip text.'), 143 | tc = new TooltipController($.extend({}, zeroTimeOpts, { placement: placement })), 144 | tipElem = $('#' + $.fn.powerTip.defaults.popupId), 145 | allClasses = Object.keys($.fn.powerTip.smartPlacementLists), 146 | key; 147 | 148 | assert.expect(allClasses.length - 1); 149 | 150 | tipElem.addClass(allClasses.join(' ')); 151 | tc.resetPosition(element); 152 | 153 | for (key in $.fn.powerTip.smartPlacementLists) { 154 | if (key !== placement) { 155 | assert.strictEqual(tipElem.hasClass(key), false, 'tooltip element does not have class: ' + key); 156 | } 157 | } 158 | }); 159 | 160 | QUnit.test('TooltipController adds custom classes', function(assert) { 161 | var done = assert.async(), 162 | element = $('').data(DATA_POWERTIP, 'This is the tooltip text.'), 163 | tc = new TooltipController($.extend({}, zeroTimeOpts, { popupClass: 'customClass' })), 164 | tipElem = $('#' + $.fn.powerTip.defaults.popupId); 165 | 166 | assert.expect(1); 167 | 168 | element.on({ 169 | powerTipOpen: function() { 170 | assert.strictEqual(tipElem.hasClass('customClass'), true, 'custom class added'); 171 | tc.hideTip(element); 172 | }, 173 | powerTipClose: function() { 174 | done(); 175 | } 176 | }); 177 | 178 | tc.showTip(element); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------