├── .gitattributes ├── .gitignore ├── Gruntfile.js ├── README.md ├── dist ├── css │ └── tipped.css └── js │ ├── tipped.js │ └── tipped.min.js ├── example ├── css │ └── style.css └── index.html ├── package-lock.json ├── package.json └── src ├── css └── tipped.css └── js ├── api.js ├── behaviors.js ├── collection.js ├── delegate.js ├── helpers ├── ajaxcache.js ├── bounds.js ├── browser.js ├── color.js ├── dimensions.js ├── helpers.js ├── mouse.js ├── position.js ├── requirements.js ├── spin.js ├── support.js └── visible.js ├── options.js ├── setup.js ├── skin.js ├── skins.js ├── stem.js ├── tooltip.js ├── tooltip ├── active.js ├── bind.js ├── disable.js ├── display.js ├── is.js ├── layout.js ├── timers.js └── update.js ├── tooltips.js ├── umd-head.js ├── umd-tail.js └── voila └── voila.custom.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | .history -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON("package.json"), 4 | dirs: { 5 | dest: "dist", 6 | }, 7 | 8 | vars: {}, 9 | 10 | concat: { 11 | options: { process: true }, 12 | dist: { 13 | src: [ 14 | "src/js/umd-head.js", 15 | "src/js/setup.js", 16 | "src/js/skins.js", 17 | 18 | // helpers 19 | "src/js/helpers/browser.js", 20 | "src/js/helpers/support.js", 21 | "src/js/helpers/helpers.js", 22 | "src/js/helpers/position.js", 23 | "src/js/helpers/bounds.js", 24 | "src/js/helpers/mouse.js", 25 | "src/js/helpers/color.js", 26 | "src/js/helpers/spin.js", 27 | "src/js/helpers/visible.js", 28 | "src/js/helpers/ajaxcache.js", 29 | "src/js/voila/voila.custom.js", 30 | 31 | // core 32 | "src/js/behaviors.js", 33 | "src/js/options.js", 34 | "src/js/skin.js", 35 | "src/js/stem.js", 36 | "src/js/tooltips.js", 37 | 38 | // tooltip 39 | "src/js/tooltip.js", 40 | "src/js/tooltip/display.js", 41 | "src/js/tooltip/update.js", 42 | "src/js/tooltip/layout.js", 43 | // tooltip (helpers) 44 | "src/js/tooltip/active.js", 45 | "src/js/tooltip/bind.js", 46 | "src/js/tooltip/disable.js", 47 | "src/js/tooltip/is.js", 48 | "src/js/tooltip/timers.js", 49 | 50 | "src/js/api.js", 51 | "src/js/delegate.js", 52 | "src/js/collection.js", 53 | 54 | "src/js/umd-tail.js", 55 | ], 56 | dest: "<%= dirs.dest %>/js/tipped.js", 57 | }, 58 | }, 59 | 60 | copy: { 61 | dist: { 62 | files: [ 63 | { 64 | expand: true, 65 | cwd: "src/css/", 66 | src: ["**"], 67 | dest: "<%= dirs.dest %>/css/", 68 | }, 69 | ], 70 | }, 71 | }, 72 | 73 | uglify: { 74 | dist: { 75 | options: { 76 | output: { 77 | comments: "some", 78 | }, 79 | }, 80 | src: ["<%= dirs.dest %>/js/tipped.js"], 81 | dest: "<%= dirs.dest %>/js/tipped.min.js", 82 | }, 83 | }, 84 | 85 | clean: { 86 | dist: "dist/", 87 | }, 88 | 89 | watch: { 90 | scripts: { 91 | files: ["src/**/*.js", "src/**/*.css"], 92 | tasks: ["default"], 93 | options: { 94 | spawn: false, 95 | }, 96 | }, 97 | }, 98 | }); 99 | 100 | // Load plugins 101 | grunt.loadNpmTasks("grunt-contrib-concat"); 102 | grunt.loadNpmTasks("grunt-contrib-copy"); 103 | grunt.loadNpmTasks("grunt-contrib-uglify"); 104 | grunt.loadNpmTasks("grunt-contrib-clean"); 105 | grunt.loadNpmTasks("grunt-contrib-watch"); 106 | 107 | grunt.registerTask("default", [ 108 | "clean:dist", 109 | "concat:dist", 110 | "copy:dist", 111 | "uglify:dist", 112 | ]); 113 | }; 114 | -------------------------------------------------------------------------------- /dist/css/tipped.css: -------------------------------------------------------------------------------- 1 | .tpd-tooltip { 2 | position: absolute; 3 | } 4 | 5 | /* Fix for CSS frameworks that don't keep the use of box-sizing: border-box 6 | within their own namespace */ 7 | .tpd-tooltip { 8 | box-sizing: content-box; 9 | } 10 | .tpd-tooltip [class^="tpd-"] { 11 | box-sizing: inherit; 12 | } 13 | 14 | /* Content */ 15 | .tpd-content-wrapper { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | float: left; 20 | width: 100%; 21 | height: 100%; 22 | overflow: hidden; 23 | } 24 | .tpd-content-spacer, 25 | .tpd-content-relative, 26 | .tpd-content-relative-padder { 27 | float: left; 28 | position: relative; 29 | } 30 | .tpd-content-relative { 31 | width: 100%; 32 | } 33 | 34 | .tpd-content { 35 | float: left; 36 | clear: both; 37 | position: relative; 38 | padding: 10px; 39 | font-size: 11px; 40 | line-height: 16px; 41 | color: #fff; 42 | box-sizing: border-box !important; 43 | } 44 | .tpd-has-inner-close .tpd-content-relative .tpd-content { 45 | padding-right: 0 !important; 46 | } 47 | .tpd-tooltip .tpd-content-no-padding { 48 | padding: 0 !important; 49 | } 50 | 51 | .tpd-title-wrapper { 52 | float: left; 53 | position: relative; 54 | overflow: hidden; 55 | } 56 | .tpd-title-spacer { 57 | float: left; 58 | } 59 | .tpd-title-relative, 60 | .tpd-title-relative-padder { 61 | float: left; 62 | position: relative; 63 | } 64 | .tpd-title-relative { 65 | width: 100%; 66 | } 67 | .tpd-title { 68 | float: left; 69 | position: relative; 70 | font-size: 11px; 71 | line-height: 16px; 72 | padding: 10px; 73 | font-weight: bold; 74 | text-transform: uppercase; 75 | color: #fff; 76 | box-sizing: border-box !important; 77 | } 78 | .tpd-has-title-close .tpd-title { 79 | padding-right: 0 !important; 80 | } 81 | .tpd-close { 82 | position: absolute; 83 | top: 0; 84 | right: 0; 85 | width: 28px; 86 | height: 28px; 87 | cursor: pointer; 88 | overflow: hidden; 89 | color: #fff; 90 | } 91 | .tpd-close-icon { 92 | float: left; 93 | font-family: Arial, Baskerville, monospace; 94 | font-weight: normal; 95 | font-style: normal; 96 | text-decoration: none; 97 | width: 28px; 98 | height: 28px; 99 | font-size: 28px; 100 | line-height: 28px; 101 | text-align: center; 102 | } 103 | 104 | /* Skin */ 105 | .tpd-skin { 106 | position: absolute; 107 | top: 0; 108 | left: 0; 109 | } 110 | 111 | .tpd-frames { 112 | position: absolute; 113 | top: 0; 114 | left: 0; 115 | } 116 | .tpd-frames .tpd-frame { 117 | float: left; 118 | width: 100%; 119 | height: 100%; 120 | clear: both; 121 | display: none; 122 | } 123 | 124 | .tpd-visible-frame-top .tpd-frame-top { 125 | display: block; 126 | } 127 | .tpd-visible-frame-bottom .tpd-frame-bottom { 128 | display: block; 129 | } 130 | .tpd-visible-frame-left .tpd-frame-left { 131 | display: block; 132 | } 133 | .tpd-visible-frame-right .tpd-frame-right { 134 | display: block; 135 | } 136 | 137 | .tpd-backgrounds { 138 | position: absolute; 139 | top: 0; 140 | left: 0; 141 | width: 100%; 142 | height: 100%; 143 | -webkit-transform-origin: 0% 0%; 144 | transform-origin: 0% 0%; 145 | } 146 | .tpd-background-shadow { 147 | position: absolute; 148 | top: 0; 149 | left: 0; 150 | width: 100%; 151 | height: 100%; 152 | background-color: transparent; 153 | pointer-events: none; 154 | } 155 | .tpd-no-shadow .tpd-skin .tpd-background-shadow { 156 | box-shadow: none !important; 157 | } 158 | 159 | .tpd-background-box { 160 | position: absolute; 161 | top: 0; 162 | left: 0; 163 | height: 100%; 164 | width: 100%; 165 | overflow: hidden; 166 | } 167 | /* only the top background box should be shown when not using a stem */ 168 | .tpd-no-stem .tpd-background-box, 169 | .tpd-no-stem .tpd-shift-stem { 170 | display: none; 171 | } 172 | .tpd-no-stem .tpd-background-box-top { 173 | display: block; 174 | } 175 | 176 | .tpd-background-box-shift, 177 | .tpd-background-box-shift-further { 178 | position: relative; 179 | float: left; 180 | width: 100%; 181 | height: 100%; 182 | } 183 | .tpd-background { 184 | border-radius: 10px; 185 | float: left; 186 | clear: both; 187 | background: none; 188 | -webkit-background-clip: padding-box; /* Safari */ 189 | background-clip: padding-box; /* IE9+, Firefox 4+, Opera, Chrome */ 190 | border-style: solid; 191 | border-width: 1px; 192 | border-color: rgba( 193 | 255, 194 | 255, 195 | 255, 196 | 0.1 197 | ); /* opacity here bugs out in firefox, .tpd-background-content should have no opacity if this opacity is less than 1 */ 198 | } 199 | .tpd-background-loading { 200 | display: none; 201 | } 202 | /* no radius */ 203 | .tpd-no-radius 204 | .tpd-skin 205 | .tpd-frames 206 | .tpd-frame 207 | .tpd-backgrounds 208 | .tpd-background { 209 | border-radius: 0; 210 | } 211 | .tpd-background-title { 212 | float: left; 213 | clear: both; 214 | width: 100%; 215 | background-color: #282828; 216 | } 217 | .tpd-background-content { 218 | float: left; 219 | clear: both; 220 | width: 100%; 221 | background-color: #282828; 222 | } 223 | .tpd-background-border-hack { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | width: 100%; 228 | height: 100%; 229 | border-style: solid; 230 | } 231 | 232 | .tpd-background-box-top { 233 | top: 0; 234 | } 235 | .tpd-background-box-bottom { 236 | bottom: 0; 237 | } 238 | .tpd-background-box-left { 239 | left: 0; 240 | } 241 | .tpd-background-box-right { 242 | right: 0; 243 | } 244 | 245 | /* Skin / Stems */ 246 | .tpd-shift-stem { 247 | position: absolute; 248 | top: 0; 249 | left: 0; 250 | overflow: hidden; 251 | } 252 | .tpd-shift-stem-side { 253 | position: absolute; 254 | } 255 | .tpd-frame-top .tpd-shift-stem-side, 256 | .tpd-frame-bottom .tpd-shift-stem-side { 257 | width: 100%; 258 | } 259 | .tpd-frame-left .tpd-shift-stem-side, 260 | .tpd-frame-right .tpd-shift-stem-side { 261 | height: 100%; 262 | } 263 | 264 | .tpd-stem { 265 | position: absolute; 266 | top: 0; 267 | left: 0; 268 | overflow: hidden; /* shows possible invalid subpx rendering */ 269 | width: 16px; /* best cross browser stem: width = 2 x height (90deg angle) */ 270 | height: 8px; 271 | margin-left: 3px; /* space from the side */ 272 | margin-top: 2px; /* space between target and stem */ 273 | -webkit-transform-origin: 0% 0%; 274 | transform-origin: 0% 0%; 275 | } 276 | /* remove margins once we're done measuring */ 277 | .tpd-tooltip .tpd-skin .tpd-frames .tpd-frame .tpd-shift-stem .tpd-stem-reset { 278 | margin: 0 !important; 279 | } 280 | 281 | .tpd-stem-spacer { 282 | position: absolute; 283 | top: 0; 284 | left: 0; 285 | width: 100%; 286 | height: 100%; 287 | } 288 | .tpd-stem-reset .tpd-stem-spacer { 289 | margin-top: 0; 290 | } 291 | 292 | .tpd-stem-point { 293 | width: 100px; 294 | position: absolute; 295 | top: 0; 296 | left: 50%; 297 | } 298 | .tpd-stem-downscale, 299 | .tpd-stem-transform { 300 | float: left; 301 | width: 100%; 302 | height: 100%; 303 | -webkit-transform-origin: 0% 0%; 304 | transform-origin: 0% 0%; 305 | position: relative; 306 | } 307 | 308 | .tpd-stem-side { 309 | width: 50%; 310 | height: 100%; 311 | float: left; 312 | position: relative; 313 | overflow: hidden; 314 | } 315 | .tpd-stem-side-inversed { 316 | -webkit-transform: scale(-1, 1); 317 | transform: scale(-1, 1); 318 | } 319 | .tpd-stem-triangle { 320 | width: 0; 321 | height: 0; 322 | border-bottom-style: solid; 323 | border-left-color: transparent; 324 | border-left-style: solid; 325 | position: absolute; 326 | top: 0; 327 | left: 0; 328 | } 329 | .tpd-stem-border { 330 | width: 20px; 331 | height: 100%; 332 | position: absolute; 333 | top: 0; 334 | left: 50%; 335 | background-color: #fff; /* will become transparent */ 336 | border-right-color: #fff; 337 | border-right-style: solid; 338 | border-right-width: 0; 339 | } 340 | 341 | .tpd-stem-border-corner { 342 | position: absolute; 343 | top: 0; 344 | left: 50%; 345 | height: 100%; 346 | border-right-style: solid; 347 | border-right-width: 0; 348 | } 349 | 350 | /* fixes rendering issue in IE */ 351 | .tpd-stem * { 352 | z-index: 0; 353 | zoom: 1; 354 | } 355 | 356 | /* used by IE < 9 */ 357 | .tpd-stem-border-center-offset, 358 | .tpd-stem-border-center-offset-inverse { 359 | float: left; 360 | position: relative; 361 | width: 100%; 362 | height: 100%; 363 | overflow: hidden; 364 | } 365 | .tpd-stem-notransform { 366 | float: left; 367 | width: 100%; 368 | height: 100%; 369 | position: relative; 370 | } 371 | .tpd-stem-notransform .tpd-stem-border { 372 | height: 100%; 373 | position: relative; 374 | float: left; 375 | top: 0; 376 | left: 0; 377 | margin: 0; 378 | } 379 | .tpd-stem-notransform .tpd-stem-border-center { 380 | position: absolute; 381 | } 382 | .tpd-stem-notransform .tpd-stem-border-corner { 383 | background: #fff; 384 | border: 0; 385 | top: auto; 386 | left: auto; 387 | } 388 | .tpd-stem-notransform .tpd-stem-border-center, 389 | .tpd-stem-notransform .tpd-stem-triangle { 390 | height: 0; 391 | border: 0; 392 | left: 50%; 393 | } 394 | 395 | /* transformations for left/right/bottom */ 396 | .tpd-stem-transform-left { 397 | -webkit-transform: rotate(-90deg) scale(-1, 1); 398 | transform: rotate(-90deg) scale(-1, 1); 399 | } 400 | .tpd-stem-transform-right { 401 | -webkit-transform: rotate(90deg) translate(0, -100%); 402 | transform: rotate(90deg) translate(0, -100%); 403 | } 404 | .tpd-stem-transform-bottom { 405 | -webkit-transform: scale(1, -1) translate(0, -100%); 406 | transform: scale(1, -1) translate(0, -100%); 407 | } 408 | 409 | /* Spinner */ 410 | .tpd-spinner { 411 | position: absolute; 412 | top: 50%; 413 | left: 50%; 414 | width: 46px; 415 | height: 36px; 416 | } 417 | .tpd-spinner-spin { 418 | position: relative; 419 | float: left; 420 | margin: 8px 0 0 13px; 421 | text-indent: -9999em; 422 | border-top: 2px solid rgba(255, 255, 255, 0.2); 423 | border-right: 2px solid rgba(255, 255, 255, 0.2); 424 | border-bottom: 2px solid rgba(255, 255, 255, 0.2); 425 | border-left: 2px solid #fff; 426 | -webkit-animation: tpd-spinner-animation 1.1s infinite linear; 427 | animation: tpd-spinner-animation 1.1s infinite linear; 428 | box-sizing: border-box !important; 429 | } 430 | .tpd-spinner-spin, 431 | .tpd-spinner-spin:after { 432 | border-radius: 50%; 433 | width: 20px; 434 | height: 20px; 435 | } 436 | @-webkit-keyframes tpd-spinner-animation { 437 | 0% { 438 | -webkit-transform: rotate(0deg); 439 | transform: rotate(0deg); 440 | } 441 | 100% { 442 | -webkit-transform: rotate(360deg); 443 | transform: rotate(360deg); 444 | } 445 | } 446 | @keyframes tpd-spinner-animation { 447 | 0% { 448 | -webkit-transform: rotate(0deg); 449 | transform: rotate(0deg); 450 | } 451 | 100% { 452 | -webkit-transform: rotate(360deg); 453 | transform: rotate(360deg); 454 | } 455 | } 456 | 457 | /* show the loader while loading and hide all the content */ 458 | .tpd-is-loading .tpd-content-wrapper, 459 | .tpd-is-loading .tpd-title-wrapper { 460 | display: none; 461 | } 462 | .tpd-is-loading .tpd-background { 463 | display: none; 464 | } 465 | .tpd-is-loading .tpd-background-loading { 466 | display: block; 467 | } 468 | 469 | /* Resets while measuring content */ 470 | .tpd-tooltip-measuring { 471 | top: 0; 472 | left: 0; 473 | position: absolute; 474 | max-width: 100%; 475 | width: 100%; 476 | } 477 | .tpd-tooltip-measuring .tpd-skin, 478 | .tpd-tooltip-measuring .tpd-spinner { 479 | display: none; 480 | } 481 | 482 | .tpd-tooltip-measuring .tpd-content-wrapper, 483 | .tpd-tooltip-measuring .tpd-title-wrapper { 484 | display: block; 485 | } 486 | 487 | /* Links */ 488 | .tpd-tooltip a, 489 | .tpd-tooltip a:hover { 490 | color: #808080; 491 | text-decoration: underline; 492 | } 493 | .tpd-tooltip a:hover { 494 | color: #6c6c6c; 495 | } 496 | 497 | /* 498 | * Sizes 499 | */ 500 | /* x-small */ 501 | .tpd-size-x-small .tpd-content, 502 | .tpd-size-x-small .tpd-title { 503 | padding: 7px 8px; 504 | font-size: 10px; 505 | line-height: 15px; 506 | } 507 | .tpd-size-x-small .tpd-background { 508 | border-radius: 5px; 509 | } 510 | .tpd-size-x-small .tpd-stem { 511 | width: 12px; 512 | height: 6px; 513 | margin-left: 4px; 514 | margin-top: 2px; /* space between target and stem */ 515 | } 516 | .tpd-size-x-small.tpd-no-radius .tpd-stem { 517 | margin-left: 7px; 518 | } 519 | .tpd-size-x-small .tpd-close { 520 | margin-bottom: 1px; 521 | } 522 | .tpd-size-x-small .tpd-spinner { 523 | width: 35px; 524 | height: 29px; 525 | } 526 | .tpd-size-x-small .tpd-spinner-spin { 527 | margin: 6px 0 0 9px; 528 | } 529 | .tpd-size-x-small .tpd-spinner-spin, 530 | .tpd-size-x-small .tpd-spinner-spin:after { 531 | width: 17px; 532 | height: 17px; 533 | } 534 | 535 | /* small */ 536 | .tpd-size-small .tpd-content, 537 | .tpd-size-small .tpd-title { 538 | padding: 8px; 539 | font-size: 10px; 540 | line-height: 16px; 541 | } 542 | .tpd-size-small .tpd-background { 543 | border-radius: 6px; 544 | } 545 | .tpd-size-small .tpd-stem { 546 | width: 14px; 547 | height: 7px; 548 | margin-left: 5px; 549 | margin-top: 2px; /* space between target and stem */ 550 | } 551 | .tpd-size-small.tpd-no-radius .tpd-stem { 552 | margin-left: 8px; 553 | } 554 | .tpd-size-small .tpd-close { 555 | margin: 2px 1px; 556 | } 557 | .tpd-size-small .tpd-spinner { 558 | width: 42px; 559 | height: 32px; 560 | } 561 | .tpd-size-small .tpd-spinner-spin { 562 | margin: 7px 0 0 13px; 563 | } 564 | .tpd-size-small .tpd-spinner-spin, 565 | .tpd-size-small .tpd-spinner-spin:after { 566 | width: 18px; 567 | height: 18px; 568 | } 569 | 570 | /* medium (default) */ 571 | .tpd-size-medium .tpd-content, 572 | .tpd-size-medium .tpd-title { 573 | padding: 10px; 574 | font-size: 11px; 575 | line-height: 16px; 576 | } 577 | .tpd-size-medium .tpd-background { 578 | border-radius: 8px; 579 | } 580 | .tpd-size-medium .tpd-stem { 581 | width: 16px; /* best cross browser stem width is 2xheight, for a 90deg angle */ 582 | height: 8px; 583 | margin-left: 6px; /* space from the side */ 584 | margin-top: 2px; /* space between target and stem */ 585 | } 586 | .tpd-size-medium.tpd-no-radius .tpd-stem { 587 | margin-left: 10px; 588 | } 589 | .tpd-size-medium .tpd-close { 590 | margin: 4px 2px; 591 | } 592 | /* ideal spinner dimensions don't cause movement op top and 593 | on the stem when switching to text using position:'topleft' */ 594 | .tpd-size-medium .tpd-spinner { 595 | width: 50px; 596 | height: 36px; 597 | } 598 | .tpd-size-medium .tpd-spinner-spin { 599 | margin: 8px 0 0 15px; 600 | } 601 | .tpd-size-medium .tpd-spinner-spin, 602 | .tpd-size-medium .tpd-spinner-spin:after { 603 | width: 20px; 604 | height: 20px; 605 | } 606 | 607 | /* large */ 608 | .tpd-size-large .tpd-content, 609 | .tpd-size-large .tpd-title { 610 | padding: 10px; 611 | font-size: 13px; 612 | line-height: 18px; 613 | } 614 | .tpd-size-large .tpd-background { 615 | border-radius: 8px; 616 | } 617 | .tpd-size-large .tpd-stem { 618 | width: 18px; 619 | height: 9px; 620 | margin-left: 7px; 621 | margin-top: 2px; /* space between target and stem */ 622 | } 623 | .tpd-size-large.tpd-no-radius .tpd-stem { 624 | margin-left: 10px; 625 | } 626 | .tpd-size-large .tpd-close { 627 | margin: 5px 2px 5px 2px; 628 | } 629 | .tpd-size-large .tpd-spinner { 630 | width: 54px; 631 | height: 38px; 632 | } 633 | .tpd-size-large .tpd-spinner-spin { 634 | margin: 9px 0 0 17px; 635 | } 636 | .tpd-size-large .tpd-spinner-spin, 637 | .tpd-size-large .tpd-spinner-spin:after { 638 | width: 20px; 639 | height: 20px; 640 | } 641 | 642 | /* Skins */ 643 | /* default (dark) */ 644 | .tpd-skin-dark .tpd-content, 645 | .tpd-skin-dark .tpd-title, 646 | .tpd-skin-dark .tpd-close { 647 | color: #fff; 648 | } 649 | .tpd-skin-dark .tpd-background-content, 650 | .tpd-skin-dark .tpd-background-title { 651 | background-color: #282828; 652 | } 653 | .tpd-skin-dark .tpd-background { 654 | border-width: 1px; 655 | border-color: rgba(255, 255, 255, 0.1); 656 | } 657 | /* line below the title */ 658 | .tpd-skin-dark .tpd-title-wrapper { 659 | border-bottom: 1px solid #404040; 660 | } 661 | /* spinner */ 662 | .tpd-skin-dark .tpd-spinner-spin { 663 | border-color: rgba(255, 255, 255, 0.2); 664 | border-left-color: #fff; 665 | } 666 | /* links */ 667 | .tpd-skin-dark a { 668 | color: #ccc; 669 | } 670 | .tpd-skin-dark a:hover { 671 | color: #c0c0c0; 672 | } 673 | 674 | /* light */ 675 | .tpd-skin-light .tpd-content, 676 | .tpd-skin-light .tpd-title, 677 | .tpd-skin-light .tpd-close { 678 | color: #333; 679 | } 680 | .tpd-skin-light .tpd-background-content { 681 | background-color: #fff; 682 | } 683 | .tpd-skin-light .tpd-background { 684 | border-width: 1px; 685 | border-color: rgba(0, 0, 0, 0.3); 686 | } 687 | .tpd-skin-light .tpd-background-title { 688 | background-color: #f7f7f7; 689 | } 690 | .tpd-skin-light .tpd-title-wrapper { 691 | border-bottom: 1px solid #c0c0c0; 692 | } 693 | .tpd-skin-light .tpd-background-shadow { 694 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 695 | } 696 | /* fallback for no/disabled shadow */ 697 | .tpd-skin-light.tpd-no-shadow .tpd-background { 698 | border-color: rgba(100, 100, 100, 0.3); 699 | } 700 | .tpd-skin-light .tpd-spinner-spin { 701 | border-color: rgba(51, 51, 51, 0.2); 702 | border-left-color: #333; 703 | } 704 | .tpd-skin-light a { 705 | color: #808080; 706 | } 707 | .tpd-skin-light a:hover { 708 | color: #6c6c6c; 709 | } 710 | 711 | /* gray */ 712 | .tpd-skin-gray .tpd-content, 713 | .tpd-skin-gray .tpd-title, 714 | .tpd-skin-gray .tpd-close { 715 | color: #fff; 716 | } 717 | .tpd-skin-gray .tpd-background-content, 718 | .tpd-skin-gray .tpd-background-title { 719 | background-color: #727272; 720 | } 721 | .tpd-skin-gray .tpd-background { 722 | border-width: 1px; 723 | border-color: rgba(255, 255, 255, 0.1); 724 | } 725 | .tpd-skin-gray .tpd-title-wrapper { 726 | border-bottom: 1px solid #505050; 727 | } 728 | .tpd-skin-gray .tpd-spinner-spin { 729 | border-color: rgba(255, 255, 255, 0.2); 730 | border-left-color: #fff; 731 | } 732 | .tpd-skin-gray a { 733 | color: #ccc; 734 | } 735 | .tpd-skin-gray a:hover { 736 | color: #b6b6b6; 737 | } 738 | 739 | /* red */ 740 | .tpd-skin-red .tpd-content, 741 | .tpd-skin-red .tpd-title, 742 | .tpd-skin-red .tpd-close { 743 | color: #fff; 744 | } 745 | .tpd-skin-red .tpd-background-content { 746 | background-color: #e13c37; 747 | } 748 | .tpd-skin-red .tpd-background { 749 | border-width: 1px; 750 | border-color: rgba(12, 0, 0, 0.6); 751 | } 752 | .tpd-skin-red .tpd-background-title { 753 | background-color: #e13c37; 754 | } 755 | .tpd-skin-red .tpd-title-wrapper { 756 | border-bottom: 1px solid #a30500; 757 | } 758 | .tpd-skin-red .tpd-background-shadow { 759 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 760 | } 761 | .tpd-skin-red .tpd-spinner-spin { 762 | border-color: rgba(255, 255, 255, 0.2); 763 | border-left-color: #fff; 764 | } 765 | .tpd-skin-red a { 766 | color: #ddd; 767 | } 768 | .tpd-skin-red a:hover { 769 | color: #c6c6c6; 770 | } 771 | 772 | /* green */ 773 | .tpd-skin-green .tpd-content, 774 | .tpd-skin-green .tpd-title, 775 | .tpd-skin-green .tpd-close { 776 | color: #fff; 777 | } 778 | .tpd-skin-green .tpd-background-content { 779 | background-color: #4aab3a; 780 | } 781 | .tpd-skin-green .tpd-background { 782 | border-width: 1px; 783 | border-color: rgba(0, 12, 0, 0.6); 784 | } 785 | .tpd-skin-green .tpd-background-title { 786 | background-color: #4aab3a; 787 | } 788 | .tpd-skin-green .tpd-title-wrapper { 789 | border-bottom: 1px solid #127c00; 790 | } 791 | .tpd-skin-green .tpd-background-shadow { 792 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 793 | } 794 | .tpd-skin-green .tpd-spinner-spin { 795 | border-color: rgba(255, 255, 255, 0.2); 796 | border-left-color: #fff; 797 | } 798 | .tpd-skin-green a { 799 | color: #ddd; 800 | } 801 | .tpd-skin-green a:hover { 802 | color: #c6c6c6; 803 | } 804 | 805 | /* blue */ 806 | .tpd-skin-blue .tpd-content, 807 | .tpd-skin-blue .tpd-title, 808 | .tpd-skin-blue .tpd-close { 809 | color: #fff; 810 | } 811 | .tpd-skin-blue .tpd-background-content { 812 | background-color: #45a3e3; 813 | } 814 | .tpd-skin-blue .tpd-background { 815 | border-width: 1px; 816 | border-color: rgba(0, 0, 12, 0.6); 817 | } 818 | .tpd-skin-blue .tpd-background-title { 819 | background-color: #45a3e3; 820 | } 821 | .tpd-skin-blue .tpd-title-wrapper { 822 | border-bottom: 1px solid #1674b4; 823 | } 824 | .tpd-skin-blue .tpd-background-shadow { 825 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 826 | } 827 | .tpd-skin-blue .tpd-spinner-spin { 828 | border-color: rgba(255, 255, 255, 0.2); 829 | border-left-color: #fff; 830 | } 831 | .tpd-skin-blue a { 832 | color: #ddd; 833 | } 834 | .tpd-skin-blue a:hover { 835 | color: #c6c6c6; 836 | } 837 | 838 | /* lightyellow */ 839 | .tpd-skin-lightyellow .tpd-content, 840 | .tpd-skin-lightyellow .tpd-title, 841 | .tpd-skin-lightyellow .tpd-close { 842 | color: #333; 843 | } 844 | .tpd-skin-lightyellow .tpd-background-content { 845 | background-color: #ffffa9; 846 | } 847 | .tpd-skin-lightyellow .tpd-background { 848 | border-width: 1px; 849 | border-color: rgba(8, 8, 0, 0.35); 850 | } 851 | .tpd-skin-lightyellow .tpd-background-title { 852 | background-color: #ffffa9; 853 | } 854 | .tpd-skin-lightyellow .tpd-title-wrapper { 855 | border-bottom: 1px solid #a7a697; 856 | } 857 | .tpd-skin-lightyellow .tpd-background-shadow { 858 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 859 | } 860 | .tpd-skin-lightyellow .tpd-spinner-spin { 861 | border-color: rgba(51, 51, 51, 0.2); 862 | border-left-color: #333; 863 | } 864 | .tpd-skin-lightyellow a { 865 | color: #777; 866 | } 867 | .tpd-skin-lightyellow a:hover { 868 | color: #868686; 869 | } 870 | 871 | /* lightblue */ 872 | .tpd-skin-lightblue .tpd-content, 873 | .tpd-skin-lightblue .tpd-title, 874 | .tpd-skin-lightblue .tpd-close { 875 | color: #333; 876 | } 877 | .tpd-skin-lightblue .tpd-background-content { 878 | background-color: #bce5ff; 879 | } 880 | .tpd-skin-lightblue .tpd-background { 881 | border-width: 1px; 882 | border-color: rgba(0, 0, 8, 0.35); 883 | } 884 | .tpd-skin-lightblue .tpd-background-title { 885 | background-color: #bce5ff; 886 | } 887 | .tpd-skin-lightblue .tpd-title-wrapper { 888 | border-bottom: 1px solid #909b9f; 889 | } 890 | .tpd-skin-lightblue .tpd-background-shadow { 891 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 892 | } 893 | .tpd-skin-lightblue .tpd-spinner-spin { 894 | border-color: rgba(51, 51, 51, 0.2); 895 | border-left-color: #333; 896 | } 897 | .tpd-skin-lightblue a { 898 | color: #777; 899 | } 900 | .tpd-skin-lightblue a:hover { 901 | color: #868686; 902 | } 903 | 904 | /* lightpink */ 905 | .tpd-skin-lightpink .tpd-content, 906 | .tpd-skin-lightpink .tpd-title, 907 | .tpd-skin-lightpink .tpd-close { 908 | color: #333; 909 | } 910 | .tpd-skin-lightpink .tpd-background-content { 911 | background-color: #ffc4bf; 912 | } 913 | .tpd-skin-lightpink .tpd-background { 914 | border-width: 1px; 915 | border-color: rgba(8, 0, 0, 0.35); 916 | } 917 | .tpd-skin-lightpink .tpd-background-title { 918 | background-color: #ffc4bf; 919 | } 920 | .tpd-skin-lightpink .tpd-title-wrapper { 921 | border-bottom: 1px solid #a08f8f; 922 | } 923 | .tpd-skin-lightpink .tpd-background-shadow { 924 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 925 | } 926 | .tpd-skin-lightpink .tpd-spinner-spin { 927 | border-color: rgba(51, 51, 51, 0.2); 928 | border-left-color: #333; 929 | } 930 | .tpd-skin-lightpink a { 931 | color: #777; 932 | } 933 | .tpd-skin-lightpink a:hover { 934 | color: #868686; 935 | } 936 | -------------------------------------------------------------------------------- /example/css/style.css: -------------------------------------------------------------------------------- 1 | /* RESET */ 2 | html, 3 | body, 4 | div, 5 | ul, 6 | ol, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | h5, 16 | h6, 17 | pre, 18 | form, 19 | p, 20 | blockquote, 21 | fieldset, 22 | input { 23 | margin: 0; 24 | padding: 0; 25 | } 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | pre, 33 | code, 34 | address, 35 | caption, 36 | cite, 37 | code, 38 | th { 39 | font-size: 1em; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | ul, 44 | ol { 45 | list-style: none; 46 | } 47 | fieldset, 48 | img { 49 | border: none; 50 | } 51 | caption, 52 | th { 53 | text-align: left; 54 | } 55 | table { 56 | border-collapse: collapse; 57 | border-spacing: 0; 58 | } 59 | 60 | html { 61 | font: 14px/22px "BemboStd-Regular", Georgia, Times, "Times New Roman", serif; 62 | } 63 | body { 64 | font: 14px/22px "BemboStd-Regular", Georgia, Times, "Times New Roman", serif; 65 | background-color: #646461; 66 | background: #fff; 67 | color: #333; 68 | } 69 | p { 70 | margin-bottom: 32px; 71 | clear: both; 72 | } 73 | a { 74 | color: #0088cc; 75 | text-decoration: none; 76 | outline-style: none; 77 | background: #f0f8fd; 78 | } 79 | a:hover { 80 | text-decoration: underline; 81 | } 82 | 83 | /* box-sizing*/ 84 | html { 85 | box-sizing: border-box; 86 | } 87 | *, 88 | *:before, 89 | *:after { 90 | box-sizing: inherit; 91 | } 92 | 93 | #page { 94 | clear: both; 95 | width: 100%; 96 | padding: 5rem 2rem; 97 | font-size: inherit; 98 | } 99 | 100 | .demonstrations { 101 | display: flex; 102 | flex-wrap: wrap; 103 | width: 100%; 104 | max-width: 300px; 105 | } 106 | .demonstrations .box { 107 | position: relative; 108 | overflow: hidden; 109 | height: 60px; 110 | width: calc((100% - (6 * 5px)) / 3); 111 | margin: 5px; 112 | line-height: 60px; 113 | background: #ccc; 114 | cursor: pointer; 115 | font-style: italic; 116 | font-family: Georgia, Times, "Times New Roman", serif; 117 | color: #444; 118 | text-shadow: 0 1px 0 rgb(255 255 255); 119 | text-align: center; 120 | } 121 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tipped 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 42 | 43 | 44 | 45 |
46 |
47 |
small
48 |
medium
49 |
large
50 | 51 |
x-small
52 |
medium ×
53 |
large
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@staaky/tipped", 3 | "title": "Tipped", 4 | "version": "4.8.1", 5 | "description": "A complete Tooltip solution based on jQuery", 6 | "author": { 7 | "name": "Nick Stakenburg", 8 | "url": "https://github.com/staaky" 9 | }, 10 | "main": "dist/js/tipped.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/staaky/tipped.git" 14 | }, 15 | "keywords": [ 16 | "tooltip", 17 | "tooltips", 18 | "responsive", 19 | "jquery", 20 | "jquery-plugin" 21 | ], 22 | "license": "CC-BY-4.0", 23 | "scripts": { 24 | "build": "grunt", 25 | "update": "ncu -u" 26 | }, 27 | "devDependencies": { 28 | "grunt": "^1.4.1", 29 | "grunt-contrib-clean": "^2.0.0", 30 | "grunt-contrib-concat": "^1.0.1", 31 | "grunt-contrib-copy": "^1.0.0", 32 | "grunt-contrib-uglify": "^5.0.1", 33 | "grunt-contrib-watch": "^1.1.0", 34 | "npm-check-updates": "^11.8.5", 35 | "prettier": "2.4.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/css/tipped.css: -------------------------------------------------------------------------------- 1 | .tpd-tooltip { 2 | position: absolute; 3 | } 4 | 5 | /* Fix for CSS frameworks that don't keep the use of box-sizing: border-box 6 | within their own namespace */ 7 | .tpd-tooltip { 8 | box-sizing: content-box; 9 | } 10 | .tpd-tooltip [class^="tpd-"] { 11 | box-sizing: inherit; 12 | } 13 | 14 | /* Content */ 15 | .tpd-content-wrapper { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | float: left; 20 | width: 100%; 21 | height: 100%; 22 | overflow: hidden; 23 | } 24 | .tpd-content-spacer, 25 | .tpd-content-relative, 26 | .tpd-content-relative-padder { 27 | float: left; 28 | position: relative; 29 | } 30 | .tpd-content-relative { 31 | width: 100%; 32 | } 33 | 34 | .tpd-content { 35 | float: left; 36 | clear: both; 37 | position: relative; 38 | padding: 10px; 39 | font-size: 11px; 40 | line-height: 16px; 41 | color: #fff; 42 | box-sizing: border-box !important; 43 | } 44 | .tpd-has-inner-close .tpd-content-relative .tpd-content { 45 | padding-right: 0 !important; 46 | } 47 | .tpd-tooltip .tpd-content-no-padding { 48 | padding: 0 !important; 49 | } 50 | 51 | .tpd-title-wrapper { 52 | float: left; 53 | position: relative; 54 | overflow: hidden; 55 | } 56 | .tpd-title-spacer { 57 | float: left; 58 | } 59 | .tpd-title-relative, 60 | .tpd-title-relative-padder { 61 | float: left; 62 | position: relative; 63 | } 64 | .tpd-title-relative { 65 | width: 100%; 66 | } 67 | .tpd-title { 68 | float: left; 69 | position: relative; 70 | font-size: 11px; 71 | line-height: 16px; 72 | padding: 10px; 73 | font-weight: bold; 74 | text-transform: uppercase; 75 | color: #fff; 76 | box-sizing: border-box !important; 77 | } 78 | .tpd-has-title-close .tpd-title { 79 | padding-right: 0 !important; 80 | } 81 | .tpd-close { 82 | position: absolute; 83 | top: 0; 84 | right: 0; 85 | width: 28px; 86 | height: 28px; 87 | cursor: pointer; 88 | overflow: hidden; 89 | color: #fff; 90 | } 91 | .tpd-close-icon { 92 | float: left; 93 | font-family: Arial, Baskerville, monospace; 94 | font-weight: normal; 95 | font-style: normal; 96 | text-decoration: none; 97 | width: 28px; 98 | height: 28px; 99 | font-size: 28px; 100 | line-height: 28px; 101 | text-align: center; 102 | } 103 | 104 | /* Skin */ 105 | .tpd-skin { 106 | position: absolute; 107 | top: 0; 108 | left: 0; 109 | } 110 | 111 | .tpd-frames { 112 | position: absolute; 113 | top: 0; 114 | left: 0; 115 | } 116 | .tpd-frames .tpd-frame { 117 | float: left; 118 | width: 100%; 119 | height: 100%; 120 | clear: both; 121 | display: none; 122 | } 123 | 124 | .tpd-visible-frame-top .tpd-frame-top { 125 | display: block; 126 | } 127 | .tpd-visible-frame-bottom .tpd-frame-bottom { 128 | display: block; 129 | } 130 | .tpd-visible-frame-left .tpd-frame-left { 131 | display: block; 132 | } 133 | .tpd-visible-frame-right .tpd-frame-right { 134 | display: block; 135 | } 136 | 137 | .tpd-backgrounds { 138 | position: absolute; 139 | top: 0; 140 | left: 0; 141 | width: 100%; 142 | height: 100%; 143 | -webkit-transform-origin: 0% 0%; 144 | transform-origin: 0% 0%; 145 | } 146 | .tpd-background-shadow { 147 | position: absolute; 148 | top: 0; 149 | left: 0; 150 | width: 100%; 151 | height: 100%; 152 | background-color: transparent; 153 | pointer-events: none; 154 | } 155 | .tpd-no-shadow .tpd-skin .tpd-background-shadow { 156 | box-shadow: none !important; 157 | } 158 | 159 | .tpd-background-box { 160 | position: absolute; 161 | top: 0; 162 | left: 0; 163 | height: 100%; 164 | width: 100%; 165 | overflow: hidden; 166 | } 167 | /* only the top background box should be shown when not using a stem */ 168 | .tpd-no-stem .tpd-background-box, 169 | .tpd-no-stem .tpd-shift-stem { 170 | display: none; 171 | } 172 | .tpd-no-stem .tpd-background-box-top { 173 | display: block; 174 | } 175 | 176 | .tpd-background-box-shift, 177 | .tpd-background-box-shift-further { 178 | position: relative; 179 | float: left; 180 | width: 100%; 181 | height: 100%; 182 | } 183 | .tpd-background { 184 | border-radius: 10px; 185 | float: left; 186 | clear: both; 187 | background: none; 188 | -webkit-background-clip: padding-box; /* Safari */ 189 | background-clip: padding-box; /* IE9+, Firefox 4+, Opera, Chrome */ 190 | border-style: solid; 191 | border-width: 1px; 192 | border-color: rgba( 193 | 255, 194 | 255, 195 | 255, 196 | 0.1 197 | ); /* opacity here bugs out in firefox, .tpd-background-content should have no opacity if this opacity is less than 1 */ 198 | } 199 | .tpd-background-loading { 200 | display: none; 201 | } 202 | /* no radius */ 203 | .tpd-no-radius 204 | .tpd-skin 205 | .tpd-frames 206 | .tpd-frame 207 | .tpd-backgrounds 208 | .tpd-background { 209 | border-radius: 0; 210 | } 211 | .tpd-background-title { 212 | float: left; 213 | clear: both; 214 | width: 100%; 215 | background-color: #282828; 216 | } 217 | .tpd-background-content { 218 | float: left; 219 | clear: both; 220 | width: 100%; 221 | background-color: #282828; 222 | } 223 | .tpd-background-border-hack { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | width: 100%; 228 | height: 100%; 229 | border-style: solid; 230 | } 231 | 232 | .tpd-background-box-top { 233 | top: 0; 234 | } 235 | .tpd-background-box-bottom { 236 | bottom: 0; 237 | } 238 | .tpd-background-box-left { 239 | left: 0; 240 | } 241 | .tpd-background-box-right { 242 | right: 0; 243 | } 244 | 245 | /* Skin / Stems */ 246 | .tpd-shift-stem { 247 | position: absolute; 248 | top: 0; 249 | left: 0; 250 | overflow: hidden; 251 | } 252 | .tpd-shift-stem-side { 253 | position: absolute; 254 | } 255 | .tpd-frame-top .tpd-shift-stem-side, 256 | .tpd-frame-bottom .tpd-shift-stem-side { 257 | width: 100%; 258 | } 259 | .tpd-frame-left .tpd-shift-stem-side, 260 | .tpd-frame-right .tpd-shift-stem-side { 261 | height: 100%; 262 | } 263 | 264 | .tpd-stem { 265 | position: absolute; 266 | top: 0; 267 | left: 0; 268 | overflow: hidden; /* shows possible invalid subpx rendering */ 269 | width: 16px; /* best cross browser stem: width = 2 x height (90deg angle) */ 270 | height: 8px; 271 | margin-left: 3px; /* space from the side */ 272 | margin-top: 2px; /* space between target and stem */ 273 | -webkit-transform-origin: 0% 0%; 274 | transform-origin: 0% 0%; 275 | } 276 | /* remove margins once we're done measuring */ 277 | .tpd-tooltip .tpd-skin .tpd-frames .tpd-frame .tpd-shift-stem .tpd-stem-reset { 278 | margin: 0 !important; 279 | } 280 | 281 | .tpd-stem-spacer { 282 | position: absolute; 283 | top: 0; 284 | left: 0; 285 | width: 100%; 286 | height: 100%; 287 | } 288 | .tpd-stem-reset .tpd-stem-spacer { 289 | margin-top: 0; 290 | } 291 | 292 | .tpd-stem-point { 293 | width: 100px; 294 | position: absolute; 295 | top: 0; 296 | left: 50%; 297 | } 298 | .tpd-stem-downscale, 299 | .tpd-stem-transform { 300 | float: left; 301 | width: 100%; 302 | height: 100%; 303 | -webkit-transform-origin: 0% 0%; 304 | transform-origin: 0% 0%; 305 | position: relative; 306 | } 307 | 308 | .tpd-stem-side { 309 | width: 50%; 310 | height: 100%; 311 | float: left; 312 | position: relative; 313 | overflow: hidden; 314 | } 315 | .tpd-stem-side-inversed { 316 | -webkit-transform: scale(-1, 1); 317 | transform: scale(-1, 1); 318 | } 319 | .tpd-stem-triangle { 320 | width: 0; 321 | height: 0; 322 | border-bottom-style: solid; 323 | border-left-color: transparent; 324 | border-left-style: solid; 325 | position: absolute; 326 | top: 0; 327 | left: 0; 328 | } 329 | .tpd-stem-border { 330 | width: 20px; 331 | height: 100%; 332 | position: absolute; 333 | top: 0; 334 | left: 50%; 335 | background-color: #fff; /* will become transparent */ 336 | border-right-color: #fff; 337 | border-right-style: solid; 338 | border-right-width: 0; 339 | } 340 | 341 | .tpd-stem-border-corner { 342 | position: absolute; 343 | top: 0; 344 | left: 50%; 345 | height: 100%; 346 | border-right-style: solid; 347 | border-right-width: 0; 348 | } 349 | 350 | /* fixes rendering issue in IE */ 351 | .tpd-stem * { 352 | z-index: 0; 353 | zoom: 1; 354 | } 355 | 356 | /* used by IE < 9 */ 357 | .tpd-stem-border-center-offset, 358 | .tpd-stem-border-center-offset-inverse { 359 | float: left; 360 | position: relative; 361 | width: 100%; 362 | height: 100%; 363 | overflow: hidden; 364 | } 365 | .tpd-stem-notransform { 366 | float: left; 367 | width: 100%; 368 | height: 100%; 369 | position: relative; 370 | } 371 | .tpd-stem-notransform .tpd-stem-border { 372 | height: 100%; 373 | position: relative; 374 | float: left; 375 | top: 0; 376 | left: 0; 377 | margin: 0; 378 | } 379 | .tpd-stem-notransform .tpd-stem-border-center { 380 | position: absolute; 381 | } 382 | .tpd-stem-notransform .tpd-stem-border-corner { 383 | background: #fff; 384 | border: 0; 385 | top: auto; 386 | left: auto; 387 | } 388 | .tpd-stem-notransform .tpd-stem-border-center, 389 | .tpd-stem-notransform .tpd-stem-triangle { 390 | height: 0; 391 | border: 0; 392 | left: 50%; 393 | } 394 | 395 | /* transformations for left/right/bottom */ 396 | .tpd-stem-transform-left { 397 | -webkit-transform: rotate(-90deg) scale(-1, 1); 398 | transform: rotate(-90deg) scale(-1, 1); 399 | } 400 | .tpd-stem-transform-right { 401 | -webkit-transform: rotate(90deg) translate(0, -100%); 402 | transform: rotate(90deg) translate(0, -100%); 403 | } 404 | .tpd-stem-transform-bottom { 405 | -webkit-transform: scale(1, -1) translate(0, -100%); 406 | transform: scale(1, -1) translate(0, -100%); 407 | } 408 | 409 | /* Spinner */ 410 | .tpd-spinner { 411 | position: absolute; 412 | top: 50%; 413 | left: 50%; 414 | width: 46px; 415 | height: 36px; 416 | } 417 | .tpd-spinner-spin { 418 | position: relative; 419 | float: left; 420 | margin: 8px 0 0 13px; 421 | text-indent: -9999em; 422 | border-top: 2px solid rgba(255, 255, 255, 0.2); 423 | border-right: 2px solid rgba(255, 255, 255, 0.2); 424 | border-bottom: 2px solid rgba(255, 255, 255, 0.2); 425 | border-left: 2px solid #fff; 426 | -webkit-animation: tpd-spinner-animation 1.1s infinite linear; 427 | animation: tpd-spinner-animation 1.1s infinite linear; 428 | box-sizing: border-box !important; 429 | } 430 | .tpd-spinner-spin, 431 | .tpd-spinner-spin:after { 432 | border-radius: 50%; 433 | width: 20px; 434 | height: 20px; 435 | } 436 | @-webkit-keyframes tpd-spinner-animation { 437 | 0% { 438 | -webkit-transform: rotate(0deg); 439 | transform: rotate(0deg); 440 | } 441 | 100% { 442 | -webkit-transform: rotate(360deg); 443 | transform: rotate(360deg); 444 | } 445 | } 446 | @keyframes tpd-spinner-animation { 447 | 0% { 448 | -webkit-transform: rotate(0deg); 449 | transform: rotate(0deg); 450 | } 451 | 100% { 452 | -webkit-transform: rotate(360deg); 453 | transform: rotate(360deg); 454 | } 455 | } 456 | 457 | /* show the loader while loading and hide all the content */ 458 | .tpd-is-loading .tpd-content-wrapper, 459 | .tpd-is-loading .tpd-title-wrapper { 460 | display: none; 461 | } 462 | .tpd-is-loading .tpd-background { 463 | display: none; 464 | } 465 | .tpd-is-loading .tpd-background-loading { 466 | display: block; 467 | } 468 | 469 | /* Resets while measuring content */ 470 | .tpd-tooltip-measuring { 471 | top: 0; 472 | left: 0; 473 | position: absolute; 474 | max-width: 100%; 475 | width: 100%; 476 | } 477 | .tpd-tooltip-measuring .tpd-skin, 478 | .tpd-tooltip-measuring .tpd-spinner { 479 | display: none; 480 | } 481 | 482 | .tpd-tooltip-measuring .tpd-content-wrapper, 483 | .tpd-tooltip-measuring .tpd-title-wrapper { 484 | display: block; 485 | } 486 | 487 | /* Links */ 488 | .tpd-tooltip a, 489 | .tpd-tooltip a:hover { 490 | color: #808080; 491 | text-decoration: underline; 492 | } 493 | .tpd-tooltip a:hover { 494 | color: #6c6c6c; 495 | } 496 | 497 | /* 498 | * Sizes 499 | */ 500 | /* x-small */ 501 | .tpd-size-x-small .tpd-content, 502 | .tpd-size-x-small .tpd-title { 503 | padding: 7px 8px; 504 | font-size: 10px; 505 | line-height: 15px; 506 | } 507 | .tpd-size-x-small .tpd-background { 508 | border-radius: 5px; 509 | } 510 | .tpd-size-x-small .tpd-stem { 511 | width: 12px; 512 | height: 6px; 513 | margin-left: 4px; 514 | margin-top: 2px; /* space between target and stem */ 515 | } 516 | .tpd-size-x-small.tpd-no-radius .tpd-stem { 517 | margin-left: 7px; 518 | } 519 | .tpd-size-x-small .tpd-close { 520 | margin-bottom: 1px; 521 | } 522 | .tpd-size-x-small .tpd-spinner { 523 | width: 35px; 524 | height: 29px; 525 | } 526 | .tpd-size-x-small .tpd-spinner-spin { 527 | margin: 6px 0 0 9px; 528 | } 529 | .tpd-size-x-small .tpd-spinner-spin, 530 | .tpd-size-x-small .tpd-spinner-spin:after { 531 | width: 17px; 532 | height: 17px; 533 | } 534 | 535 | /* small */ 536 | .tpd-size-small .tpd-content, 537 | .tpd-size-small .tpd-title { 538 | padding: 8px; 539 | font-size: 10px; 540 | line-height: 16px; 541 | } 542 | .tpd-size-small .tpd-background { 543 | border-radius: 6px; 544 | } 545 | .tpd-size-small .tpd-stem { 546 | width: 14px; 547 | height: 7px; 548 | margin-left: 5px; 549 | margin-top: 2px; /* space between target and stem */ 550 | } 551 | .tpd-size-small.tpd-no-radius .tpd-stem { 552 | margin-left: 8px; 553 | } 554 | .tpd-size-small .tpd-close { 555 | margin: 2px 1px; 556 | } 557 | .tpd-size-small .tpd-spinner { 558 | width: 42px; 559 | height: 32px; 560 | } 561 | .tpd-size-small .tpd-spinner-spin { 562 | margin: 7px 0 0 13px; 563 | } 564 | .tpd-size-small .tpd-spinner-spin, 565 | .tpd-size-small .tpd-spinner-spin:after { 566 | width: 18px; 567 | height: 18px; 568 | } 569 | 570 | /* medium (default) */ 571 | .tpd-size-medium .tpd-content, 572 | .tpd-size-medium .tpd-title { 573 | padding: 10px; 574 | font-size: 11px; 575 | line-height: 16px; 576 | } 577 | .tpd-size-medium .tpd-background { 578 | border-radius: 8px; 579 | } 580 | .tpd-size-medium .tpd-stem { 581 | width: 16px; /* best cross browser stem width is 2xheight, for a 90deg angle */ 582 | height: 8px; 583 | margin-left: 6px; /* space from the side */ 584 | margin-top: 2px; /* space between target and stem */ 585 | } 586 | .tpd-size-medium.tpd-no-radius .tpd-stem { 587 | margin-left: 10px; 588 | } 589 | .tpd-size-medium .tpd-close { 590 | margin: 4px 2px; 591 | } 592 | /* ideal spinner dimensions don't cause movement op top and 593 | on the stem when switching to text using position:'topleft' */ 594 | .tpd-size-medium .tpd-spinner { 595 | width: 50px; 596 | height: 36px; 597 | } 598 | .tpd-size-medium .tpd-spinner-spin { 599 | margin: 8px 0 0 15px; 600 | } 601 | .tpd-size-medium .tpd-spinner-spin, 602 | .tpd-size-medium .tpd-spinner-spin:after { 603 | width: 20px; 604 | height: 20px; 605 | } 606 | 607 | /* large */ 608 | .tpd-size-large .tpd-content, 609 | .tpd-size-large .tpd-title { 610 | padding: 10px; 611 | font-size: 13px; 612 | line-height: 18px; 613 | } 614 | .tpd-size-large .tpd-background { 615 | border-radius: 8px; 616 | } 617 | .tpd-size-large .tpd-stem { 618 | width: 18px; 619 | height: 9px; 620 | margin-left: 7px; 621 | margin-top: 2px; /* space between target and stem */ 622 | } 623 | .tpd-size-large.tpd-no-radius .tpd-stem { 624 | margin-left: 10px; 625 | } 626 | .tpd-size-large .tpd-close { 627 | margin: 5px 2px 5px 2px; 628 | } 629 | .tpd-size-large .tpd-spinner { 630 | width: 54px; 631 | height: 38px; 632 | } 633 | .tpd-size-large .tpd-spinner-spin { 634 | margin: 9px 0 0 17px; 635 | } 636 | .tpd-size-large .tpd-spinner-spin, 637 | .tpd-size-large .tpd-spinner-spin:after { 638 | width: 20px; 639 | height: 20px; 640 | } 641 | 642 | /* Skins */ 643 | /* default (dark) */ 644 | .tpd-skin-dark .tpd-content, 645 | .tpd-skin-dark .tpd-title, 646 | .tpd-skin-dark .tpd-close { 647 | color: #fff; 648 | } 649 | .tpd-skin-dark .tpd-background-content, 650 | .tpd-skin-dark .tpd-background-title { 651 | background-color: #282828; 652 | } 653 | .tpd-skin-dark .tpd-background { 654 | border-width: 1px; 655 | border-color: rgba(255, 255, 255, 0.1); 656 | } 657 | /* line below the title */ 658 | .tpd-skin-dark .tpd-title-wrapper { 659 | border-bottom: 1px solid #404040; 660 | } 661 | /* spinner */ 662 | .tpd-skin-dark .tpd-spinner-spin { 663 | border-color: rgba(255, 255, 255, 0.2); 664 | border-left-color: #fff; 665 | } 666 | /* links */ 667 | .tpd-skin-dark a { 668 | color: #ccc; 669 | } 670 | .tpd-skin-dark a:hover { 671 | color: #c0c0c0; 672 | } 673 | 674 | /* light */ 675 | .tpd-skin-light .tpd-content, 676 | .tpd-skin-light .tpd-title, 677 | .tpd-skin-light .tpd-close { 678 | color: #333; 679 | } 680 | .tpd-skin-light .tpd-background-content { 681 | background-color: #fff; 682 | } 683 | .tpd-skin-light .tpd-background { 684 | border-width: 1px; 685 | border-color: rgba(0, 0, 0, 0.3); 686 | } 687 | .tpd-skin-light .tpd-background-title { 688 | background-color: #f7f7f7; 689 | } 690 | .tpd-skin-light .tpd-title-wrapper { 691 | border-bottom: 1px solid #c0c0c0; 692 | } 693 | .tpd-skin-light .tpd-background-shadow { 694 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 695 | } 696 | /* fallback for no/disabled shadow */ 697 | .tpd-skin-light.tpd-no-shadow .tpd-background { 698 | border-color: rgba(100, 100, 100, 0.3); 699 | } 700 | .tpd-skin-light .tpd-spinner-spin { 701 | border-color: rgba(51, 51, 51, 0.2); 702 | border-left-color: #333; 703 | } 704 | .tpd-skin-light a { 705 | color: #808080; 706 | } 707 | .tpd-skin-light a:hover { 708 | color: #6c6c6c; 709 | } 710 | 711 | /* gray */ 712 | .tpd-skin-gray .tpd-content, 713 | .tpd-skin-gray .tpd-title, 714 | .tpd-skin-gray .tpd-close { 715 | color: #fff; 716 | } 717 | .tpd-skin-gray .tpd-background-content, 718 | .tpd-skin-gray .tpd-background-title { 719 | background-color: #727272; 720 | } 721 | .tpd-skin-gray .tpd-background { 722 | border-width: 1px; 723 | border-color: rgba(255, 255, 255, 0.1); 724 | } 725 | .tpd-skin-gray .tpd-title-wrapper { 726 | border-bottom: 1px solid #505050; 727 | } 728 | .tpd-skin-gray .tpd-spinner-spin { 729 | border-color: rgba(255, 255, 255, 0.2); 730 | border-left-color: #fff; 731 | } 732 | .tpd-skin-gray a { 733 | color: #ccc; 734 | } 735 | .tpd-skin-gray a:hover { 736 | color: #b6b6b6; 737 | } 738 | 739 | /* red */ 740 | .tpd-skin-red .tpd-content, 741 | .tpd-skin-red .tpd-title, 742 | .tpd-skin-red .tpd-close { 743 | color: #fff; 744 | } 745 | .tpd-skin-red .tpd-background-content { 746 | background-color: #e13c37; 747 | } 748 | .tpd-skin-red .tpd-background { 749 | border-width: 1px; 750 | border-color: rgba(12, 0, 0, 0.6); 751 | } 752 | .tpd-skin-red .tpd-background-title { 753 | background-color: #e13c37; 754 | } 755 | .tpd-skin-red .tpd-title-wrapper { 756 | border-bottom: 1px solid #a30500; 757 | } 758 | .tpd-skin-red .tpd-background-shadow { 759 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 760 | } 761 | .tpd-skin-red .tpd-spinner-spin { 762 | border-color: rgba(255, 255, 255, 0.2); 763 | border-left-color: #fff; 764 | } 765 | .tpd-skin-red a { 766 | color: #ddd; 767 | } 768 | .tpd-skin-red a:hover { 769 | color: #c6c6c6; 770 | } 771 | 772 | /* green */ 773 | .tpd-skin-green .tpd-content, 774 | .tpd-skin-green .tpd-title, 775 | .tpd-skin-green .tpd-close { 776 | color: #fff; 777 | } 778 | .tpd-skin-green .tpd-background-content { 779 | background-color: #4aab3a; 780 | } 781 | .tpd-skin-green .tpd-background { 782 | border-width: 1px; 783 | border-color: rgba(0, 12, 0, 0.6); 784 | } 785 | .tpd-skin-green .tpd-background-title { 786 | background-color: #4aab3a; 787 | } 788 | .tpd-skin-green .tpd-title-wrapper { 789 | border-bottom: 1px solid #127c00; 790 | } 791 | .tpd-skin-green .tpd-background-shadow { 792 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 793 | } 794 | .tpd-skin-green .tpd-spinner-spin { 795 | border-color: rgba(255, 255, 255, 0.2); 796 | border-left-color: #fff; 797 | } 798 | .tpd-skin-green a { 799 | color: #ddd; 800 | } 801 | .tpd-skin-green a:hover { 802 | color: #c6c6c6; 803 | } 804 | 805 | /* blue */ 806 | .tpd-skin-blue .tpd-content, 807 | .tpd-skin-blue .tpd-title, 808 | .tpd-skin-blue .tpd-close { 809 | color: #fff; 810 | } 811 | .tpd-skin-blue .tpd-background-content { 812 | background-color: #45a3e3; 813 | } 814 | .tpd-skin-blue .tpd-background { 815 | border-width: 1px; 816 | border-color: rgba(0, 0, 12, 0.6); 817 | } 818 | .tpd-skin-blue .tpd-background-title { 819 | background-color: #45a3e3; 820 | } 821 | .tpd-skin-blue .tpd-title-wrapper { 822 | border-bottom: 1px solid #1674b4; 823 | } 824 | .tpd-skin-blue .tpd-background-shadow { 825 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 826 | } 827 | .tpd-skin-blue .tpd-spinner-spin { 828 | border-color: rgba(255, 255, 255, 0.2); 829 | border-left-color: #fff; 830 | } 831 | .tpd-skin-blue a { 832 | color: #ddd; 833 | } 834 | .tpd-skin-blue a:hover { 835 | color: #c6c6c6; 836 | } 837 | 838 | /* lightyellow */ 839 | .tpd-skin-lightyellow .tpd-content, 840 | .tpd-skin-lightyellow .tpd-title, 841 | .tpd-skin-lightyellow .tpd-close { 842 | color: #333; 843 | } 844 | .tpd-skin-lightyellow .tpd-background-content { 845 | background-color: #ffffa9; 846 | } 847 | .tpd-skin-lightyellow .tpd-background { 848 | border-width: 1px; 849 | border-color: rgba(8, 8, 0, 0.35); 850 | } 851 | .tpd-skin-lightyellow .tpd-background-title { 852 | background-color: #ffffa9; 853 | } 854 | .tpd-skin-lightyellow .tpd-title-wrapper { 855 | border-bottom: 1px solid #a7a697; 856 | } 857 | .tpd-skin-lightyellow .tpd-background-shadow { 858 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 859 | } 860 | .tpd-skin-lightyellow .tpd-spinner-spin { 861 | border-color: rgba(51, 51, 51, 0.2); 862 | border-left-color: #333; 863 | } 864 | .tpd-skin-lightyellow a { 865 | color: #777; 866 | } 867 | .tpd-skin-lightyellow a:hover { 868 | color: #868686; 869 | } 870 | 871 | /* lightblue */ 872 | .tpd-skin-lightblue .tpd-content, 873 | .tpd-skin-lightblue .tpd-title, 874 | .tpd-skin-lightblue .tpd-close { 875 | color: #333; 876 | } 877 | .tpd-skin-lightblue .tpd-background-content { 878 | background-color: #bce5ff; 879 | } 880 | .tpd-skin-lightblue .tpd-background { 881 | border-width: 1px; 882 | border-color: rgba(0, 0, 8, 0.35); 883 | } 884 | .tpd-skin-lightblue .tpd-background-title { 885 | background-color: #bce5ff; 886 | } 887 | .tpd-skin-lightblue .tpd-title-wrapper { 888 | border-bottom: 1px solid #909b9f; 889 | } 890 | .tpd-skin-lightblue .tpd-background-shadow { 891 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 892 | } 893 | .tpd-skin-lightblue .tpd-spinner-spin { 894 | border-color: rgba(51, 51, 51, 0.2); 895 | border-left-color: #333; 896 | } 897 | .tpd-skin-lightblue a { 898 | color: #777; 899 | } 900 | .tpd-skin-lightblue a:hover { 901 | color: #868686; 902 | } 903 | 904 | /* lightpink */ 905 | .tpd-skin-lightpink .tpd-content, 906 | .tpd-skin-lightpink .tpd-title, 907 | .tpd-skin-lightpink .tpd-close { 908 | color: #333; 909 | } 910 | .tpd-skin-lightpink .tpd-background-content { 911 | background-color: #ffc4bf; 912 | } 913 | .tpd-skin-lightpink .tpd-background { 914 | border-width: 1px; 915 | border-color: rgba(8, 0, 0, 0.35); 916 | } 917 | .tpd-skin-lightpink .tpd-background-title { 918 | background-color: #ffc4bf; 919 | } 920 | .tpd-skin-lightpink .tpd-title-wrapper { 921 | border-bottom: 1px solid #a08f8f; 922 | } 923 | .tpd-skin-lightpink .tpd-background-shadow { 924 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 925 | } 926 | .tpd-skin-lightpink .tpd-spinner-spin { 927 | border-color: rgba(51, 51, 51, 0.2); 928 | border-left-color: #333; 929 | } 930 | .tpd-skin-lightpink a { 931 | color: #777; 932 | } 933 | .tpd-skin-lightpink a:hover { 934 | color: #868686; 935 | } 936 | -------------------------------------------------------------------------------- /src/js/api.js: -------------------------------------------------------------------------------- 1 | $.extend(Tipped, { 2 | init: function () { 3 | Tooltips.init(); 4 | }, 5 | 6 | create: function (element, content) { 7 | var options = $.extend({}, arguments[2] || {}), 8 | tooltips = []; 9 | 10 | // initialize tooltips 11 | if (_.isElement(element)) { 12 | tooltips.push(new Tooltip(element, content, options)); 13 | } else { 14 | // assume selector 15 | $(element).each(function (i, el) { 16 | tooltips.push(new Tooltip(el, content, options)); 17 | }); 18 | } 19 | 20 | return new Collection(tooltips); 21 | }, 22 | 23 | get: function (selector) { 24 | var tooltips = Tooltips.get(selector); 25 | return new Collection(tooltips); 26 | }, 27 | 28 | findElement: function (element) { 29 | return Tooltips.findElement(element); 30 | }, 31 | 32 | hideAll: function () { 33 | Tooltips.hideAll(); 34 | return this; 35 | }, 36 | 37 | setDefaultSkin: function (name) { 38 | Tooltips.setDefaultSkin(name); 39 | return this; 40 | }, 41 | 42 | visible: function (selector) { 43 | if (_.isElement(selector)) { 44 | return Tooltips.isVisibleByElement(selector); 45 | } else if (typeof selector !== "undefined") { 46 | var elements = $(selector), 47 | visible = 0; 48 | $.each(elements, function (i, element) { 49 | if (Tooltips.isVisibleByElement(element)) visible++; 50 | }); 51 | return visible; 52 | } else { 53 | return Tooltips.getVisible().length; 54 | } 55 | }, 56 | 57 | clearAjaxCache: function () { 58 | Tooltips.clearAjaxCache(); 59 | return this; 60 | }, 61 | 62 | refresh: function (selector, doneCallback, progressCallback) { 63 | Tooltips.refresh(selector, doneCallback, progressCallback); 64 | return this; 65 | }, 66 | 67 | setStartingZIndex: function (index) { 68 | Tooltips.setStartingZIndex(index); 69 | return this; 70 | }, 71 | 72 | remove: function (selector) { 73 | Tooltips.remove(selector); 74 | return this; 75 | }, 76 | }); 77 | 78 | $.each("show hide toggle disable enable".split(" "), function (i, name) { 79 | Tipped[name] = function (selector) { 80 | this.get(selector)[name](); 81 | return this; 82 | }; 83 | }); 84 | -------------------------------------------------------------------------------- /src/js/behaviors.js: -------------------------------------------------------------------------------- 1 | Tipped.Behaviors = { 2 | hide: { 3 | showOn: { 4 | element: "mouseenter", 5 | tooltip: false, 6 | }, 7 | hideOn: { 8 | element: "mouseleave", 9 | tooltip: "mouseenter", 10 | }, 11 | }, 12 | 13 | mouse: { 14 | showOn: { 15 | element: "mouseenter", 16 | tooltip: false, 17 | }, 18 | hideOn: { 19 | element: "mouseleave", 20 | tooltip: "mouseenter", 21 | }, 22 | target: "mouse", 23 | showDelay: 100, 24 | fadeIn: 0, 25 | hideDelay: 0, 26 | fadeOut: 0, 27 | }, 28 | 29 | sticky: { 30 | showOn: { 31 | element: "mouseenter", 32 | tooltip: "mouseenter", 33 | }, 34 | hideOn: { 35 | element: "mouseleave", 36 | tooltip: "mouseleave", 37 | }, 38 | // more show delay solves issues positioning at the initial mouse 39 | // position when elements span multiple lines/line-breaks, since 40 | // the mouse won't be positioning close to the edge 41 | showDelay: 150, 42 | target: "mouse", 43 | fixed: true, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/js/collection.js: -------------------------------------------------------------------------------- 1 | function Collection() { 2 | this.initialize.apply(this, _slice.call(arguments)); 3 | } 4 | 5 | $.extend(Collection.prototype, { 6 | initialize: function (tooltips) { 7 | this.tooltips = tooltips; 8 | return this; 9 | }, 10 | 11 | items: function () { 12 | // everytime we grab a tooltip collection we'll clear the mouse buffer 13 | // this way it's never passed onto the elements 14 | $.each(this.tooltips, function (i, tooltip) { 15 | tooltip.is("api", true); 16 | }); 17 | 18 | return this.tooltips; 19 | }, 20 | 21 | refresh: function () { 22 | $.each(this._tooltips, function (_i, tooltip) { 23 | if (tooltip.is("visible")) { 24 | tooltip.refresh(); 25 | } 26 | }); 27 | return this; 28 | }, 29 | 30 | remove: function () { 31 | Tooltips.removeTooltips(this.tooltips); 32 | 33 | // clear tooltips on this collection 34 | this.tooltips = []; 35 | 36 | return this; 37 | }, 38 | }); 39 | 40 | $.each("show hide toggle disable enable".split(" "), function (_i, name) { 41 | Collection.prototype[name] = function () { 42 | $.each(this.tooltips, function (_j, tooltip) { 43 | tooltip.is("api", true); 44 | tooltip[name](); 45 | }); 46 | return this; 47 | }; 48 | }); 49 | -------------------------------------------------------------------------------- /src/js/delegate.js: -------------------------------------------------------------------------------- 1 | $.extend(Tipped, { 2 | delegate: function () { 3 | Delegations.add.apply(Delegations, _slice.call(arguments)); 4 | }, 5 | 6 | undelegate: function () { 7 | Delegations.remove.apply(Delegations, _slice.call(arguments)); 8 | }, 9 | }); 10 | 11 | var Delegations = { 12 | _uid: 0, 13 | _delegations: {}, 14 | 15 | add: function (selector, content, options) { 16 | var options; 17 | if (typeof content === "object" && !_.isElement(content)) { 18 | options = content; 19 | content = null; 20 | } else { 21 | options = arguments[2] || {}; 22 | } 23 | 24 | var uid = ++this._uid; 25 | 26 | var ttOptions = Options.create($.extend({}, options)); 27 | 28 | this._delegations[uid] = { 29 | uid: uid, 30 | selector: selector, 31 | content: content, 32 | options: ttOptions, 33 | }; 34 | 35 | var handler = function (event) { 36 | // store the uid so we don't create a second tooltip 37 | $(this).addClass("tpd-delegation-uid-" + uid); 38 | 39 | // now create the tooltip 40 | var tooltip = new Tooltip(this, content, options); 41 | 42 | // store any cached pageX/Y on it 43 | tooltip._cache.event = event; 44 | 45 | tooltip.setActive(); 46 | 47 | tooltip.showDelayed(); 48 | }; 49 | 50 | this._delegations[uid].removeTitleHandler = this.removeTitle.bind(this); 51 | $(document).on( 52 | "mouseenter", 53 | selector + ":not(.tpd-delegation-uid-" + uid + ")", 54 | this._delegations[uid].removeTitleHandler 55 | ); 56 | 57 | this._delegations[uid].handler = handler; 58 | $(document).on( 59 | ttOptions.showOn.element, 60 | selector + ":not(.tpd-delegation-uid-" + uid + ")", 61 | handler 62 | ); 63 | }, 64 | 65 | // puts the title into data-tipped-restore-title, 66 | // this way tooltip creation picks up on it 67 | // without showing the native title tooltip 68 | removeTitle: function (event) { 69 | var element = event.currentTarget; 70 | 71 | var title = $(element).attr("title"); 72 | 73 | // backup title 74 | if (title) { 75 | $(element).data("tipped-restore-title", title); 76 | $(element)[0].setAttribute("title", ""); // IE needs setAttribute 77 | } 78 | }, 79 | 80 | remove: function (selector) { 81 | $.each( 82 | this._delegations, 83 | function (uid, delegation) { 84 | if (delegation.selector === selector) { 85 | $(document) 86 | .off( 87 | "mouseenter", 88 | selector + ":not(.tpd-delegation-uid-" + uid + ")", 89 | delegation.removeTitleHandler 90 | ) 91 | .off( 92 | delegation.options.showOn.element, 93 | selector + ":not(.tpd-delegation-uid-" + uid + ")", 94 | delegation.handler 95 | ); 96 | delete this._delegations[uid]; 97 | } 98 | }.bind(this) 99 | ); 100 | }, 101 | 102 | removeAll: function () { 103 | $.each( 104 | this._delegations, 105 | function (uid, delegation) { 106 | $(document) 107 | .off( 108 | "mouseenter", 109 | delegation.selector + ":not(.tpd-delegation-uid-" + uid + ")", 110 | delegation.removeTitleHandler 111 | ) 112 | .off( 113 | delegation.options.showOn.element, 114 | delegation.selector + ":not(.tpd-delegation-uid-" + uid + ")", 115 | delegation.handler 116 | ); 117 | delete this._delegations[uid]; 118 | }.bind(this) 119 | ); 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /src/js/helpers/ajaxcache.js: -------------------------------------------------------------------------------- 1 | var AjaxCache = (function () { 2 | var cache = []; 3 | 4 | return { 5 | // return an update object to pass onto tooltip.update() 6 | get: function (ajax) { 7 | var entry = null; 8 | for (var i = 0; i < cache.length; i++) { 9 | if ( 10 | cache[i] && 11 | cache[i].url === ajax.url && 12 | (cache[i].type || "GET").toUpperCase() === 13 | (ajax.type || "GET").toUpperCase() && 14 | $.param(cache[i].data || {}) === $.param(ajax.data || {}) 15 | ) { 16 | entry = cache[i]; 17 | } 18 | } 19 | return entry; 20 | }, 21 | 22 | set: function (ajax, callbackName, args) { 23 | var entry = this.get(ajax); 24 | if (!entry) { 25 | entry = $.extend({ callbacks: {} }, ajax); 26 | cache.push(entry); 27 | } 28 | 29 | entry.callbacks[callbackName] = args; 30 | }, 31 | 32 | remove: function (url) { 33 | for (var i = 0; i < cache.length; i++) { 34 | if (cache[i] && cache[i].url === url) { 35 | delete cache[i]; 36 | } 37 | } 38 | }, 39 | 40 | clear: function () { 41 | cache = []; 42 | }, 43 | }; 44 | })(); 45 | -------------------------------------------------------------------------------- /src/js/helpers/bounds.js: -------------------------------------------------------------------------------- 1 | var Bounds = { 2 | viewport: function () { 3 | var vp; 4 | if (Browser.MobileSafari || (Browser.Android && Browser.Gecko)) { 5 | vp = { width: window.innerWidth, height: window.innerHeight }; 6 | } else { 7 | vp = { 8 | height: $(window).height(), 9 | width: $(window).width(), 10 | }; 11 | } 12 | 13 | return vp; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/js/helpers/browser.js: -------------------------------------------------------------------------------- 1 | var Browser = (function (uA) { 2 | function getVersion(identifier) { 3 | var version = new RegExp(identifier + "([\\d.]+)").exec(uA); 4 | return version ? parseFloat(version[1]) : true; 5 | } 6 | 7 | return { 8 | IE: 9 | !!(window.attachEvent && uA.indexOf("Opera") === -1) && 10 | getVersion("MSIE "), 11 | Opera: 12 | uA.indexOf("Opera") > -1 && 13 | ((!!window.opera && opera.version && parseFloat(opera.version())) || 14 | 7.55), 15 | WebKit: uA.indexOf("AppleWebKit/") > -1 && getVersion("AppleWebKit/"), 16 | Gecko: 17 | uA.indexOf("Gecko") > -1 && 18 | uA.indexOf("KHTML") === -1 && 19 | getVersion("rv:"), 20 | MobileSafari: !!uA.match(/Apple.*Mobile.*Safari/), 21 | Chrome: uA.indexOf("Chrome") > -1 && getVersion("Chrome/"), 22 | ChromeMobile: uA.indexOf("CrMo") > -1 && getVersion("CrMo/"), 23 | Android: uA.indexOf("Android") > -1 && getVersion("Android "), 24 | IEMobile: uA.indexOf("IEMobile") > -1 && getVersion("IEMobile/"), 25 | }; 26 | })(navigator.userAgent); 27 | -------------------------------------------------------------------------------- /src/js/helpers/color.js: -------------------------------------------------------------------------------- 1 | var Color = (function () { 2 | var names = { 3 | _default: "#000000", 4 | aqua: "#00ffff", 5 | black: "#000000", 6 | blue: "#0000ff", 7 | fuchsia: "#ff00ff", 8 | gray: "#808080", 9 | green: "#008000", 10 | lime: "#00ff00", 11 | maroon: "#800000", 12 | navy: "#000080", 13 | olive: "#808000", 14 | purple: "#800080", 15 | red: "#ff0000", 16 | silver: "#c0c0c0", 17 | teal: "#008080", 18 | white: "#ffffff", 19 | yellow: "#ffff00", 20 | }; 21 | 22 | function hex(x) { 23 | return ("0" + parseInt(x).toString(16)).slice(-2); 24 | } 25 | 26 | function rgb2hex(rgb) { 27 | rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/); 28 | return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); 29 | } 30 | 31 | return { 32 | toRGB: function (str) { 33 | if (/^rgba?\(/.test(str)) { 34 | return rgb2hex(str); 35 | } else { 36 | // first try color name to hex 37 | if (names[str]) str = names[str]; 38 | 39 | // assume already hex, just normalize #rgb #rrggbb 40 | var hex = str.replace("#", ""); 41 | if (!/^(?:[0-9a-fA-F]{3}){1,2}$/.test(hex)) return names._default; 42 | 43 | if (hex.length == 3) { 44 | hex = 45 | hex.charAt(0) + 46 | hex.charAt(0) + 47 | hex.charAt(1) + 48 | hex.charAt(1) + 49 | hex.charAt(2) + 50 | hex.charAt(2); 51 | } 52 | 53 | return "#" + hex; 54 | } 55 | }, 56 | }; 57 | })(); 58 | -------------------------------------------------------------------------------- /src/js/helpers/dimensions.js: -------------------------------------------------------------------------------- 1 | var Dimensions = { 2 | _count: 0, 3 | 4 | // dimensions are returned as the 1st parameter of the callback 5 | get: function (url, options, callback) { 6 | if (typeof options === "function") { 7 | callback = options; 8 | options = {}; 9 | } 10 | options = $.extend( 11 | { 12 | type: "image", 13 | lifetime: 1000 * 60 * 5, 14 | }, 15 | options || {} 16 | ); 17 | 18 | var cache = Dimensions.cache.get(url), 19 | type = options.type, 20 | data = { type: type, callback: callback }; 21 | 22 | var img; 23 | if (!cache) { 24 | // nothing in cache, go check 25 | switch (type) { 26 | case "image": 27 | img = new Image(); 28 | img.onload = function () { 29 | img.onload = function () {}; 30 | cache = { 31 | dimensions: { 32 | width: img.width, 33 | height: img.height, 34 | }, 35 | }; 36 | 37 | data.image = img; 38 | 39 | Dimensions.cache.set(url, cache.dimensions, data); 40 | if (callback) { 41 | callback(cache.dimensions, data); 42 | } 43 | }; 44 | 45 | img.src = url; 46 | break; 47 | } 48 | } else { 49 | img = cache.data.image; 50 | // we return cloned dimensions so the value can't be modified 51 | if (callback) { 52 | callback($.extend({}, cache.dimensions), cache.data); 53 | } 54 | } 55 | 56 | return img; 57 | }, 58 | }; 59 | 60 | Dimensions.Cache = function () { 61 | return this.initialize.apply(this, _slice.call(arguments)); 62 | }; 63 | $.extend(Dimensions.Cache.prototype, { 64 | initialize: function () { 65 | this.cache = []; 66 | }, 67 | 68 | get: function (url) { 69 | var entry = null; 70 | for (var i = 0; i < this.cache.length; i++) { 71 | if (this.cache[i] && this.cache[i].url === url) entry = this.cache[i]; 72 | } 73 | return entry; 74 | }, 75 | 76 | set: function (url, dimensions, data) { 77 | this.remove(url); 78 | this.cache.push({ url: url, dimensions: dimensions, data: data }); 79 | }, 80 | 81 | remove: function (url) { 82 | for (var i = 0; i < this.cache.length; i++) { 83 | if (this.cache[i] && this.cache[i].url === url) { 84 | delete this.cache[i]; 85 | } 86 | } 87 | }, 88 | 89 | // forcefully inject a cache entry or extend the data of existing cache 90 | inject: function (data) { 91 | var entry = get(data.url); 92 | 93 | if (entry) { 94 | $.extend(entry, data); 95 | } else { 96 | this.cache.push(data); 97 | } 98 | }, 99 | }); 100 | 101 | Dimensions.cache = new Dimensions.Cache(); 102 | 103 | //Loading 104 | Dimensions.Loading = function () { 105 | return this.initialize.apply(this, _slice.call(arguments)); 106 | }; 107 | $.extend(Dimensions.Loading.prototype, { 108 | initialize: function () { 109 | this.cache = []; 110 | }, 111 | 112 | set: function (url, data) { 113 | this.clear(url); 114 | this.cache.push({ url: url, data: data }); 115 | }, 116 | 117 | get: function (url) { 118 | var entry = null; 119 | for (var i = 0; i < this.cache.length; i++) { 120 | if (this.cache[i] && this.cache[i].url === url) entry = this.cache[i]; 121 | } 122 | return entry; 123 | }, 124 | 125 | clear: function (url) { 126 | var cache = this.cache; 127 | 128 | for (var i = 0; i < cache.length; i++) { 129 | if (cache[i] && cache[i].url === url && cache[i].data) { 130 | var data = cache[i].data; 131 | switch (data.type) { 132 | case "image": 133 | if (data.image && data.image.onload) { 134 | data.image.onload = function () {}; 135 | } 136 | break; 137 | } 138 | delete cache[i]; 139 | } 140 | } 141 | }, 142 | }); 143 | 144 | Dimensions.loading = new Dimensions.Loading(); 145 | -------------------------------------------------------------------------------- /src/js/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | var _slice = Array.prototype.slice; 2 | 3 | var _ = { 4 | wrap: function (fn, wrapper) { 5 | var __fn = fn; 6 | return function () { 7 | var args = [__fn.bind(this)].concat(_slice.call(arguments)); 8 | return wrapper.apply(this, args); 9 | }; 10 | }, 11 | 12 | // is 13 | isElement: function (object) { 14 | return object && object.nodeType === 1; 15 | }, 16 | 17 | isText: function (object) { 18 | return object && object.nodeType === 3; 19 | }, 20 | 21 | isDocumentFragment: function (object) { 22 | return object && object.nodeType === 11; 23 | }, 24 | 25 | delay: function (fn, ms) { 26 | var args = _slice.call(arguments, 2); 27 | return setTimeout(function () { 28 | return fn.apply(fn, args); 29 | }, ms); 30 | }, 31 | 32 | defer: function (fn) { 33 | return _.delay.apply(this, [fn, 1].concat(_slice.call(arguments, 1))); 34 | }, 35 | 36 | // Event 37 | pointer: function (event) { 38 | return { x: event.pageX, y: event.pageY }; 39 | }, 40 | 41 | element: { 42 | isAttached: (function () { 43 | function findTopAncestor(element) { 44 | // Walk up the DOM tree until we are at the top 45 | var ancestor = element; 46 | while (ancestor && ancestor.parentNode) { 47 | ancestor = ancestor.parentNode; 48 | } 49 | return ancestor; 50 | } 51 | 52 | return function (element) { 53 | var topAncestor = findTopAncestor(element); 54 | return !!(topAncestor && topAncestor.body); 55 | }; 56 | })(), 57 | }, 58 | }; 59 | 60 | function degrees(radian) { 61 | return (radian * 180) / Math.PI; 62 | } 63 | 64 | function radian(degrees) { 65 | return (degrees * Math.PI) / 180; 66 | } 67 | 68 | function sec(x) { 69 | return 1 / Math.cos(x); 70 | } 71 | 72 | function sfcc(c) { 73 | return String.fromCharCode.apply(String, c.replace(" ", "").split(",")); 74 | } 75 | 76 | //deep extend 77 | function deepExtend(destination, source) { 78 | for (var property in source) { 79 | if ( 80 | source[property] && 81 | source[property].constructor && 82 | source[property].constructor === Object 83 | ) { 84 | destination[property] = $.extend({}, destination[property]) || {}; 85 | deepExtend(destination[property], source[property]); 86 | } else { 87 | destination[property] = source[property]; 88 | } 89 | } 90 | return destination; 91 | } 92 | 93 | var getUID = (function () { 94 | var count = 0, 95 | _prefix = "_tipped-uid-"; 96 | 97 | return function (prefix) { 98 | prefix = prefix || _prefix; 99 | 100 | count++; 101 | // raise the count as long as we find a conflicting element on the page 102 | while (document.getElementById(prefix + count)) { 103 | count++; 104 | } 105 | return prefix + count; 106 | }; 107 | })(); 108 | -------------------------------------------------------------------------------- /src/js/helpers/mouse.js: -------------------------------------------------------------------------------- 1 | var Mouse = { 2 | _buffer: { pageX: 0, pageY: 0 }, 3 | _dimensions: { 4 | width: 30, // should both be even 5 | height: 30, 6 | }, 7 | _shift: { 8 | x: 2, 9 | y: 10, // correction so the tooltip doesn't appear on top of the mouse 10 | }, 11 | 12 | // a modified version of the actual position, to match the box 13 | getPosition: function (event) { 14 | var position = this.getActualPosition(event); 15 | 16 | return { 17 | left: 18 | position.left - 19 | Math.round(this._dimensions.width * 0.5) + 20 | this._shift.x, 21 | top: 22 | position.top - 23 | Math.round(this._dimensions.height * 0.5) + 24 | this._shift.y, 25 | }; 26 | }, 27 | 28 | getActualPosition: function (event) { 29 | var position = 30 | event && typeof event.pageX === "number" ? event : this._buffer; 31 | 32 | return { 33 | left: position.pageX, 34 | top: position.pageY, 35 | }; 36 | }, 37 | 38 | getDimensions: function () { 39 | return this._dimensions; 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/js/helpers/position.js: -------------------------------------------------------------------------------- 1 | var Position = { 2 | positions: [ 3 | "topleft", 4 | "topmiddle", 5 | "topright", 6 | "righttop", 7 | "rightmiddle", 8 | "rightbottom", 9 | "bottomright", 10 | "bottommiddle", 11 | "bottomleft", 12 | "leftbottom", 13 | "leftmiddle", 14 | "lefttop", 15 | ], 16 | 17 | regex: { 18 | toOrientation: 19 | /^(top|left|bottom|right)(top|left|bottom|right|middle|center)$/, 20 | horizontal: /^(top|bottom)/, 21 | isCenter: /(middle|center)/, 22 | side: /^(top|bottom|left|right)/, 23 | }, 24 | 25 | toDimension: (function () { 26 | var translate = { 27 | top: "height", 28 | left: "width", 29 | bottom: "height", 30 | right: "width", 31 | }; 32 | 33 | return function (position) { 34 | return translate[position]; 35 | }; 36 | })(), 37 | 38 | isCenter: function (position) { 39 | return !!position.toLowerCase().match(this.regex.isCenter); 40 | }, 41 | 42 | isCorner: function (position) { 43 | return !this.isCenter(position); 44 | }, 45 | 46 | //returns 'horizontal' or 'vertical' based on the options object 47 | getOrientation: function (position) { 48 | return position.toLowerCase().match(this.regex.horizontal) 49 | ? "horizontal" 50 | : "vertical"; 51 | }, 52 | 53 | getSide: function (position) { 54 | var side = null, 55 | matches = position.toLowerCase().match(this.regex.side); 56 | if (matches && matches[1]) side = matches[1]; 57 | return side; 58 | }, 59 | 60 | split: function (position) { 61 | return position.toLowerCase().match(this.regex.toOrientation); 62 | }, 63 | 64 | _flip: { 65 | top: "bottom", 66 | bottom: "top", 67 | left: "right", 68 | right: "left", 69 | }, 70 | flip: function (position, corner) { 71 | var split = this.split(position); 72 | 73 | if (corner) { 74 | return this.inverseCornerPlane( 75 | this.flip(this.inverseCornerPlane(position)) 76 | ); 77 | } else { 78 | return this._flip[split[1]] + split[2]; 79 | } 80 | }, 81 | 82 | inverseCornerPlane: function (position) { 83 | if (Position.isCorner(position)) { 84 | var split = this.split(position); 85 | return split[2] + split[1]; 86 | } else { 87 | return position; 88 | } 89 | }, 90 | 91 | adjustOffsetBasedOnPosition: function ( 92 | offset, 93 | defaultTargetPosition, 94 | targetPosition 95 | ) { 96 | var adjustedOffset = $.extend({}, offset); 97 | var orientationXY = { horizontal: "x", vertical: "y" }; 98 | var inverseXY = { x: "y", y: "x" }; 99 | 100 | var inverseSides = { 101 | top: { right: "x" }, 102 | bottom: { left: "x" }, 103 | left: { bottom: "y" }, 104 | right: { top: "y" }, 105 | }; 106 | 107 | var defaultOrientation = Position.getOrientation(defaultTargetPosition); 108 | if (defaultOrientation === Position.getOrientation(targetPosition)) { 109 | // we're on the same orientation 110 | // inverse when needed 111 | if ( 112 | Position.getSide(defaultTargetPosition) !== 113 | Position.getSide(targetPosition) 114 | ) { 115 | var inverse = inverseXY[orientationXY[defaultOrientation]]; 116 | adjustedOffset[inverse] *= -1; 117 | } 118 | } else { 119 | // moving to a side 120 | // flipXY 121 | var fx = adjustedOffset.x; 122 | adjustedOffset.x = adjustedOffset.y; 123 | adjustedOffset.y = fx; 124 | 125 | // inversing x or y might be required based on movement 126 | var inverseSide = 127 | inverseSides[Position.getSide(defaultTargetPosition)][ 128 | Position.getSide(targetPosition) 129 | ]; 130 | if (inverseSide) { 131 | adjustedOffset[inverseSide] *= -1; 132 | } 133 | 134 | // nullify x or y 135 | // move to left/right (vertical) = nullify y 136 | adjustedOffset[ 137 | orientationXY[Position.getOrientation(targetPosition)] 138 | ] = 0; 139 | } 140 | 141 | return adjustedOffset; 142 | }, 143 | 144 | getBoxFromPoints: function (x1, y1, x2, y2) { 145 | var minX = Math.min(x1, x2), 146 | maxX = Math.max(x1, x2), 147 | minY = Math.min(y1, y2), 148 | maxY = Math.max(y1, y2); 149 | 150 | return { 151 | left: minX, 152 | top: minY, 153 | width: Math.max(maxX - minX, 0), 154 | height: Math.max(maxY - minY, 0), 155 | }; 156 | }, 157 | 158 | isPointWithinBox: function (x1, y1, bx1, by1, bx2, by2) { 159 | var box = this.getBoxFromPoints(bx1, by1, bx2, by2); 160 | 161 | return ( 162 | x1 >= box.left && 163 | x1 <= box.left + box.width && 164 | y1 >= box.top && 165 | y1 <= box.top + box.height 166 | ); 167 | }, 168 | isPointWithinBoxLayout: function (x, y, layout) { 169 | return this.isPointWithinBox( 170 | x, 171 | y, 172 | layout.position.left, 173 | layout.position.top, 174 | layout.position.left + layout.dimensions.width, 175 | layout.position.top + layout.dimensions.height 176 | ); 177 | }, 178 | 179 | getDistance: function (x1, y1, x2, y2) { 180 | return Math.sqrt( 181 | Math.pow(Math.abs(x2 - x1), 2) + Math.pow(Math.abs(y2 - y1), 2) 182 | ); 183 | }, 184 | 185 | intersectsLine: (function () { 186 | var ccw = function (x1, y1, x2, y2, x3, y3) { 187 | var cw = (y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1); 188 | return cw > 0 ? true : cw < 0 ? false : true; 189 | }; 190 | 191 | return function (x1, y1, x2, y2, x3, y3, x4, y4, isReturnPosition) { 192 | if (!isReturnPosition) { 193 | /* http://www.bryceboe.com/2006/10/23/line-segment-intersection-algorithm */ 194 | return ( 195 | ccw(x1, y1, x3, y3, x4, y4) != ccw(x2, y2, x3, y3, x4, y4) && 196 | ccw(x1, y1, x2, y2, x3, y3) != ccw(x1, y1, x2, y2, x4, y4) 197 | ); 198 | } 199 | 200 | /* http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/1968345#1968345 */ 201 | var s1_x, s1_y, s2_x, s2_y; 202 | s1_x = x2 - x1; 203 | s1_y = y2 - y1; 204 | s2_x = x4 - x3; 205 | s2_y = y4 - y3; 206 | 207 | var s, t; 208 | s = (-s1_y * (x1 - x3) + s1_x * (y1 - y3)) / (-s2_x * s1_y + s1_x * s2_y); 209 | t = (s2_x * (y1 - y3) - s2_y * (x1 - x3)) / (-s2_x * s1_y + s1_x * s2_y); 210 | 211 | if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { 212 | // Collision detected 213 | var atX = x1 + t * s1_x; 214 | var atY = y1 + t * s1_y; 215 | return { x: atX, y: atY }; 216 | } 217 | 218 | return false; // No collision 219 | }; 220 | })(), 221 | }; 222 | -------------------------------------------------------------------------------- /src/js/helpers/requirements.js: -------------------------------------------------------------------------------- 1 | var Requirements = { 2 | scripts: { 3 | jQuery: { 4 | required: "1.7", 5 | available: window.jQuery && jQuery.fn.jquery, 6 | }, 7 | }, 8 | 9 | check: (function () { 10 | // Version check, works with 1.2.10.99_beta2 notations 11 | var VERSION_STRING = /^(\d+(\.?\d+){0,3})([A-Za-z_-]+[A-Za-z0-9]+)?/; 12 | 13 | function convertVersionString(versionString) { 14 | var vA = versionString.match(VERSION_STRING), 15 | nA = (vA && vA[1] && vA[1].split(".")) || [], 16 | v = 0; 17 | for (var i = 0, l = nA.length; i < l; i++) 18 | v += parseInt(nA[i] * Math.pow(10, 6 - i * 2)); 19 | 20 | return vA && vA[3] ? v - 1 : v; 21 | } 22 | 23 | return function require(script) { 24 | if ( 25 | !this.scripts[script].available || 26 | (convertVersionString(this.scripts[script].available) < 27 | convertVersionString(this.scripts[script].required) && 28 | !this.scripts[script].notified) 29 | ) { 30 | // mark this alert so it only shows up once 31 | this.scripts[script].notified = true; 32 | warn( 33 | "Tipped requires " + script + " >= " + this.scripts[script].required 34 | ); 35 | } 36 | }; 37 | })(), 38 | }; 39 | -------------------------------------------------------------------------------- /src/js/helpers/spin.js: -------------------------------------------------------------------------------- 1 | // Spin 2 | // Create pure CSS based spinners 3 | function Spin() { 4 | return this.initialize.apply(this, _slice.call(arguments)); 5 | } 6 | 7 | // mark as supported 8 | Spin.supported = Support.css.transform && Support.css.animation; 9 | 10 | $.extend(Spin.prototype, { 11 | initialize: function () { 12 | this.options = $.extend({}, arguments[0] || {}); 13 | 14 | this.build(); 15 | this.start(); 16 | }, 17 | 18 | build: function () { 19 | var d = (this.options.length + this.options.radius) * 2; 20 | var dimensions = { height: d, width: d }; 21 | 22 | this.element = $("
").addClass("tpd-spin").css(dimensions); 23 | 24 | this.element.append( 25 | (this._rotate = $("
").addClass("tpd-spin-rotate")) 26 | ); 27 | 28 | this.element.css({ 29 | "margin-left": -0.5 * dimensions.width, 30 | "margin-top": -0.5 * dimensions.height, 31 | }); 32 | 33 | var lines = this.options.lines; 34 | 35 | // insert 12 frames 36 | for (var i = 0; i < lines; i++) { 37 | var frame, line; 38 | this._rotate.append( 39 | (frame = $("
") 40 | .addClass("tpd-spin-frame") 41 | .append((line = $("
").addClass("tpd-spin-line")))) 42 | ); 43 | 44 | line.css({ 45 | "background-color": this.options.color, 46 | width: this.options.width, 47 | height: this.options.length, 48 | "margin-left": -0.5 * this.options.width, 49 | "border-radius": Math.round(0.5 * this.options.width), 50 | }); 51 | 52 | frame.css({ opacity: ((1 / lines) * (i + 1)).toFixed(2) }); 53 | 54 | var transformCSS = {}; 55 | transformCSS[Support.css.prefixed("transform")] = 56 | "rotate(" + (360 / lines) * (i + 1) + "deg)"; 57 | frame.css(transformCSS); 58 | } 59 | }, 60 | 61 | start: function () { 62 | var rotateCSS = {}; 63 | rotateCSS[Support.css.prefixed("animation")] = 64 | "tpd-spin 1s infinite steps(" + this.options.lines + ")"; 65 | this._rotate.css(rotateCSS); 66 | }, 67 | 68 | stop: function () { 69 | var rotateCSS = {}; 70 | rotateCSS[Support.css.prefixed("animation")] = "none"; 71 | this._rotate.css(rotateCSS); 72 | this.element.detach(); 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /src/js/helpers/support.js: -------------------------------------------------------------------------------- 1 | var Support = (function () { 2 | var testElement = document.createElement("div"), 3 | domPrefixes = "Webkit Moz O ms Khtml".split(" "); 4 | 5 | function prefixed(property) { 6 | return testAllProperties(property, "prefix"); 7 | } 8 | 9 | function testProperties(properties, prefixed) { 10 | for (var i in properties) { 11 | if (testElement.style[properties[i]] !== undefined) { 12 | return prefixed === "prefix" ? properties[i] : true; 13 | } 14 | } 15 | return false; 16 | } 17 | 18 | function testAllProperties(property, prefixed) { 19 | var ucProperty = property.charAt(0).toUpperCase() + property.substr(1), 20 | properties = ( 21 | property + 22 | " " + 23 | domPrefixes.join(ucProperty + " ") + 24 | ucProperty 25 | ).split(" "); 26 | 27 | return testProperties(properties, prefixed); 28 | } 29 | 30 | // feature detect 31 | return { 32 | css: { 33 | animation: testAllProperties("animation"), 34 | transform: testAllProperties("transform"), 35 | prefixed: prefixed, 36 | }, 37 | 38 | shadow: 39 | testAllProperties("boxShadow") && testAllProperties("pointerEvents"), 40 | 41 | touch: (function () { 42 | try { 43 | return !!( 44 | "ontouchstart" in window || 45 | (window.DocumentTouch && document instanceof DocumentTouch) 46 | ); // firefox for Android 47 | } catch (e) { 48 | return false; 49 | } 50 | })(), 51 | }; 52 | })(); 53 | -------------------------------------------------------------------------------- /src/js/helpers/visible.js: -------------------------------------------------------------------------------- 1 | function Visible() { 2 | return this.initialize.apply(this, _slice.call(arguments)); 3 | } 4 | 5 | $.extend(Visible.prototype, { 6 | initialize: function (elements) { 7 | elements = Array.isArray(elements) ? elements : [elements]; // ensure array 8 | this.elements = elements; 9 | 10 | this._restore = []; 11 | $.each( 12 | elements, 13 | function (_i, element) { 14 | var visible = $(element).is(":visible"); 15 | 16 | if (!visible) { 17 | $(element).show(); 18 | } 19 | 20 | this._restore.push({ 21 | element: element, 22 | visible: visible, 23 | }); 24 | }.bind(this) 25 | ); 26 | return this; 27 | }, 28 | 29 | restore: function () { 30 | $.each(this._restore, function (i, entry) { 31 | if (!entry.visible) { 32 | $(entry.element).show(); 33 | } 34 | }); 35 | 36 | this._restore = null; 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | var Options = { 2 | create: (function () { 3 | var BASE, RESET; 4 | 5 | // hideOn helper 6 | function toDisplayObject(input, display) { 7 | var on; 8 | if (typeof input === "string") { 9 | on = { 10 | element: 11 | (RESET[display] && RESET[display].element) || BASE[display].element, 12 | event: input, 13 | }; 14 | } else { 15 | on = deepExtend($.extend({}, BASE[display]), input); 16 | } 17 | 18 | return on; 19 | } 20 | 21 | // hideOn helper 22 | function initialize(options) { 23 | BASE = Tipped.Skins.base; 24 | RESET = deepExtend($.extend({}, BASE), Tipped.Skins["reset"]); 25 | initialize = create; 26 | return create(options); 27 | } 28 | 29 | function middleize(position) { 30 | if (position.match(/^(top|left|bottom|right)$/)) { 31 | position += "middle"; 32 | } 33 | 34 | position.replace("center", "middle").replace(" ", ""); 35 | 36 | return position; 37 | } 38 | 39 | function presetiffy(options) { 40 | var preset, behavior; 41 | if (options.behavior && (behavior = Tipped.Behaviors[options.behavior])) { 42 | preset = deepExtend($.extend({}, behavior), options); 43 | } else { 44 | preset = options; 45 | } 46 | 47 | return preset; 48 | } 49 | 50 | function create(options) { 51 | var selected_skin = options.skin 52 | ? options.skin 53 | : Tooltips.options.defaultSkin; 54 | var SELECTED = $.extend({}, Tipped.Skins[selected_skin] || {}); 55 | // make sure the skin option is set 56 | if (!SELECTED.skin) { 57 | SELECTED.skin = Tooltips.options.defaultSkin || "dark"; 58 | } 59 | 60 | var MERGED_SELECTED = deepExtend( 61 | $.extend({}, RESET), 62 | presetiffy(SELECTED) 63 | ); // work presets into selected skin 64 | 65 | var MERGED = deepExtend( 66 | $.extend({}, MERGED_SELECTED), 67 | presetiffy(options) 68 | ); // also work presets into the given options 69 | 70 | // Ajax 71 | if (MERGED.ajax) { 72 | var RESET_ajax = RESET.ajax || {}, 73 | BASE_ajax = BASE.ajax; 74 | 75 | if (typeof MERGED.ajax === "boolean") { 76 | // true 77 | MERGED.ajax = { 78 | //method: RESET_ajax.type || BASE_ajax.type 79 | }; 80 | } 81 | // otherwise it must be an object 82 | MERGED.ajax = deepExtend($.extend({}, BASE_ajax), MERGED.ajax); 83 | } 84 | 85 | var position; 86 | var targetPosition = (targetPosition = 87 | (MERGED.position && MERGED.position.target) || 88 | (typeof MERGED.position === "string" && MERGED.position) || 89 | (RESET.position && RESET.position.target) || 90 | (typeof RESET.position === "string" && RESET.position) || 91 | (BASE.position && BASE.position.target) || 92 | BASE.position); 93 | targetPosition = middleize(targetPosition); 94 | 95 | var tooltipPosition = 96 | (MERGED.position && MERGED.position.tooltip) || 97 | (RESET.position && RESET.position.tooltip) || 98 | (BASE.position && BASE.position.tooltip) || 99 | Tooltips.Position.getInversedPosition(targetPosition); 100 | tooltipPosition = middleize(tooltipPosition); 101 | 102 | if (MERGED.position) { 103 | if (typeof MERGED.position === "string") { 104 | MERGED.position = middleize(MERGED.position); 105 | position = { 106 | target: MERGED.position, 107 | tooltip: Tooltips.Position.getTooltipPositionFromTarget( 108 | MERGED.position 109 | ), 110 | }; 111 | } else { 112 | // object 113 | position = { tooltip: tooltipPosition, target: targetPosition }; 114 | if (MERGED.position.tooltip) { 115 | position.tooltip = middleize(MERGED.position.tooltip); 116 | } 117 | if (MERGED.position.target) { 118 | position.target = middleize(MERGED.position.target); 119 | } 120 | } 121 | } else { 122 | position = { 123 | tooltip: tooltipPosition, 124 | target: targetPosition, 125 | }; 126 | } 127 | 128 | // make sure the 2 positions are on the same plane when centered 129 | // this aligns the sweet spot when repositioning based on the stem 130 | if ( 131 | Position.isCorner(position.target) && 132 | Position.getOrientation(position.target) !== 133 | Position.getOrientation(position.tooltip) 134 | ) { 135 | // switch over the target only, cause we shouldn't be resetting the stem on the tooltip 136 | position.target = Position.inverseCornerPlane(position.target); 137 | } 138 | 139 | // if we're hooking to the mouse we want the center 140 | if (MERGED.target === "mouse") { 141 | var orientation = Position.getOrientation(position.target); 142 | 143 | // force center alignment on the mouse 144 | if (orientation === "horizontal") { 145 | position.target = position.target.replace(/(left|right)/, "middle"); 146 | } else { 147 | position.target = position.target.replace(/(top|bottom)/, "middle"); 148 | } 149 | } 150 | 151 | // if the target is the mouse we set the position to 'bottomright' so the position system can work with it 152 | MERGED.position = position; 153 | 154 | // Offset 155 | var offset; 156 | if (MERGED.target === "mouse") { 157 | // get the offset of the base class 158 | offset = $.extend({}, BASE.offset); 159 | $.extend(offset, Tipped.Skins["reset"].offset || {}); 160 | 161 | if (options.skin) { 162 | $.extend( 163 | offset, 164 | ( 165 | Tipped.Skins[options.skin] || 166 | Tipped.Skins[Tooltips.options.defaultSkin] || 167 | {} 168 | ).offset || {} 169 | ); 170 | } 171 | 172 | // find out what the offset should be 173 | offset = Position.adjustOffsetBasedOnPosition( 174 | BASE.offset, 175 | BASE.position, 176 | position.target, 177 | true 178 | ); 179 | 180 | // now put any given options on top of that 181 | if (options.offset) { 182 | offset = $.extend(offset, options.offset || {}); 183 | } 184 | } else { 185 | offset = { 186 | x: MERGED.offset.x, 187 | y: MERGED.offset.y, 188 | }; 189 | } 190 | 191 | MERGED.offset = offset; 192 | 193 | // hideOnClickOutside 194 | if (MERGED.hideOn && MERGED.hideOn === "click-outside") { 195 | MERGED.hideOnClickOutside = true; 196 | MERGED.hideOn = false; 197 | MERGED.fadeOut = 0; // instantly fadeout for better UI 198 | } 199 | 200 | if (MERGED.showOn) { 201 | // showOn and hideOn should not abide by inheritance, 202 | // otherwise we'd always have the BASE/RESET object for it as starting point 203 | var showOn = MERGED.showOn; 204 | 205 | if (typeof showOn === "string") { 206 | showOn = { element: showOn }; 207 | } 208 | 209 | MERGED.showOn = showOn; 210 | } 211 | 212 | if (MERGED.hideOn) { 213 | var hideOn = MERGED.hideOn; 214 | 215 | if (typeof hideOn === "string") { 216 | hideOn = { element: hideOn }; 217 | } 218 | 219 | MERGED.hideOn = hideOn; 220 | } 221 | 222 | // normalize inline 223 | if (MERGED.inline) { 224 | if (typeof MERGED.inline !== "string") { 225 | MERGED.inline = false; 226 | } 227 | } 228 | 229 | // fadeIn 0 on IE < 9 to prevent text transform during fade 230 | if (Browser.IE && Browser.IE < 9) { 231 | $.extend(MERGED, { fadeIn: 0, fadeOut: 0, hideDelay: 0 }); 232 | } 233 | 234 | if (MERGED.spinner) { 235 | if (!Spin.supported) { 236 | MERGED.spinner = false; 237 | } else { 238 | if (typeof MERGED.spinner === "boolean") { 239 | MERGED.spinner = RESET.spinner || BASE.spinner || {}; 240 | } 241 | } 242 | } 243 | 244 | if (!MERGED.container) { 245 | MERGED.container = document.body; 246 | } 247 | 248 | if (MERGED.containment) { 249 | if (typeof MERGED.containment === "string") { 250 | MERGED.containment = { 251 | selector: MERGED.containment, 252 | padding: 253 | (RESET.containment && RESET.containment.padding) || 254 | (BASE.padding && BASE.containment.padding), 255 | }; 256 | } 257 | } 258 | 259 | // normalize shadow, setting it to true should only mean it's enabled when supported 260 | if (MERGED.shadow) { 261 | MERGED.shadow = Support.shadow; 262 | } 263 | 264 | return MERGED; 265 | } 266 | 267 | return initialize; 268 | })(), 269 | }; 270 | -------------------------------------------------------------------------------- /src/js/setup.js: -------------------------------------------------------------------------------- 1 | var Tipped = {}; 2 | 3 | $.extend(Tipped, { 4 | version: "<%= pkg.version %>", 5 | }); 6 | -------------------------------------------------------------------------------- /src/js/skin.js: -------------------------------------------------------------------------------- 1 | function Skin() { 2 | this.initialize.apply(this, _slice.call(arguments)); 3 | } 4 | 5 | $.extend(Skin.prototype, { 6 | initialize: function (tooltip) { 7 | this.tooltip = tooltip; 8 | this.element = tooltip._skin; 9 | 10 | // classes to further style the tooltip 11 | var options = this.tooltip.options; 12 | this.tooltip._tooltip[(options.shadow ? "remove" : "add") + "Class"]( 13 | "tpd-no-shadow" 14 | ) 15 | [(options.radius ? "remove" : "add") + "Class"]("tpd-no-radius") 16 | [(options.stem ? "remove" : "add") + "Class"]("tpd-no-stem"); 17 | 18 | // we should get radius and border when initializing 19 | var frames, bg, bgc, spinner; 20 | var prefixedRadius = Support.css.prefixed("borderTopLeftRadius"); 21 | 22 | this.element 23 | .append( 24 | (frames = $("
") 25 | .addClass("tpd-frames") 26 | .append( 27 | $("
") 28 | .addClass("tpd-frame") 29 | .append( 30 | $("
") 31 | .addClass("tpd-backgrounds") 32 | .append( 33 | (bg = $("
") 34 | .addClass("tpd-background") 35 | .append( 36 | (bgc = $("
").addClass("tpd-background-content")) 37 | )) 38 | ) 39 | ) 40 | )) 41 | ) 42 | .append((spinner = $("
").addClass("tpd-spinner"))); 43 | 44 | // REQUIRED FOR IE < 8 45 | bg.css({ width: 999, height: 999, zoom: 1 }); 46 | 47 | this._css = { 48 | border: parseFloat(bg.css("border-top-width")), 49 | radius: parseFloat(prefixedRadius ? bg.css(prefixedRadius) : 0), 50 | padding: parseFloat(tooltip._content.css("padding-top")), 51 | borderColor: bg.css("border-top-color"), 52 | //spacing: parseFloat(this.element.css('margin-top')), 53 | // borderOpacity: .5 // IE pre rgba fallback can be inserted here 54 | backgroundColor: bgc.css("background-color"), 55 | backgroundOpacity: bgc.css("opacity"), 56 | spinner: { 57 | dimensions: { 58 | width: spinner.innerWidth(), 59 | height: spinner.innerHeight(), 60 | }, 61 | }, 62 | }; 63 | 64 | spinner.remove(); 65 | frames.remove(); 66 | 67 | this._side = Position.getSide(tooltip.options.position.tooltip) || "top"; 68 | 69 | this._vars = {}; 70 | }, 71 | 72 | destroy: function () { 73 | if (!this.frames) return; 74 | 75 | // remove all the stems 76 | $.each( 77 | "top right bottom left".split(" "), 78 | function (_i, side) { 79 | if (this["stem_" + side]) this["stem_" + side].destroy(); 80 | }.bind(this) 81 | ); 82 | 83 | this.frames.remove(); 84 | this.frames = null; 85 | }, 86 | 87 | build: function () { 88 | // if already build exit 89 | if (this.frames) return; 90 | 91 | this.element.append((this.frames = $("
").addClass("tpd-frames"))); 92 | 93 | $.each( 94 | "top right bottom left".split(" "), 95 | function (_i, side) { 96 | this.insertFrame(side); 97 | }.bind(this) 98 | ); 99 | 100 | // insert a spinner, if we haven't already 101 | if (!this._spinner) { 102 | this.tooltip._tooltip.append( 103 | (this._spinner = $("
") 104 | .addClass("tpd-spinner") 105 | .hide() 106 | .append($("
").addClass("tpd-spinner-spin"))) 107 | ); 108 | } 109 | }, 110 | 111 | _frame: (function () { 112 | var backgrounds; 113 | 114 | var frame = $("
") 115 | .addClass("tpd-frame") 116 | // background 117 | .append( 118 | (backgrounds = $("
") 119 | .addClass("tpd-backgrounds") 120 | .append($("
").addClass("tpd-background-shadow"))) 121 | ) 122 | .append( 123 | $("
") 124 | .addClass("tpd-shift-stem") 125 | .append( 126 | $("
").addClass( 127 | "tpd-shift-stem-side tpd-shift-stem-side-before" 128 | ) 129 | ) 130 | .append($("
").addClass("tpd-stem")) 131 | .append( 132 | $("
").addClass("tpd-shift-stem-side tpd-shift-stem-side-after") 133 | ) 134 | ); 135 | 136 | $.each( 137 | "top right bottom left".split(" "), 138 | function (_i, s) { 139 | backgrounds.append( 140 | $("
") 141 | .addClass("tpd-background-box tpd-background-box-" + s) 142 | .append( 143 | $("
") 144 | .addClass("tpd-background-box-shift") 145 | .append( 146 | $("
") 147 | .addClass("tpd-background-box-shift-further") 148 | .append( 149 | $("
") 150 | .addClass("tpd-background") 151 | .append($("
").addClass("tpd-background-title")) 152 | .append($("
").addClass("tpd-background-content")) 153 | ) 154 | .append( 155 | $("
").addClass( 156 | "tpd-background tpd-background-loading" 157 | ) 158 | ) 159 | .append( 160 | $("
").addClass("tpd-background-border-hack").hide() 161 | ) 162 | ) 163 | ) 164 | ); 165 | }.bind(this) 166 | ); 167 | 168 | return frame; 169 | })(), 170 | 171 | _getFrame: function (side) { 172 | var frame = this._frame.clone(); 173 | 174 | // class 175 | frame.addClass("tpd-frame-" + side); 176 | 177 | // put border radius on shadow 178 | frame 179 | .find(".tpd-background-shadow") 180 | .css({ "border-radius": this._css.radius }); 181 | 182 | // mark side on stem 183 | if (this.tooltip.options.stem) { 184 | frame.find(".tpd-stem").attr("data-stem-position", side); 185 | } 186 | 187 | // radius on background layers 188 | var innerBackgroundRadius = Math.max( 189 | this._css.radius - this._css.border, 190 | 0 191 | ); 192 | frame.find(".tpd-background-title").css({ 193 | "border-top-left-radius": innerBackgroundRadius, 194 | "border-top-right-radius": innerBackgroundRadius, 195 | }); 196 | frame.find(".tpd-background-content").css({ 197 | "border-bottom-left-radius": innerBackgroundRadius, 198 | "border-bottom-right-radius": innerBackgroundRadius, 199 | }); 200 | frame.find(".tpd-background-loading").css({ 201 | "border-radius": innerBackgroundRadius, 202 | }); 203 | 204 | // adjust the dimensions of the shift sides 205 | var ss = { backgroundColor: this._css.borderColor }; 206 | var orientation = Position.getOrientation(side), 207 | isHorizontal = orientation === "horizontal"; 208 | ss[isHorizontal ? "height" : "width"] = this._css.border + "px"; 209 | var inverse = { 210 | top: "bottom", 211 | bottom: "top", 212 | left: "right", 213 | right: "left", 214 | }; 215 | ss[inverse[side]] = 0; 216 | frame.find(".tpd-shift-stem-side").css(ss); 217 | 218 | return frame; 219 | }, 220 | 221 | insertFrame: function (side) { 222 | var frame = (this["frame_" + side] = this._getFrame(side)); 223 | this.frames.append(frame); 224 | 225 | if (this.tooltip.options.stem) { 226 | var stem = frame.find(".tpd-stem"); 227 | this["stem_" + side] = new Stem(stem, this, {}); 228 | } 229 | }, 230 | 231 | // Loading 232 | startLoading: function () { 233 | if (!this.tooltip.supportsLoading) return; 234 | this.build(); // make sure the tooltip is build 235 | 236 | // resize to the dimensions of the spinner the first time a tooltip is shown 237 | if (!this._spinner && !this.tooltip.is("resize-to-content")) { 238 | this.setDimensions(this._css.spinner.dimensions); // this creates ._spinner 239 | } 240 | 241 | if (this._spinner) { 242 | this._spinner.show(); 243 | } 244 | }, 245 | 246 | // the idea behind stopLoading is that dimensions are set right after calling this function 247 | // that's why we don't set the manually here 248 | stopLoading: function () { 249 | if (!this.tooltip.supportsLoading || !this._spinner) return; 250 | this.build(); // make sure the tooltip is build 251 | 252 | this._spinner.hide(); 253 | }, 254 | 255 | // updates the background of the currently active side 256 | updateBackground: function () { 257 | var frame = this._vars.frames[this._side]; 258 | 259 | var backgroundDimensions = $.extend({}, frame.background.dimensions); 260 | 261 | if (this.tooltip.title && !this.tooltip.is("loading")) { 262 | // show both background children 263 | this.element 264 | .find(".tpd-background-title, .tpd-background-content") 265 | .show(); 266 | 267 | // remove background color and replace it with transparent 268 | this.element 269 | .find(".tpd-background") 270 | .css({ "background-color": "transparent" }); 271 | 272 | var contentDimensions = $.extend({}, backgroundDimensions); 273 | var innerBackgroundRadius = Math.max( 274 | this._css.radius - this._css.border, 275 | 0 276 | ); 277 | var contentRadius = { 278 | "border-top-left-radius": innerBackgroundRadius, 279 | "border-top-right-radius": innerBackgroundRadius, 280 | "border-bottom-left-radius": innerBackgroundRadius, 281 | "border-bottom-right-radius": innerBackgroundRadius, 282 | }; 283 | 284 | // measure the title 285 | var visible = new Visible(this.tooltip._tooltip); 286 | 287 | var titleHeight = this.tooltip._titleWrapper.innerHeight(); // without margins 288 | 289 | contentDimensions.height -= titleHeight; 290 | 291 | // set all title dimensions 292 | this.element.find(".tpd-background-title").css({ 293 | height: titleHeight, 294 | width: backgroundDimensions.width, 295 | }); 296 | 297 | // remove radius at the top 298 | contentRadius["border-top-left-radius"] = 0; 299 | contentRadius["border-top-right-radius"] = 0; 300 | 301 | visible.restore(); 302 | 303 | // set all content dimensions 304 | // set correct radius 305 | this.element 306 | .find(".tpd-background-content") 307 | .css(contentDimensions) 308 | .css(contentRadius); 309 | 310 | // loading indicator 311 | this.element.find(".tpd-background-loading").css({ 312 | "background-color": this._css.backgroundColor, 313 | }); 314 | } else { 315 | // no title or close button creates a bar at the top 316 | // set background color only for better px handling in the corners 317 | // show both background children 318 | this.element 319 | .find(".tpd-background-title, .tpd-background-content") 320 | .hide(); 321 | 322 | this.element 323 | .find(".tpd-background") 324 | .css({ "background-color": this._css.backgroundColor }); 325 | } 326 | 327 | // border fix, required as a workaround for the following bugs: 328 | // https://bugzilla.mozilla.org/show_bug.cgi?id=929979 329 | // https://code.google.com/p/chromium/issues/detail?id=320330 330 | if (this._css.border) { 331 | this.element 332 | .find(".tpd-background") 333 | .css({ "border-color": "transparent" }); 334 | 335 | this.element 336 | .find(".tpd-background-border-hack") 337 | // scaled 338 | .css({ 339 | width: backgroundDimensions.width, 340 | height: backgroundDimensions.height, 341 | "border-radius": this._css.radius, 342 | "border-width": this._css.border, 343 | "border-color": this._css.borderColor, 344 | }) 345 | .show(); 346 | } 347 | }, 348 | 349 | // update dimensions of the currently active side 350 | // background + stem 351 | paint: function () { 352 | // don't update if we've already rendered the dimensions at current stem position 353 | if ( 354 | this._paintedDimensions && 355 | this._paintedDimensions.width === this._dimensions.width && 356 | this._paintedDimensions.height === this._dimensions.height && 357 | this._paintedStemPosition === this._stemPosition 358 | ) { 359 | return; 360 | } 361 | 362 | // store these to prevent future updates at the same dimensions 363 | this._paintedDimensions = this._dimensions; 364 | this._paintedStemPosition = this._stemPosition; 365 | 366 | // visible side, hide others 367 | this.element 368 | .removeClass( 369 | "tpd-visible-frame-top tpd-visible-frame-bottom tpd-visible-frame-left tpd-visible-frame-right" 370 | ) 371 | .addClass("tpd-visible-frame-" + this._side); 372 | 373 | var frame = this._vars.frames[this._side]; 374 | 375 | // set dimensions 376 | var backgroundDimensions = $.extend({}, frame.background.dimensions); 377 | this.element.find(".tpd-background").css(backgroundDimensions); 378 | this.element.find(".tpd-background-shadow").css({ 379 | width: backgroundDimensions.width + 2 * this._css.border, 380 | height: backgroundDimensions.height + 2 * this._css.border, 381 | }); 382 | 383 | // update background to the correct display method 384 | this.updateBackground(); 385 | 386 | this.element 387 | .find(".tpd-background-box-shift, .tpd-background-box-shift-further") 388 | .removeAttr("style"); 389 | 390 | // dimensions of the skin 391 | this.element 392 | .add(this.frames) 393 | // and the tooltip 394 | .add(this.tooltip._tooltip) 395 | .css(frame.dimensions); 396 | 397 | // resize every frame 398 | var name = this._side, 399 | value = this._vars.frames[name]; 400 | var f = this.element.find(".tpd-frame-" + this._side), 401 | fdimensions = this._vars.frames[name].dimensions; 402 | 403 | f.css(fdimensions); 404 | 405 | // background 406 | f.find(".tpd-backgrounds").css( 407 | $.extend({}, value.background.position, { 408 | width: fdimensions.width - value.background.position.left, 409 | height: fdimensions.height - value.background.position.top, 410 | }) 411 | ); 412 | 413 | // find the position of this frame 414 | // adjust the backgrounds 415 | var orientation = Position.getOrientation(name); 416 | 417 | // no stem only shows the top frame (using CSS) 418 | // with a stem we have to make adjustments 419 | if (this.tooltip.options.stem) { 420 | // position the shiftstem 421 | f.find(".tpd-shift-stem").css( 422 | $.extend({}, value.shift.dimensions, value.shift.position) 423 | ); 424 | 425 | if (orientation === "vertical") { 426 | // left or right 427 | // top or bottom 428 | // make top/bottom side small 429 | var smallBoxes = f.find( 430 | ".tpd-background-box-top, .tpd-background-box-bottom" 431 | ); 432 | smallBoxes.css({ 433 | height: this._vars.cut, 434 | width: this._css.border, 435 | }); 436 | 437 | // align the bottom side with the bottom 438 | f.find(".tpd-background-box-bottom") 439 | .css({ 440 | top: value.dimensions.height - this._vars.cut, 441 | }) 442 | // shift right side back 443 | .find(".tpd-background-box-shift") 444 | .css({ 445 | "margin-top": -1 * value.dimensions.height + this._vars.cut, 446 | }); 447 | 448 | // both sides should now be moved left or right depending on the current side 449 | var moveSmallBy = 450 | name === "right" 451 | ? value.dimensions.width - value.stemPx - this._css.border 452 | : 0; 453 | smallBoxes 454 | .css({ 455 | left: moveSmallBy, 456 | }) 457 | .find(".tpd-background-box-shift") 458 | .css({ 459 | // inverse of the above 460 | "margin-left": -1 * moveSmallBy, 461 | }); 462 | 463 | // hide the background that will be replaced by the stemshift when we have a stem 464 | f.find( 465 | ".tpd-background-box-" + (name == "left" ? "left" : "right") 466 | ).hide(); 467 | 468 | // resize the other one 469 | if (name === "right") { 470 | // top can be resized to height - border 471 | f.find(".tpd-background-box-left").css({ 472 | width: value.dimensions.width - value.stemPx - this._css.border, 473 | }); 474 | } else { 475 | f.find(".tpd-background-box-right") 476 | .css({ 477 | "margin-left": this._css.border, //, 478 | //height: (value.dimensions.height - value.stemPx - this._vars.border) + 'px' 479 | }) 480 | .find(".tpd-background-box-shift") 481 | .css({ 482 | "margin-left": -1 * this._css.border, 483 | }); 484 | } 485 | 486 | // left or right should be shifted to the center 487 | // depending on which side is used 488 | var smallBox = f.find(".tpd-background-box-" + this._side); 489 | smallBox.css({ 490 | height: value.dimensions.height - 2 * this._vars.cut, // resize 491 | "margin-top": this._vars.cut, 492 | }); 493 | smallBox.find(".tpd-background-box-shift").css({ 494 | "margin-top": -1 * this._vars.cut, 495 | }); 496 | } else { 497 | // top or bottom 498 | // make left and right side small 499 | var smallBoxes = f.find( 500 | ".tpd-background-box-left, .tpd-background-box-right" 501 | ); 502 | smallBoxes.css({ 503 | width: this._vars.cut, 504 | height: this._css.border, 505 | }); 506 | 507 | // align the right side with the right 508 | f.find(".tpd-background-box-right") 509 | .css({ 510 | left: value.dimensions.width - this._vars.cut, 511 | }) 512 | // shift right side back 513 | .find(".tpd-background-box-shift") 514 | .css({ 515 | "margin-left": -1 * value.dimensions.width + this._vars.cut, 516 | }); 517 | 518 | // both sides should now be moved up or down depending on the current side 519 | var moveSmallBy = 520 | name === "bottom" 521 | ? value.dimensions.height - value.stemPx - this._css.border 522 | : 0; 523 | smallBoxes 524 | .css({ 525 | top: moveSmallBy, 526 | }) 527 | .find(".tpd-background-box-shift") 528 | .css({ 529 | // inverse of the above 530 | "margin-top": -1 * moveSmallBy, 531 | }); 532 | 533 | // hide the background that will be replaced by the stemshift 534 | f.find( 535 | ".tpd-background-box-" + (name === "top" ? "top" : "bottom") 536 | ).hide(); 537 | 538 | // resize the other one 539 | if (name === "bottom") { 540 | // top can be resized to height - border 541 | f.find(".tpd-background-box-top").css({ 542 | height: value.dimensions.height - value.stemPx - this._css.border, 543 | }); 544 | } else { 545 | f.find(".tpd-background-box-bottom") 546 | .css({ 547 | "margin-top": this._css.border, 548 | }) 549 | .find(".tpd-background-box-shift") 550 | .css({ 551 | "margin-top": -1 * this._css.border, 552 | }); 553 | } 554 | 555 | // top or bottom should be shifted to the center 556 | // depending on which side is used 557 | var smallBox = f.find(".tpd-background-box-" + this._side); 558 | smallBox.css({ 559 | width: value.dimensions.width - 2 * this._vars.cut, 560 | "margin-left": this._vars.cut, 561 | }); 562 | smallBox.find(".tpd-background-box-shift").css({ 563 | "margin-left": -1 * this._vars.cut, 564 | }); 565 | } 566 | } 567 | 568 | // position the loader 569 | var fb = frame.background, 570 | fbp = fb.position, 571 | fbd = fb.dimensions; 572 | this._spinner.css({ 573 | top: 574 | fbp.top + 575 | this._css.border + 576 | (fbd.height * 0.5 - this._css.spinner.dimensions.height * 0.5), 577 | left: 578 | fbp.left + 579 | this._css.border + 580 | (fbd.width * 0.5 - this._css.spinner.dimensions.width * 0.5), 581 | }); 582 | }, 583 | 584 | getVars: function () { 585 | var padding = this._css.padding, 586 | radius = this._css.radius, 587 | border = this._css.border; 588 | 589 | var maxStemHeight = this._vars.maxStemHeight || 0; 590 | var dimensions = $.extend({}, this._dimensions || {}); 591 | var vars = { 592 | frames: {}, 593 | dimensions: dimensions, 594 | maxStemHeight: maxStemHeight, 595 | }; 596 | 597 | // set the cut 598 | vars.cut = Math.max(this._css.border, this._css.radius) || 0; 599 | 600 | var stemDimensions = { width: 0, height: 0 }; 601 | var stemOffset = 0; 602 | var stemPx = 0; 603 | 604 | if (this.tooltip.options.stem) { 605 | stemDimensions = this.stem_top.getMath().dimensions.outside; 606 | stemOffset = this.stem_top._css.offset; 607 | stemPx = Math.max(stemDimensions.height - this._css.border, 0); // the height we assume the stem is should never be negative 608 | } 609 | 610 | // store for later use 611 | vars.stemDimensions = stemDimensions; 612 | vars.stemOffset = stemOffset; 613 | 614 | // positition the background and resize the outer frame 615 | $.each( 616 | "top right bottom left".split(" "), 617 | function (_i, side) { 618 | var orientation = Position.getOrientation(side), 619 | isLR = orientation === "vertical"; 620 | 621 | var frameDimensions = { 622 | width: dimensions.width + 2 * border, 623 | height: dimensions.height + 2 * border, 624 | }; 625 | 626 | var shiftWidth = 627 | frameDimensions[isLR ? "height" : "width"] - 2 * vars.cut; 628 | 629 | var frame = { 630 | dimensions: frameDimensions, 631 | stemPx: stemPx, 632 | position: { top: 0, left: 0 }, 633 | background: { 634 | dimensions: $.extend({}, dimensions), 635 | position: { top: 0, left: 0 }, 636 | }, 637 | }; 638 | vars.frames[side] = frame; 639 | 640 | // adjust width or height of frame based on orientation 641 | frame.dimensions[isLR ? "width" : "height"] += stemPx; 642 | 643 | if (side === "top" || side === "left") { 644 | frame.background.position[side] += stemPx; 645 | } 646 | 647 | $.extend(frame, { 648 | shift: { 649 | position: { top: 0, left: 0 }, 650 | dimensions: { 651 | width: isLR ? stemDimensions.height : shiftWidth, 652 | height: isLR ? shiftWidth : stemDimensions.height, 653 | }, 654 | }, 655 | }); 656 | 657 | switch (side) { 658 | case "top": 659 | case "bottom": 660 | frame.shift.position.left += vars.cut; 661 | 662 | if (side === "bottom") { 663 | frame.shift.position.top += 664 | frameDimensions.height - border - stemPx; 665 | } 666 | break; 667 | case "left": 668 | case "right": 669 | frame.shift.position.top += vars.cut; 670 | 671 | if (side === "right") { 672 | frame.shift.position.left += 673 | frameDimensions.width - border - stemPx; 674 | } 675 | break; 676 | } 677 | }.bind(this) 678 | ); 679 | 680 | // add connections 681 | vars.connections = {}; 682 | $.each( 683 | Position.positions, 684 | function (_i, position) { 685 | vars.connections[position] = this.getConnectionLayout(position, vars); 686 | }.bind(this) 687 | ); 688 | 689 | return vars; 690 | }, 691 | 692 | setDimensions: function (dimensions) { 693 | this.build(); 694 | 695 | // don't update if nothing changed 696 | var d = this._dimensions; 697 | if (d && d.width === dimensions.width && d.height === dimensions.height) { 698 | return; 699 | } 700 | 701 | this._dimensions = dimensions; 702 | this._vars = this.getVars(); 703 | }, 704 | 705 | setSide: function (side) { 706 | this._side = side; 707 | this._vars = this.getVars(); 708 | }, 709 | 710 | // gets position and offset of the given stem 711 | getConnectionLayout: function (position, vars) { 712 | var side = Position.getSide(position), 713 | orientation = Position.getOrientation(position), 714 | dimensions = vars.dimensions, 715 | cut = vars.cut; // where the stem starts 716 | 717 | var stem = this["stem_" + side], 718 | stemOffset = vars.stemOffset, 719 | stemWidth = this.tooltip.options.stem 720 | ? stem.getMath().dimensions.outside.width 721 | : 0, 722 | stemMiddleFromSide = cut + stemOffset + stemWidth * 0.5; 723 | 724 | // at the end of this function we should know how much the stem is able to shift 725 | var layout = { 726 | stem: {}, 727 | }; 728 | var move = { 729 | left: 0, 730 | right: 0, 731 | up: 0, 732 | down: 0, 733 | }; 734 | 735 | var stemConnection = { top: 0, left: 0 }, 736 | connection = { top: 0, left: 0 }; 737 | 738 | var frame = vars.frames[side], 739 | stemMiddleFromSide = 0; 740 | 741 | // top/bottom 742 | if (orientation == "horizontal") { 743 | var width = frame.dimensions.width; 744 | 745 | if (this.tooltip.options.stem) { 746 | width = frame.shift.dimensions.width; 747 | 748 | // if there's not enough width for twice the stemOffset, calculate what is available, divide the width 749 | if (width - stemWidth < 2 * stemOffset) { 750 | stemOffset = Math.floor((width - stemWidth) * 0.5) || 0; 751 | } 752 | 753 | stemMiddleFromSide = cut + stemOffset + stemWidth * 0.5; 754 | } 755 | 756 | var availableWidth = width - 2 * stemOffset; 757 | 758 | var split = Position.split(position); 759 | var left = stemOffset; 760 | switch (split[2]) { 761 | case "left": 762 | move.right = availableWidth - stemWidth; 763 | 764 | stemConnection.left = stemMiddleFromSide; 765 | break; 766 | case "middle": 767 | left += Math.round(availableWidth * 0.5 - stemWidth * 0.5); 768 | 769 | move.left = left - stemOffset; 770 | move.right = left - stemOffset; 771 | 772 | stemConnection.left = connection.left = Math.round( 773 | frame.dimensions.width * 0.5 774 | ); 775 | //connection.left = stemConnection.left; 776 | break; 777 | case "right": 778 | left += availableWidth - stemWidth; 779 | 780 | move.left = availableWidth - stemWidth; 781 | 782 | stemConnection.left = frame.dimensions.width - stemMiddleFromSide; 783 | connection.left = frame.dimensions.width; 784 | break; 785 | } 786 | 787 | // if we're working with the bottom stems we have to add the height to the connection 788 | if (split[1] === "bottom") { 789 | stemConnection.top += frame.dimensions.height; 790 | connection.top += frame.dimensions.height; 791 | } 792 | 793 | $.extend(layout.stem, { 794 | position: { left: left }, 795 | before: { width: left }, 796 | after: { 797 | left: left + stemWidth, 798 | //right: 0, // seems to work better in Chrome (subpixel bug) 799 | // but it fails in oldIE, se we add overlap to compensate 800 | width: width - left - stemWidth + 1, 801 | }, 802 | }); 803 | } else { 804 | // we are dealing with height 805 | var height = frame.dimensions.height; 806 | 807 | if (this.tooltip.options.stem) { 808 | height = frame.shift.dimensions.height; 809 | 810 | if (height - stemWidth < 2 * stemOffset) { 811 | stemOffset = Math.floor((height - stemWidth) * 0.5) || 0; 812 | } 813 | 814 | stemMiddleFromSide = cut + stemOffset + stemWidth * 0.5; 815 | } 816 | 817 | var availableHeight = height - 2 * stemOffset; 818 | 819 | var split = Position.split(position); 820 | var top = stemOffset; 821 | switch (split[2]) { 822 | case "top": 823 | move.down = availableHeight - stemWidth; 824 | 825 | stemConnection.top = stemMiddleFromSide; 826 | break; 827 | case "middle": 828 | top += Math.round(availableHeight * 0.5 - stemWidth * 0.5); 829 | 830 | move.up = top - stemOffset; 831 | move.down = top - stemOffset; 832 | 833 | stemConnection.top = connection.top = Math.round( 834 | frame.dimensions.height * 0.5 835 | ); 836 | break; 837 | case "bottom": 838 | top += availableHeight - stemWidth; 839 | 840 | move.up = availableHeight - stemWidth; 841 | 842 | stemConnection.top = frame.dimensions.height - stemMiddleFromSide; 843 | connection.top = frame.dimensions.height; 844 | break; 845 | } 846 | 847 | // if we're working with the right stems we have to add the height to the connection 848 | if (split[1] === "right") { 849 | stemConnection.left += frame.dimensions.width; 850 | connection.left += frame.dimensions.width; 851 | } 852 | 853 | $.extend(layout.stem, { 854 | position: { top: top }, 855 | before: { height: top }, 856 | after: { 857 | top: top + stemWidth, 858 | height: height - top - stemWidth + 1, 859 | }, 860 | }); 861 | } 862 | 863 | // store movement and connection 864 | layout.move = move; 865 | layout.stem.connection = stemConnection; 866 | layout.connection = connection; 867 | 868 | return layout; 869 | }, 870 | 871 | // sets the stem as one of the available 12 positions 872 | // we also need to call this function without a stem because it sets 873 | // connections 874 | setStemPosition: function (stemPosition, shift) { 875 | if (this._stemPosition !== stemPosition) { 876 | this._stemPosition = stemPosition; 877 | var side = Position.getSide(stemPosition); 878 | this.setSide(side); 879 | } 880 | 881 | // actual positioning 882 | if (this.tooltip.options.stem) { 883 | this.setStemShift(stemPosition, shift); 884 | } 885 | }, 886 | 887 | setStemShift: function (stemPosition, shift) { 888 | var _shift = this._shift, 889 | _dimensions = this._dimensions; 890 | // return if we have the same shift on the same dimensions 891 | if ( 892 | _shift && 893 | _shift.stemPosition === stemPosition && 894 | _shift.shift.x === shift.x && 895 | _shift.shift.y === shift.y && 896 | _dimensions && 897 | _shift.dimensions.width === _dimensions.width && 898 | _shift.dimensions.height === _dimensions.height 899 | ) { 900 | return; 901 | } 902 | this._shift = { 903 | stemPosition: stemPosition, 904 | shift: shift, 905 | dimensions: _dimensions, 906 | }; 907 | 908 | var side = Position.getSide(stemPosition), 909 | xy = { horizontal: "x", vertical: "y" }[ 910 | Position.getOrientation(stemPosition) 911 | ], 912 | leftWidth = { 913 | x: { left: "left", width: "width" }, 914 | y: { left: "top", width: "height" }, 915 | }[xy], 916 | stem = this["stem_" + side], 917 | layout = deepExtend({}, this._vars.connections[stemPosition].stem); 918 | 919 | // only use offset in the orientation of this position 920 | if (shift && shift[xy] !== 0) { 921 | layout.before[leftWidth["width"]] += shift[xy]; 922 | layout.position[leftWidth["left"]] += shift[xy]; 923 | layout.after[leftWidth["left"]] += shift[xy]; 924 | layout.after[leftWidth["width"]] -= shift[xy]; 925 | } 926 | 927 | // actual positioning 928 | stem.element.css(layout.position); 929 | stem.element.siblings(".tpd-shift-stem-side-before").css(layout.before); 930 | stem.element.siblings(".tpd-shift-stem-side-after").css(layout.after); 931 | }, 932 | }); 933 | -------------------------------------------------------------------------------- /src/js/skins.js: -------------------------------------------------------------------------------- 1 | Tipped.Skins = { 2 | // base skin, don't modify! (create custom skins in a separate file) 3 | base: { 4 | afterUpdate: false, 5 | ajax: {}, 6 | cache: true, 7 | container: false, 8 | containment: { 9 | selector: "viewport", 10 | padding: 5, 11 | }, 12 | close: false, 13 | detach: true, 14 | fadeIn: 200, 15 | fadeOut: 200, 16 | showDelay: 75, 17 | hideDelay: 25, 18 | hideAfter: false, 19 | hideOn: { element: "mouseleave" }, 20 | hideOthers: false, 21 | position: "top", 22 | inline: false, 23 | offset: { x: 0, y: 0 }, 24 | onHide: false, 25 | onShow: false, 26 | padding: true, 27 | radius: true, 28 | shadow: true, 29 | showOn: { element: "mousemove" }, 30 | size: "medium", 31 | spinner: true, 32 | stem: true, 33 | target: "element", 34 | voila: true, 35 | }, 36 | 37 | // Every other skin inherits from this one 38 | reset: { 39 | ajax: false, 40 | hideOn: { 41 | element: "mouseleave", 42 | tooltip: "mouseleave", 43 | }, 44 | showOn: { 45 | element: "mouseenter", 46 | tooltip: "mouseenter", 47 | }, 48 | }, 49 | }; 50 | 51 | $.each( 52 | "dark light gray red green blue lightyellow lightblue lightpink".split(" "), 53 | function (i, s) { 54 | Tipped.Skins[s] = {}; 55 | } 56 | ); 57 | -------------------------------------------------------------------------------- /src/js/stem.js: -------------------------------------------------------------------------------- 1 | function Stem() { 2 | this.initialize.apply(this, _slice.call(arguments)); 3 | } 4 | 5 | $.extend(Stem.prototype, { 6 | initialize: function (element, skin) { 7 | this.element = $(element); 8 | if (!this.element[0]) return; 9 | 10 | this.skin = skin; 11 | 12 | this.element.removeClass("tpd-stem-reset"); // for correct offset 13 | this._css = $.extend({}, skin._css, { 14 | width: this.element.innerWidth(), 15 | height: this.element.innerHeight(), 16 | offset: parseFloat(this.element.css("margin-left")), // side 17 | spacing: parseFloat(this.element.css("margin-top")), 18 | }); 19 | this.element.addClass("tpd-stem-reset"); 20 | 21 | this.options = $.extend({}, arguments[2] || {}); 22 | 23 | this._position = this.element.attr("data-stem-position") || "top"; 24 | this._m = 100; // multiplier, improves rendering when scaling everything down 25 | 26 | this.build(); 27 | }, 28 | 29 | destroy: function () { 30 | this.element.html(""); 31 | }, 32 | 33 | build: function () { 34 | this.destroy(); 35 | 36 | // figure out low opacity based on the background color 37 | var backgroundColor = this._css.backgroundColor, 38 | alpha = 39 | backgroundColor.indexOf("rgba") > -1 && 40 | parseFloat(backgroundColor.replace(/^.*,(.+)\)/, "$1")), 41 | hasLowOpacityTriangle = alpha && alpha < 1; 42 | 43 | // if the triangle doesn't have opacity or when we don't have to deal with a border 44 | // we can get away with a better way to draw the stem. 45 | // otherwise we need to draw the border as a seperate element, but that 46 | // can only happen on browsers with support for transforms 47 | this._useTransform = hasLowOpacityTriangle && Support.css.transform; 48 | if (!this._css.border) this._useTransform = false; 49 | this[(this._useTransform ? "build" : "buildNo") + "Transform"](); 50 | }, 51 | 52 | buildTransform: function () { 53 | this.element.append( 54 | (this.spacer = $("
") 55 | .addClass("tpd-stem-spacer") 56 | .append( 57 | (this.downscale = $("
") 58 | .addClass("tpd-stem-downscale") 59 | .append( 60 | (this.transform = $("
") 61 | .addClass("tpd-stem-transform") 62 | .append( 63 | (this.first = $("
") 64 | .addClass("tpd-stem-side") 65 | .append( 66 | (this.border = $("
").addClass("tpd-stem-border")) 67 | ) 68 | .append($("
").addClass("tpd-stem-border-corner")) 69 | .append($("
").addClass("tpd-stem-triangle"))) 70 | )) 71 | )) 72 | )) 73 | ); 74 | this.transform.append( 75 | (this.last = this.first.clone().addClass("tpd-stem-side-inversed")) 76 | ); 77 | this.sides = this.first.add(this.last); 78 | 79 | var math = this.getMath(), 80 | md = math.dimensions, 81 | _m = this._m, 82 | _side = Position.getSide(this._position); 83 | 84 | //if (!math.enabled) return; 85 | 86 | this.element.find(".tpd-stem-spacer").css({ 87 | width: _flip ? md.inside.height : md.inside.width, 88 | height: _flip ? md.inside.width : md.inside.height, 89 | }); 90 | if (_side === "top" || _side === "left") { 91 | var _scss = {}; 92 | if (_side === "top") { 93 | _scss.bottom = 0; 94 | _scss.top = "auto"; 95 | } else if (_side === "left") { 96 | _scss.right = 0; 97 | _scss.left = "auto"; 98 | } 99 | 100 | this.element.find(".tpd-stem-spacer").css(_scss); 101 | } 102 | 103 | this.transform.css({ 104 | width: md.inside.width * _m, 105 | height: md.inside.height * _m, 106 | }); 107 | 108 | // adjust the dimensions of the element to that of the 109 | var _transform = Support.css.prefixed("transform"); 110 | 111 | // triangle 112 | var triangleStyle = { 113 | "background-color": "transparent", 114 | "border-bottom-color": this._css.backgroundColor, 115 | "border-left-width": md.inside.width * 0.5 * _m, 116 | "border-bottom-width": md.inside.height * _m, 117 | }; 118 | triangleStyle[_transform] = "translate(" + math.border * _m + "px, 0)"; 119 | this.element.find(".tpd-stem-triangle").css(triangleStyle); 120 | 121 | // border 122 | // first convert color to rgb + opacity 123 | // otherwise we'd be working with a border that overlays the background 124 | var borderColor = this._css.borderColor; 125 | alpha = 126 | borderColor.indexOf("rgba") > -1 && 127 | parseFloat(borderColor.replace(/^.*,(.+)\)/, "$1")); 128 | if (alpha && alpha < 1) { 129 | // turn the borderColor into a color without alpha 130 | borderColor = ( 131 | borderColor.substring(0, borderColor.lastIndexOf(",")) + ")" 132 | ).replace("rgba", "rgb"); 133 | } else { 134 | alpha = 1; 135 | } 136 | 137 | var borderStyle = { 138 | "background-color": "transparent", 139 | "border-right-width": math.border * _m, 140 | width: math.border * _m, 141 | "margin-left": -2 * math.border * _m, 142 | "border-color": borderColor, 143 | opacity: alpha, 144 | }; 145 | borderStyle[_transform] = 146 | "skew(" + 147 | math.skew + 148 | "deg) translate(" + 149 | math.border * _m + 150 | "px, " + 151 | -1 * this._css.border * _m + 152 | "px)"; 153 | this.element.find(".tpd-stem-border").css(borderStyle); 154 | 155 | var borderColor = this._css.borderColor; 156 | alpha = 157 | borderColor.indexOf("rgba") > -1 && 158 | parseFloat(borderColor.replace(/^.*,(.+)\)/, "$1")); 159 | if (alpha && alpha < 1) { 160 | // turn the borderColor into a color without alpha 161 | borderColor = ( 162 | borderColor.substring(0, borderColor.lastIndexOf(",")) + ")" 163 | ).replace("rgba", "rgb"); 164 | } else { 165 | alpha = 1; 166 | } 167 | 168 | var borderCornerStyle = { 169 | width: math.border * _m, 170 | "border-right-width": math.border * _m, 171 | "border-right-color": borderColor, 172 | background: borderColor, 173 | opacity: alpha, 174 | // setting opacity here causes a flicker in firefox, it's set in css now 175 | // 'opacity': this._css.borderOpacity, 176 | "margin-left": -2 * math.border * _m, 177 | }; 178 | borderCornerStyle[_transform] = 179 | "skew(" + 180 | math.skew + 181 | "deg) translate(" + 182 | math.border * _m + 183 | "px, " + 184 | (md.inside.height - this._css.border) * _m + 185 | "px)"; 186 | 187 | this.element.find(".tpd-stem-border-corner").css(borderCornerStyle); 188 | 189 | // measurements are done, now flip things if needed 190 | this.setPosition(this._position); 191 | 192 | // now downscale to improve subpixel rendering 193 | if (_m > 1) { 194 | var t = {}; 195 | t[_transform] = "scale(" + 1 / _m + "," + 1 / _m + ")"; 196 | this.downscale.css(t); 197 | } 198 | // switch around the visible dimensions if needed 199 | var _flip = /^(left|right)$/.test(this._position); 200 | 201 | if (!this._css.border) { 202 | this.element.find(".tpd-stem-border, .tpd-stem-border-corner").hide(); 203 | } 204 | 205 | this.element.css({ 206 | width: _flip ? md.outside.height : md.outside.width, 207 | height: _flip ? md.outside.width : md.outside.height, 208 | }); 209 | }, 210 | 211 | buildNoTransform: function () { 212 | this.element.append( 213 | (this.spacer = $("
") 214 | .addClass("tpd-stem-spacer") 215 | .append( 216 | $("
") 217 | .addClass("tpd-stem-notransform") 218 | .append( 219 | $("
") 220 | .addClass("tpd-stem-border") 221 | .append($("
").addClass("tpd-stem-border-corner")) 222 | .append( 223 | $("
") 224 | .addClass("tpd-stem-border-center-offset") 225 | .append( 226 | $("
") 227 | .addClass("tpd-stem-border-center-offset-inverse") 228 | .append($("
").addClass("tpd-stem-border-center")) 229 | ) 230 | ) 231 | ) 232 | .append($("
").addClass("tpd-stem-triangle")) 233 | )) 234 | ); 235 | 236 | var math = this.getMath(), 237 | md = math.dimensions; 238 | 239 | var _flip = /^(left|right)$/.test(this._position), 240 | _bottom = /^(bottom)$/.test(this._position), 241 | _right = /^(right)$/.test(this._position), 242 | _side = Position.getSide(this._position); 243 | 244 | this.element.css({ 245 | width: _flip ? md.outside.height : md.outside.width, 246 | height: _flip ? md.outside.width : md.outside.height, 247 | }); 248 | 249 | // handle spacer 250 | this.element 251 | .find(".tpd-stem-notransform") 252 | .add(this.element.find(".tpd-stem-spacer")) 253 | .css({ 254 | width: _flip ? md.inside.height : md.inside.width, 255 | height: _flip ? md.inside.width : md.inside.height, 256 | }); 257 | if (_side === "top" || _side === "left") { 258 | var _scss = {}; 259 | if (_side === "top") { 260 | _scss.bottom = 0; 261 | _scss.top = "auto"; 262 | } else if (_side === "left") { 263 | _scss.right = 0; 264 | _scss.left = "auto"; 265 | } 266 | 267 | this.element.find(".tpd-stem-spacer").css(_scss); 268 | } 269 | 270 | // resets 271 | this.element.find(".tpd-stem-border").css({ 272 | width: "100%", 273 | background: "transparent", 274 | }); 275 | 276 | // == on bottom 277 | var borderCornerStyle = { 278 | opacity: 1, 279 | }; 280 | 281 | borderCornerStyle[_flip ? "height" : "width"] = "100%"; 282 | borderCornerStyle[_flip ? "width" : "height"] = this._css.border; 283 | borderCornerStyle[_bottom ? "top" : "bottom"] = 0; 284 | 285 | $.extend(borderCornerStyle, !_right ? { right: 0 } : { left: 0 }); 286 | 287 | this.element.find(".tpd-stem-border-corner").css(borderCornerStyle); 288 | 289 | // border /\ 290 | // top or bottom 291 | var borderStyle = { 292 | width: 0, 293 | "background-color": "transparent", 294 | opacity: 1, 295 | }; 296 | 297 | var borderSideCSS = md.inside.width * 0.5 + "px solid transparent"; 298 | 299 | var triangleStyle = { "background-color": "transparent" }; 300 | var triangleSideCSS = 301 | md.inside.width * 0.5 - math.border + "px solid transparent"; 302 | 303 | if (!_flip) { 304 | var shared = { 305 | "margin-left": -0.5 * md.inside.width, 306 | "border-left": borderSideCSS, 307 | "border-right": borderSideCSS, 308 | }; 309 | 310 | // == 311 | $.extend(borderStyle, shared); 312 | borderStyle[_bottom ? "border-top" : "border-bottom"] = 313 | md.inside.height + "px solid " + this._css.borderColor; 314 | 315 | // /\ 316 | $.extend(triangleStyle, shared); 317 | triangleStyle[_bottom ? "border-top" : "border-bottom"] = 318 | md.inside.height + "px solid " + this._css.backgroundColor; 319 | triangleStyle[!_bottom ? "top" : "bottom"] = math.top; 320 | triangleStyle[_bottom ? "top" : "bottom"] = "auto"; 321 | 322 | // add offset 323 | this.element 324 | .find(".tpd-stem-border-center-offset") 325 | .css({ 326 | "margin-top": -1 * this._css.border * (_bottom ? -1 : 1), 327 | }) 328 | .find(".tpd-stem-border-center-offset-inverse") 329 | .css({ 330 | "margin-top": this._css.border * (_bottom ? -1 : 1), 331 | }); 332 | } else { 333 | var shared = { 334 | left: "auto", 335 | top: "50%", 336 | "margin-top": -0.5 * md.inside.width, 337 | "border-top": borderSideCSS, 338 | "border-bottom": borderSideCSS, 339 | }; 340 | 341 | // == 342 | $.extend(borderStyle, shared); 343 | borderStyle[_right ? "right" : "left"] = 0; 344 | borderStyle[_right ? "border-left" : "border-right"] = 345 | md.inside.height + "px solid " + this._css.borderColor; 346 | 347 | // /\ 348 | $.extend(triangleStyle, shared); 349 | triangleStyle[_right ? "border-left" : "border-right"] = 350 | md.inside.height + "px solid " + this._css.backgroundColor; 351 | triangleStyle[!_right ? "left" : "right"] = math.top; 352 | triangleStyle[_right ? "left" : "right"] = "auto"; 353 | 354 | // add offset 355 | this.element 356 | .find(".tpd-stem-border-center-offset") 357 | .css({ 358 | "margin-left": -1 * this._css.border * (_right ? -1 : 1), 359 | }) 360 | .find(".tpd-stem-border-center-offset-inverse") 361 | .css({ 362 | "margin-left": this._css.border * (_right ? -1 : 1), 363 | }); 364 | } 365 | 366 | this.element.find(".tpd-stem-border-center").css(borderStyle); 367 | this.element 368 | .find(".tpd-stem-border-corner") 369 | .css({ "background-color": this._css.borderColor }); 370 | this.element.find(".tpd-stem-triangle").css(triangleStyle); 371 | 372 | if (!this._css.border) { 373 | this.element.find(".tpd-stem-border").hide(); 374 | } 375 | }, 376 | 377 | setPosition: function (position) { 378 | this._position = position; 379 | this.transform.attr( 380 | "class", 381 | "tpd-stem-transform tpd-stem-transform-" + position 382 | ); 383 | }, 384 | 385 | getMath: function () { 386 | var height = this._css.height, 387 | width = this._css.width, 388 | border = this._css.border; 389 | 390 | // width should be divisible by 2 391 | // this fixes pixel bugs in the transform methods, so only do it there 392 | if (this._useTransform && !!(Math.floor(width) % 2)) { 393 | width = Math.max(Math.floor(width) - 1, 0); 394 | } 395 | 396 | // first increase the original dimensions so the triangle is that of the given css dimensions 397 | var corner_top = degrees(Math.atan((width * 0.5) / height)), 398 | corner_side = 90 - corner_top, 399 | side = border / Math.cos(((90 - corner_side) * Math.PI) / 180), 400 | top = border / Math.cos(((90 - corner_top) * Math.PI) / 180); 401 | var dimensions = { 402 | width: width + side * 2, 403 | height: height + top, 404 | }; 405 | 406 | var cut = Math.max(border, this._css.radius); 407 | 408 | // adjust height and width 409 | height = dimensions.height; 410 | width = dimensions.width * 0.5; 411 | 412 | // calculate the rest 413 | var cA = degrees(Math.atan(height / width)), 414 | cB = 90 - cA, 415 | overstaand = border / Math.cos((cB * Math.PI) / 180); 416 | 417 | var angle = (Math.atan(height / width) * 180) / Math.PI, 418 | skew = -1 * (90 - angle), 419 | angleTop = 90 - angle, 420 | cornerWidth = border * Math.tan((angleTop * Math.PI) / 180); 421 | 422 | var top = border / Math.cos(((90 - angleTop) * Math.PI) / 180); 423 | 424 | // add spacing 425 | //dimensions.height += this._css.spacing; 426 | var inside = $.extend({}, dimensions), 427 | outside = $.extend({}, dimensions); 428 | outside.height += this._css.spacing; 429 | 430 | // IE11 and below have issues with rendering stems that 431 | // end up with floating point dimensions 432 | // ceil the outside height to fix this 433 | outside.height = Math.ceil(outside.height); 434 | 435 | // if the border * 2 is bigger than the width, we should disable the stem 436 | var enabled = true; 437 | if (border * 2 >= dimensions.width) { 438 | enabled = false; 439 | } 440 | 441 | return { 442 | enabled: enabled, 443 | outside: outside, 444 | dimensions: { 445 | inside: inside, 446 | outside: outside, 447 | }, 448 | top: top, 449 | border: overstaand, 450 | skew: skew, 451 | corner: cornerWidth, 452 | }; 453 | }, 454 | }); 455 | -------------------------------------------------------------------------------- /src/js/tooltip.js: -------------------------------------------------------------------------------- 1 | function Tooltip() { 2 | this.initialize.apply(this, _slice.call(arguments)); 3 | } 4 | 5 | $.extend(Tooltip.prototype, { 6 | supportsLoading: Support.css.transform && Support.css.animation, 7 | 8 | initialize: function (element, content) { 9 | this.element = element; 10 | if (!this.element) return; 11 | 12 | var options; 13 | if ( 14 | typeof content === "object" && 15 | !( 16 | _.isElement(content) || 17 | _.isText(content) || 18 | _.isDocumentFragment(content) || 19 | content instanceof $ 20 | ) 21 | ) { 22 | options = content; 23 | content = null; 24 | } else { 25 | options = arguments[2] || {}; 26 | } 27 | 28 | // append element options if data-tpd-options 29 | var dataOptions = $(element).data("tipped-options"); 30 | if (dataOptions) { 31 | options = deepExtend( 32 | $.extend({}, options), 33 | eval("({" + dataOptions + "})") 34 | ); 35 | } 36 | 37 | this.options = Options.create(options); 38 | 39 | // all the garbage goes in here 40 | this._cache = { 41 | dimensions: { 42 | width: 0, 43 | height: 0, 44 | }, 45 | events: [], 46 | timers: {}, 47 | layouts: {}, 48 | is: {}, 49 | fnCallFn: "", 50 | updatedTo: {}, 51 | }; 52 | 53 | // queues for effects 54 | this.queues = { 55 | showhide: $({}), 56 | }; 57 | 58 | // title 59 | var title = 60 | $(element).attr("title") || $(element).data("tipped-restore-title"); 61 | 62 | if (!content) { 63 | // grab the content off the attribute 64 | var dt = $(element).attr("data-tipped"); 65 | 66 | if (dt) { 67 | content = dt; 68 | } else if (title) { 69 | content = title; 70 | } 71 | 72 | if (content) { 73 | // avoid scripts in title/data-tipped 74 | var SCRIPT_REGEX = 75 | /)<[^<]*)*<\/script>/gi; 76 | content = content.replace(SCRIPT_REGEX, ""); 77 | } 78 | } 79 | 80 | if ( 81 | (!content || (content instanceof $ && !content[0])) && 82 | !((this.options.ajax && this.options.ajax.url) || this.options.inline) 83 | ) { 84 | this._aborted = true; 85 | return; 86 | } 87 | 88 | // backup title 89 | if (title) { 90 | // backup the title so we can restore it once the tooltip is removed 91 | $(element).data("tipped-restore-title", title); 92 | $(element)[0].setAttribute("title", ""); // IE needs setAttribute 93 | } 94 | 95 | this.content = content; 96 | this.title = $(this.element).data("tipped-title"); 97 | if (typeof this.options.title !== "undefined") 98 | this.title = this.options.title; 99 | 100 | this.zIndex = this.options.zIndex || +Tooltips.options.startingZIndex; 101 | 102 | // make sure the element has a uids array 103 | var uids = $(element).data("tipped-uids"); //, initial_uid = uid; 104 | if (!uids) { 105 | uids = []; 106 | } 107 | 108 | // generate a new uid 109 | var uid = getUID(); 110 | this.uid = uid; 111 | uids.push(uid); 112 | 113 | // store grown uids array back into data 114 | $(element).data("tipped-uids", uids); 115 | 116 | // mark parent tooltips as being a nest if this tooltip is created on an element within another tooltip 117 | var parentTooltipElement = $(this.element).closest(".tpd-tooltip")[0], 118 | parentTooltip; 119 | if ( 120 | parentTooltipElement && 121 | (parentTooltip = 122 | Tooltips.getTooltipByTooltipElement(parentTooltipElement)) 123 | ) { 124 | parentTooltip.is("nest", true); 125 | } 126 | 127 | // set the target 128 | var target = this.options.target; 129 | this.target = 130 | target === "mouse" 131 | ? this.element 132 | : target === "element" || !target 133 | ? this.element 134 | : _.isElement(target) 135 | ? target 136 | : target instanceof $ && target[0] 137 | ? target[0] 138 | : this.element; 139 | 140 | // for inline content 141 | if (this.options.inline) { 142 | this.content = $("#" + this.options.inline)[0]; 143 | } 144 | 145 | // ajax might not be using ajax: { url: ... } but instead have the 2nd parameter as its url 146 | // we store _content 147 | if (this.options.ajax) { 148 | this.__content = this.content; 149 | } 150 | 151 | // function as content 152 | if (typeof this.content === "function") { 153 | this._fn = this.content; 154 | } 155 | 156 | this.preBuild(); 157 | 158 | Tooltips.add(this); 159 | }, 160 | 161 | remove: function () { 162 | this.unbind(); 163 | 164 | this.clearTimers(); 165 | 166 | // restore content if it was an element attached to the DOM before insertion 167 | this.restoreElementToMarker(); 168 | 169 | this.stopLoading(); 170 | this.abort(); 171 | 172 | // delete the tooltip 173 | if (this.is("build") && this._tooltip) { 174 | this._tooltip.remove(); 175 | this._tooltip = null; 176 | } 177 | 178 | var uids = $(this.element).data("tipped-uids") || []; 179 | var uid_index = $.inArray(this.uid, uids); 180 | if (uid_index > -1) { 181 | uids.splice(uid_index, 1); 182 | $(this.element).data("tipped-uids", uids); 183 | } 184 | 185 | if (uids.length < 1) { 186 | // restore title 187 | var da = "tipped-restore-title", 188 | r_title; 189 | if ((r_title = $(this.element).data(da))) { 190 | // only restore it when the title hasn't been altered 191 | if (!$(this.element)[0].getAttribute("title") != "") { 192 | $(this.element).attr("title", r_title); 193 | } 194 | // remove the data 195 | $(this.element).removeData(da); 196 | } 197 | 198 | // remove the data attribute uid 199 | $(this.element).removeData("tipped-uids"); 200 | } 201 | 202 | // remove any delegation classes 203 | var classList = $(this.element).attr("class") || "", 204 | newClassList = classList 205 | .replace(/(tpd-delegation-uid-)\d+/g, "") 206 | .replace(/^\s\s*/, "") 207 | .replace(/\s\s*$/, ""); // trim whitespace 208 | $(this.element).attr("class", newClassList); 209 | }, 210 | 211 | detach: function () { 212 | if (this.options.detach && !this.is("detached")) { 213 | if (this._tooltip) this._tooltip.detach(); 214 | this.is("detached", true); 215 | } 216 | }, 217 | 218 | attach: function () { 219 | if (this.is("detached")) { 220 | var container; 221 | if (typeof this.options.container === "string") { 222 | var target = this.target; 223 | if (target === "mouse") { 224 | target = this.element; 225 | } 226 | 227 | container = $($(target).closest(this.options.container).first()); 228 | } else { 229 | container = $(this.options.container); 230 | } 231 | 232 | // we default to document body, if nothing was found 233 | if (!container[0]) container = $(document.body); 234 | 235 | container.append(this._tooltip); 236 | this.is("detached", false); 237 | } 238 | }, 239 | 240 | preBuild: function () { 241 | this.is("detached", true); 242 | 243 | var initialCSS = { 244 | left: "-10000px", // TODO: remove 245 | top: "-10000px", 246 | opacity: 0, 247 | zIndex: this.zIndex, 248 | }; 249 | 250 | this._tooltip = $("
") 251 | .addClass("tpd-tooltip") 252 | .addClass("tpd-skin-" + this.options.skin) 253 | .addClass("tpd-size-" + this.options.size) 254 | .css(initialCSS) 255 | .hide(); 256 | 257 | this.createPreBuildObservers(); 258 | }, 259 | 260 | build: function () { 261 | if (this.is("build")) return; 262 | 263 | this.attach(); 264 | 265 | this._tooltip.append((this._skin = $("
").addClass("tpd-skin"))).append( 266 | (this._contentWrapper = $("
") 267 | .addClass("tpd-content-wrapper") 268 | .append( 269 | (this._contentSpacer = $("
") 270 | .addClass("tpd-content-spacer") 271 | .append( 272 | (this._titleWrapper = $("
") 273 | .addClass("tpd-title-wrapper") 274 | .append( 275 | (this._titleSpacer = $("
") 276 | .addClass("tpd-title-spacer") 277 | .append( 278 | (this._titleRelative = $("
") 279 | .addClass("tpd-title-relative") 280 | .append( 281 | (this._titleRelativePadder = $("
") 282 | .addClass("tpd-title-relative-padder") 283 | .append( 284 | (this._title = $("
").addClass("tpd-title")) 285 | )) 286 | )) 287 | )) 288 | ) 289 | .append( 290 | (this._close = $("
") 291 | .addClass("tpd-close") 292 | .append( 293 | $("
").addClass("tpd-close-icon").html("×") 294 | )) 295 | )) 296 | ) 297 | .append( 298 | (this._contentRelative = $("
") 299 | .addClass("tpd-content-relative") 300 | .append( 301 | (this._contentRelativePadder = $("
") 302 | .addClass("tpd-content-relative-padder") 303 | .append( 304 | (this._content = $("
").addClass("tpd-content")) 305 | )) 306 | ) 307 | .append( 308 | (this._inner_close = $("
") 309 | .addClass("tpd-close") 310 | .append( 311 | $("
").addClass("tpd-close-icon").html("×") 312 | )) 313 | )) 314 | )) 315 | )) 316 | ); 317 | 318 | this.skin = new Skin(this); // TODO: remove instances of is('skinned'), and look into why they are there 319 | 320 | // set radius of contenspacer to be that found on the skin 321 | this._contentSpacer.css({ 322 | "border-radius": Math.max( 323 | this.skin._css.radius - this.skin._css.border, 324 | 0 325 | ), 326 | }); 327 | 328 | this.createPostBuildObservers(); 329 | 330 | this.is("build", true); 331 | }, 332 | 333 | createPostBuildObservers: function () { 334 | // x 335 | this._tooltip.delegate( 336 | ".tpd-close, .close-tooltip", 337 | "click", 338 | function (event) { 339 | // this helps prevent the click on x to trigger a click on the body 340 | // which could conflict with some scripts 341 | event.stopPropagation(); 342 | event.preventDefault(); 343 | 344 | this.is("api", false); 345 | 346 | this.hide(true); 347 | }.bind(this) 348 | ); 349 | }, 350 | 351 | createPreBuildObservers: function () { 352 | // what can be observed before build 353 | // - the element 354 | this.bind(this.element, "mouseenter", this.setActive); 355 | this.bind( 356 | this._tooltip, 357 | // avoid double click issues 358 | Support.touch && Browser.MobileSafari ? "touchstart" : "mouseenter", 359 | this.setActive 360 | ); 361 | 362 | // idle stats 363 | this.bind(this.element, "mouseleave", function (event) { 364 | this.setIdle(event); 365 | }); 366 | this.bind(this._tooltip, "mouseleave", function (event) { 367 | this.setIdle(event); 368 | }); 369 | 370 | if (this.options.showOn) { 371 | $.each( 372 | this.options.showOn, 373 | function (name, events) { 374 | var element, 375 | toggleable = false; 376 | 377 | switch (name) { 378 | case "element": 379 | element = this.element; 380 | 381 | if ( 382 | this.options.hideOn && 383 | this.options.showOn && 384 | this.options.hideOn.element === "click" && 385 | this.options.showOn.element === "click" 386 | ) { 387 | toggleable = true; 388 | this.is("toggleable", toggleable); 389 | } 390 | break; 391 | 392 | case "tooltip": 393 | element = this._tooltip; 394 | break; 395 | case "target": 396 | element = this.target; 397 | break; 398 | } 399 | 400 | if (!element) return; 401 | 402 | if (events) { 403 | // Translate mouseenter to touchstart 404 | // just for the tooltip to fix double click issues 405 | // https://davidwalsh.name/ios-hover-menu-fix 406 | var useEvents = events; 407 | 408 | this.bind( 409 | element, 410 | useEvents, 411 | events === "click" && toggleable 412 | ? function () { 413 | this.is("api", false); 414 | this.toggle(); 415 | } 416 | : function () { 417 | this.is("api", false); 418 | this.showDelayed(); 419 | } 420 | ); 421 | } 422 | }.bind(this) 423 | ); 424 | 425 | // iOS requires that we track touchend time to avoid 426 | // links requiring a double-click 427 | if (Support.touch && Browser.MobileSafari) { 428 | this.bind(this._tooltip, "touchend", function () { 429 | this._tooltipTouchEndTime = new Date().getTime(); 430 | }); 431 | } 432 | } 433 | 434 | if (this.options.hideOn) { 435 | $.each( 436 | this.options.hideOn, 437 | function (name, events) { 438 | var element; 439 | 440 | switch (name) { 441 | case "element": 442 | // no events needed if the element toggles 443 | if (this.is("toggleable") && events === "click") return; 444 | element = this.element; 445 | break; 446 | case "tooltip": 447 | element = this._tooltip; 448 | break; 449 | case "target": 450 | element = this.target; 451 | break; 452 | } 453 | 454 | // if we don't have an element now we don't have to attach anything 455 | if (!element) return; 456 | 457 | if (events) { 458 | var useEvents = events; 459 | 460 | // prevent having to double-click links on iOS 461 | // by comparing the touchend time on the tooltip to a mouseleave/out 462 | // triggered on the element or target, if it is within a short duration 463 | // we cancel the hide event. 464 | // we basically track if we've moved from element/target to tooltip 465 | if ( 466 | Support.touch && 467 | Browser.MobileSafari && 468 | /^(target|element)/.test(name) && 469 | /mouse(leave|out)/.test(useEvents) 470 | ) { 471 | this.bind(element, useEvents, function (event) { 472 | if ( 473 | this._tooltipTouchEndTime && 474 | /^mouse(leave|out)$/.test(event.type) 475 | ) { 476 | var now = new Date().getTime(); 477 | if (now - this._tooltipTouchEndTime < 450) { 478 | // quicktap (355-369ms) 479 | return; 480 | } 481 | } 482 | this.is("api", false); 483 | this.hideDelayed(); 484 | }); 485 | } else { 486 | this.bind(element, useEvents, function () { 487 | this.is("api", false); 488 | this.hideDelayed(); 489 | }); 490 | } 491 | } 492 | }.bind(this) 493 | ); 494 | } 495 | 496 | if (this.options.hideOnClickOutside) { 497 | // add a class to check for the hideOnClickOutSide element 498 | $(this.element).addClass("tpd-hideOnClickOutside"); 499 | 500 | // touchend is an iOS fix to prevent the need to double tap 501 | // without this it doesn't even work at all on iOS 502 | this.bind( 503 | document.documentElement, 504 | "click touchend", 505 | function (event) { 506 | if (!this.visible()) return; 507 | 508 | var element = $(event.target).closest( 509 | ".tpd-tooltip, .tpd-hideOnClickOutside" 510 | )[0]; 511 | 512 | if ( 513 | !element || 514 | (element && 515 | element !== this._tooltip[0] && 516 | element !== this.element) 517 | ) { 518 | this.hide(); 519 | } 520 | }.bind(this) 521 | ); 522 | } 523 | 524 | if (this.options.target === "mouse") { 525 | this.bind( 526 | this.element, 527 | "mouseenter mousemove", 528 | function (event) { 529 | this._cache.event = event; 530 | }.bind(this) 531 | ); 532 | } 533 | 534 | var isMouseMove = false; 535 | if ( 536 | this.options.showOn && 537 | this.options.target === "mouse" && 538 | !this.options.fixed 539 | ) { 540 | isMouseMove = true; 541 | } 542 | 543 | if (isMouseMove) { 544 | this.bind(this.element, "mousemove", function () { 545 | if (!this.is("build")) return; 546 | this.is("api", false); 547 | this.position(); 548 | }); 549 | } 550 | }, 551 | }); 552 | -------------------------------------------------------------------------------- /src/js/tooltip/active.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | setActive: function () { 3 | this.is("active", true); 4 | 5 | // raise the tooltip if it's visible 6 | if (this.visible()) { 7 | this.raise(); 8 | } 9 | 10 | if (this.options.hideAfter) { 11 | this.clearTimer("idle"); 12 | } 13 | }, 14 | setIdle: function () { 15 | this.is("active", false); 16 | 17 | if (this.options.hideAfter) { 18 | this.setTimer( 19 | "idle", 20 | function () { 21 | this.clearTimer("idle"); 22 | 23 | if (!this.is("active")) { 24 | this.hide(); 25 | } 26 | }.bind(this), 27 | this.options.hideAfter 28 | ); 29 | } 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/js/tooltip/bind.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | // bind with cached event to make unbinding cached handlers easy 3 | bind: function (element, eventName, handler, context) { 4 | var cachedHandler = handler.bind(context || this); 5 | 6 | this._cache.events.push({ 7 | element: element, 8 | eventName: eventName, 9 | handler: cachedHandler, 10 | }); 11 | 12 | $(element).on(eventName, cachedHandler); 13 | }, 14 | 15 | unbind: function () { 16 | $.each(this._cache.events, function (i, event) { 17 | $(event.element).off(event.eventName, event.handler); 18 | }); 19 | 20 | this._cache.events = []; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/js/tooltip/disable.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | disable: function () { 3 | if (this.is("disabled")) return; 4 | this.is("disabled", true); 5 | }, 6 | 7 | enable: function () { 8 | if (!this.is("disabled")) return; 9 | this.is("disabled", false); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/js/tooltip/display.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | // make sure there are no animations queued up, and stop any animations currently going on 3 | stop: function () { 4 | // cancel when we call this function before the tooltip is created 5 | if (!this._tooltip) return; 6 | 7 | var shq = this.queues.showhide; 8 | // clear queue 9 | shq.queue([]); 10 | // stop possible show/hide event 11 | this._tooltip.stop(1, 0); 12 | }, 13 | 14 | showDelayed: function (event) { 15 | if (this.is("disabled")) return; 16 | 17 | // cancel hide timer 18 | this.clearTimer("hide"); 19 | 20 | // if there is a show timer we don't have to start another one 21 | if (this.is("visible") || this.getTimer("show")) return; 22 | 23 | // otherwise we start one 24 | this.setTimer( 25 | "show", 26 | function () { 27 | this.clearTimer("show"); 28 | this.show(); 29 | }.bind(this), 30 | this.options.showDelay || 1 31 | ); 32 | }, 33 | 34 | show: function () { 35 | this.clearTimer("hide"); 36 | 37 | // don't show tooltip already visible or on hidden targets, those would end up at (0, 0) 38 | if ( 39 | this.visible() || 40 | this.is("disabled") || 41 | !$(this.target).is(":visible") 42 | ) { 43 | return; 44 | } 45 | 46 | this.is("visible", true); 47 | 48 | this.attach(); 49 | 50 | this.stop(); 51 | var shq = this.queues.showhide; 52 | 53 | // update 54 | if (!(this.is("updated") || this.is("updating"))) { 55 | shq.queue( 56 | function (next_updated) { 57 | this._onResizeDimensions = { width: 0, height: 0 }; 58 | 59 | this.update( 60 | function (aborted) { 61 | if (aborted) { 62 | this.is("visible", false); 63 | this.detach(); 64 | return; 65 | } 66 | 67 | next_updated(); 68 | }.bind(this) 69 | ); 70 | }.bind(this) 71 | ); 72 | } 73 | 74 | // sanitize every time 75 | // we've moved this outside of the update in 4.3 76 | // allowing the update to finish without conflicting with the sanitize 77 | // that might even be performed later or cancelled 78 | shq.queue( 79 | function (next_ready_to_show) { 80 | if (!this.is("sanitized")) { 81 | this._contentWrapper.css({ visibility: "hidden" }); 82 | 83 | this.startLoading(); 84 | 85 | this.sanitize( 86 | function () { 87 | this.stopLoading(); 88 | this._contentWrapper.css({ visibility: "visible" }); 89 | this.is("resize-to-content", true); 90 | next_ready_to_show(); 91 | }.bind(this) 92 | ); 93 | } else { 94 | // already sanitized 95 | this.stopLoading(); // always stop loading 96 | this._contentWrapper.css({ visibility: "visible" }); // and make visible 97 | this.is("resize-to-content", true); 98 | next_ready_to_show(); 99 | } 100 | }.bind(this) 101 | ); 102 | 103 | // position and raise 104 | // we always do this because when the tooltip hides and ajax updates, we'd otherwise have incorrect dimensions 105 | shq.queue( 106 | function (next_position_raise) { 107 | this.position(); 108 | this.raise(); 109 | next_position_raise(); 110 | }.bind(this) 111 | ); 112 | 113 | // onShow callback 114 | shq.queue( 115 | function (next_onshow) { 116 | // only fire it here if we've already updated 117 | if (this.is("updated") && typeof this.options.onShow === "function") { 118 | // 119 | var visible = new Visible(this._tooltip); 120 | this.options.onShow(this._content[0], this.element); // todo: update 121 | visible.restore(); 122 | next_onshow(); 123 | } else { 124 | next_onshow(); 125 | } 126 | }.bind(this) 127 | ); 128 | 129 | // Fade-in 130 | shq.queue( 131 | function (next_show) { 132 | this._show(/*instant ? 0 :*/ this.options.fadeIn, function () { 133 | next_show(); 134 | }); 135 | }.bind(this) 136 | ); 137 | }, 138 | 139 | _show: function (duration, callback) { 140 | duration = 141 | (typeof duration === "number" ? duration : this.options.fadeIn) || 0; 142 | callback = 143 | callback || (typeof arguments[0] == "function" ? arguments[0] : false); 144 | 145 | // hide others 146 | if (this.options.hideOthers) { 147 | Tooltips.hideAll(this); 148 | } 149 | 150 | this._tooltip.fadeTo( 151 | duration, 152 | 1, 153 | function () { 154 | if (callback) callback(); 155 | }.bind(this) 156 | ); 157 | }, 158 | 159 | hideDelayed: function () { 160 | // cancel show timer 161 | this.clearTimer("show"); 162 | 163 | // if there is a hide timer we don't have to start another one 164 | if (this.getTimer("hide") || !this.visible() || this.is("disabled")) return; 165 | 166 | // otherwise we start one 167 | this.setTimer( 168 | "hide", 169 | function () { 170 | this.clearTimer("hide"); 171 | this.hide(); 172 | }.bind(this), 173 | this.options.hideDelay || 1 // always at least some delay 174 | ); 175 | }, 176 | 177 | hide: function (instant, callback) { 178 | this.clearTimer("show"); 179 | if (!this.visible() || this.is("disabled")) return; 180 | 181 | this.is("visible", false); 182 | 183 | this.stop(); 184 | var shq = this.queues.showhide; 185 | 186 | // instantly cancel ajax/sanitize/refresh 187 | shq.queue( 188 | function (next_aborted) { 189 | this.abort(); 190 | next_aborted(); 191 | }.bind(this) 192 | ); 193 | 194 | // Fade-out 195 | shq.queue( 196 | function (next_fade_out) { 197 | this._hide(instant, next_fade_out); 198 | }.bind(this) 199 | ); 200 | 201 | // if all tooltips are hidden now we can reset Tooltips.zIndex.current 202 | shq.queue(function (next_resetZ) { 203 | Tooltips.resetZ(); 204 | next_resetZ(); 205 | }); 206 | 207 | // update on next open 208 | shq.queue( 209 | function (next_update_on_show) { 210 | this.clearUpdatedTo(); 211 | next_update_on_show(); 212 | }.bind(this) 213 | ); 214 | 215 | if (typeof this.options.afterHide === "function" && this.is("updated")) { 216 | shq.queue( 217 | function (next_afterhide) { 218 | this.options.afterHide(this._content[0], this.element); // TODO: update 219 | next_afterhide(); 220 | }.bind(this) 221 | ); 222 | } 223 | 224 | // if we have a non-caching ajax or function based tooltip, reset updated 225 | // after afterHide callback since it checks for this 226 | if (!this.options.cache && (this.options.ajax || this._fn)) { 227 | shq.queue( 228 | function (next_non_cached_reset) { 229 | this.is("updated", false); 230 | this.is("updating", false); 231 | this.is("sanitized", false); // sanitize again 232 | next_non_cached_reset(); 233 | }.bind(this) 234 | ); 235 | } 236 | 237 | // callback 238 | if (typeof callback === "function") { 239 | shq.queue(function (next_callback) { 240 | callback(); 241 | next_callback(); 242 | }); 243 | } 244 | 245 | // detach last 246 | shq.queue( 247 | function (next_detach) { 248 | this.detach(); 249 | next_detach(); 250 | }.bind(this) 251 | ); 252 | }, 253 | 254 | _hide: function (instant, callback) { 255 | callback = 256 | callback || (typeof arguments[0] === "function" ? arguments[0] : false); 257 | 258 | this.attach(); 259 | 260 | // we use fadeTo instead of fadeOut because it has some bugs with detached/reattached elements (jQuery) 261 | this._tooltip.fadeTo( 262 | instant ? 0 : this.options.fadeOut, 263 | 0, 264 | function () { 265 | // stop loading after a complete hide to make sure a loading icon 266 | // fades out without switching to content during a hide() 267 | this.stopLoading(); 268 | 269 | // the next show should resize to spinner 270 | // if it has to sanitize again 271 | // the logic behind that is handled in show() 272 | this.is("resize-to-content", false); 273 | 274 | // jQuerys fadein/out is bugged when working with elements that get detached elements 275 | // fading to 0 doesn't mean we hide at the end, so force that 276 | this._tooltip.hide(); 277 | 278 | if (callback) callback(); 279 | }.bind(this) 280 | ); 281 | }, 282 | 283 | toggle: function () { 284 | if (this.is("disabled")) return; 285 | this[this.visible() ? "hide" : "show"](); 286 | }, 287 | 288 | raise: function () { 289 | // if zIndex is set on the tooltip we don't raise it. 290 | if (!this.is("build") || this.options.zIndex) return; 291 | var highestTooltip = Tooltips.getHighestTooltip(); 292 | 293 | if ( 294 | highestTooltip && 295 | highestTooltip !== this && 296 | this.zIndex <= highestTooltip.zIndex 297 | ) { 298 | this.zIndex = highestTooltip.zIndex + 1; 299 | this._tooltip.css({ "z-index": this.zIndex }); 300 | 301 | if (this._tooltipShadow) { 302 | this._tooltipShadow.css({ "z-index": this.zIndex }); 303 | 304 | this.zIndex = highestTooltip.zIndex + 2; 305 | this._tooltip.css({ "z-index": this.zIndex }); 306 | } 307 | } 308 | }, 309 | }); 310 | -------------------------------------------------------------------------------- /src/js/tooltip/is.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | // states 3 | is: function (question, answer) { 4 | if (typeof answer === "boolean") { 5 | this._cache.is[question] = answer; 6 | } 7 | 8 | return this._cache.is[question]; 9 | }, 10 | 11 | visible: function () { 12 | return this.is("visible"); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/js/tooltip/timers.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | setTimer: function (name, handler, ms) { 3 | this._cache.timers[name] = _.delay(handler, ms); 4 | }, 5 | 6 | getTimer: function (name) { 7 | return this._cache.timers[name]; 8 | }, 9 | 10 | clearTimer: function (name) { 11 | if (this._cache.timers[name]) { 12 | clearTimeout(this._cache.timers[name]); 13 | delete this._cache.timers[name]; 14 | } 15 | }, 16 | 17 | clearTimers: function () { 18 | $.each(this._cache.timers, function (_i, timer) { 19 | clearTimeout(timer); 20 | }); 21 | this._cache.timers = {}; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/js/tooltip/update.js: -------------------------------------------------------------------------------- 1 | $.extend(Tooltip.prototype, { 2 | createElementMarker: function () { 3 | // marker for inline content 4 | if ( 5 | !this.elementMarker && 6 | this.content && 7 | _.element.isAttached(this.content) 8 | ) { 9 | // save the original display on the element 10 | $(this.content).data( 11 | "tpd-restore-inline-display", 12 | $(this.content).css("display") 13 | ); 14 | 15 | // put an inline marker before the element 16 | this.elementMarker = $("
").hide(); 17 | 18 | $(this.content).before($(this.elementMarker).hide()); 19 | } 20 | }, 21 | 22 | restoreElementToMarker: function () { 23 | var rid; 24 | 25 | if (this.elementMarker && this.content) { 26 | // restore old visibility 27 | if ((rid = $(this.content).data("tpd-restore-inline-display"))) { 28 | $(this.content).css({ display: rid }); 29 | } 30 | 31 | $(this.elementMarker).before(this.content).remove(); 32 | } 33 | }, 34 | 35 | startLoading: function () { 36 | if (this.is("loading")) return; 37 | 38 | // make sure the tooltip is build, otherwise there won't be a skin 39 | this.build(); 40 | 41 | // always set this flag 42 | this.is("loading", true); 43 | 44 | // can exit now if no spinner through options 45 | if (!this.options.spinner) return; 46 | 47 | this._tooltip.addClass("tpd-is-loading"); 48 | 49 | this.skin.startLoading(); 50 | 51 | // if we're showing for the first time, force show 52 | if (!this.is("resize-to-content")) { 53 | this.position(); 54 | this.raise(); 55 | this._show(); 56 | } 57 | }, 58 | 59 | stopLoading: function () { 60 | // make sure the tooltip is build, otherwise there won't be a skin 61 | this.build(); 62 | 63 | this.is("loading", false); 64 | 65 | if (!this.options.spinner) return; 66 | 67 | this._tooltip.removeClass("tpd-is-loading"); 68 | 69 | this.skin.stopLoading(); 70 | }, 71 | 72 | // abort 73 | abort: function () { 74 | this.abortAjax(); 75 | this.abortSanitize(); 76 | this.is("refreshed-before-sanitized", false); 77 | }, 78 | 79 | abortSanitize: function () { 80 | if (this._cache.voila) { 81 | this._cache.voila.abort(); 82 | this._cache.voila = null; 83 | } 84 | }, 85 | 86 | abortAjax: function () { 87 | if (this._cache.xhr) { 88 | this._cache.xhr.abort(); 89 | this._cache.xhr = null; 90 | this.is("updated", false); 91 | this.is("updating", false); 92 | } 93 | }, 94 | 95 | update: function (callback) { 96 | if (this.is("updating")) return; 97 | 98 | // mark as updating 99 | this.is("updating", true); 100 | 101 | this.build(); 102 | 103 | var type = this.options.inline 104 | ? "inline" 105 | : this.options.ajax 106 | ? "ajax" 107 | : _.isElement(this.content) || 108 | _.isText(this.content) || 109 | _.isDocumentFragment(this.content) 110 | ? "element" 111 | : this._fn 112 | ? "function" 113 | : "html"; 114 | 115 | // it could be that when we update the element that it gets so much content that it overlaps the current mouse position 116 | // for just a few ms, enough to trigger a mouseleave event. To work around this we hide the tooltip if it was visible. 117 | // hide the content container while updating, using visibility instead of display to work around 118 | // issues with scripts that depend on display 119 | this._contentWrapper.css({ visibility: "hidden" }); 120 | 121 | // from here we go into routes that should always return a prepared element to be inserted 122 | switch (type) { 123 | case "html": 124 | case "element": 125 | case "inline": 126 | // if we've already updated, just forward to the callback 127 | if (this.is("updated")) { 128 | if (callback) callback(); 129 | return; 130 | } 131 | 132 | this._update(this.content, callback); 133 | break; 134 | 135 | case "function": 136 | if (this.is("updated")) { 137 | if (callback) callback(); 138 | return; 139 | } 140 | 141 | var updateWith = this._fn(this.element); 142 | 143 | // if there's nothing to update with, abort 144 | if (!updateWith) { 145 | this.is("updating", false); 146 | if (callback) callback(true); // true means aborted in this case 147 | return; 148 | } 149 | 150 | this._update(updateWith, callback); 151 | break; 152 | 153 | case "ajax": 154 | var ajaxOptions = this.options.ajax || {}; 155 | 156 | var url = ajaxOptions.url || this.__content, 157 | data = ajaxOptions.data || {}, 158 | type = ajaxOptions.type || "GET", // jQuery default 159 | dataType = ajaxOptions.dataType; 160 | 161 | var initialOptions = { url: url, data: data }; 162 | if (type) $.extend(initialOptions, { type: type }); // keep jQuery initial type intact 163 | if (dataType) $.extend(initialOptions, { dataType: dataType }); // keep intelligent guess intact 164 | 165 | // merge initial options with given 166 | var options = $.extend({}, initialOptions, ajaxOptions); 167 | 168 | // remove method from the request, we want to use type only to support jQuery 1.9- 169 | if (options.method) { 170 | options = $.extend({}, options); 171 | delete options.method; 172 | } 173 | 174 | // make sure there are callbacks 175 | $.each( 176 | "complete error success".split(" "), 177 | function (i, cb) { 178 | if (!options[cb]) { 179 | if (cb === "success") { 180 | // when no success callback is given create a callback that sets 181 | // the responseText as content, otherwise we use the given one 182 | options[cb] = function (data, textStatus, jqXHR) { 183 | return jqXHR.responseText; 184 | }; 185 | } else { 186 | // for every other callback use an empty one 187 | options[cb] = function () {}; 188 | } 189 | } 190 | 191 | options[cb] = _.wrap( 192 | options[cb], 193 | function (proceed) { 194 | var args = _slice.call(arguments, 1), 195 | jqXHR = typeof args[0] === "object" ? args[0] : args[2]; // success callback has jqXHR as 3th arg, complete and error as 1st 196 | 197 | // don't store aborts 198 | if (jqXHR.statusText && jqXHR.statusText === "abort") return; 199 | 200 | // we should cache each individual callback here and make that fetchable 201 | if (this.options.cache) { 202 | AjaxCache.set( 203 | { 204 | url: options.url, 205 | type: options.type, 206 | data: options.data, 207 | }, 208 | cb, 209 | args 210 | ); 211 | } 212 | 213 | this._cache.xhr = null; 214 | 215 | // proceed is the callback at this point (complete/success/error) 216 | // we expect it's return value to hold the value to update the tooltip with 217 | var updateWith = proceed.apply(this, args); 218 | if (updateWith) { 219 | this._update(updateWith, callback); 220 | } 221 | }.bind(this) 222 | ); 223 | }.bind(this) 224 | ); 225 | 226 | // try cache first, for entries that have previously been successful 227 | var entry; 228 | if ( 229 | this.options.cache && 230 | (entry = AjaxCache.get(options)) && 231 | entry.callbacks.success 232 | ) { 233 | // if there is a cache, still call success and complete, but clear out the api 234 | $.each( 235 | entry.callbacks, 236 | function (cb, args) { 237 | if (typeof options[cb] === "function") { 238 | options[cb].apply(this, args); 239 | } 240 | }.bind(this) 241 | ); 242 | 243 | // stop here and avoid the request 244 | return; 245 | } 246 | 247 | // first check cache for possible update object and avoid load if we have one 248 | this.startLoading(); 249 | 250 | this._cache.xhr = $.ajax(options); 251 | 252 | break; 253 | } 254 | }, 255 | 256 | _update: function (content, callback) { 257 | // defaults 258 | var data = { 259 | title: this.options.title, 260 | close: this.options.close, 261 | }; 262 | 263 | if ( 264 | typeof content === "string" || 265 | _.isElement(content) || 266 | _.isText(content) || 267 | _.isDocumentFragment(content) || 268 | content instanceof $ 269 | ) { 270 | data.content = content; 271 | } else { 272 | $.extend(data, content); 273 | } 274 | 275 | var content = data.content, 276 | title = data.title, 277 | close = data.close; 278 | 279 | // store the new content, title and close so dimension/positioning functions can work with it 280 | this.content = content; 281 | this.title = title; 282 | this.close = close; 283 | 284 | // create a marker for when the content is an element attached to the DOM 285 | this.createElementMarker(); 286 | 287 | // make sure the content is visible 288 | if (_.isElement(content) || content instanceof $) { 289 | $(content).show(); 290 | } 291 | 292 | // append instantly 293 | this._content.html(this.content); 294 | 295 | this._title.html(title && typeof title === "string" ? title : ""); 296 | this._titleWrapper[title ? "show" : "hide"](); 297 | this._close[ 298 | (this.title || this.options.title) && close ? "show" : "hide" 299 | ](); 300 | 301 | var hasInnerClose = close && !(this.options.title || this.title), 302 | hasInnerCloseNonOverlap = 303 | close && !(this.options.title || this.title) && close !== "overlap", 304 | hasTitleCloseNonOverlap = 305 | close && (this.options.title || this.title) && close !== "overlap"; 306 | this._inner_close[hasInnerClose ? "show" : "hide"](); 307 | this._tooltip[(hasInnerCloseNonOverlap ? "add" : "remove") + "Class"]( 308 | "tpd-has-inner-close" 309 | ); 310 | this._tooltip[(hasTitleCloseNonOverlap ? "add" : "remove") + "Class"]( 311 | "tpd-has-title-close" 312 | ); 313 | 314 | // possible remove padding 315 | this._content[(this.options.padding ? "remove" : "add") + "Class"]( 316 | "tpd-content-no-padding" 317 | ); 318 | 319 | this.finishUpdate(callback); 320 | }, 321 | 322 | sanitize: function (callback) { 323 | // if the images loaded plugin isn't loaded, just callback 324 | if ( 325 | !this.options.voila || // also callback on manual disable 326 | this._content.find("img").length < 1 // or when no images need preloading 327 | ) { 328 | this.is("sanitized", true); 329 | if (callback) callback(); 330 | return; 331 | } 332 | 333 | // Voila uses img.complete and polling to detect if an image loaded 334 | // but if the src of an image is changed, complete will still be true 335 | // even as it's loading a new source. so we have to fallback to onload 336 | // to allow for src updates. 337 | this._cache.voila = Voila( 338 | this._content, 339 | { method: "onload" }, 340 | function (instance) { 341 | // mark images as sanitized so we can avoid sanitizing them again 342 | // for an instant refresh() later 343 | this._markImagesAsSanitized(instance.images); 344 | 345 | if (this.is("refreshed-before-sanitized")) { 346 | this.is("refreshed-before-sanitized", false); 347 | this.sanitize(callback); 348 | } else { 349 | // finish up 350 | this.is("sanitized", true); 351 | if (callback) callback(); 352 | } 353 | }.bind(this) 354 | ); 355 | }, 356 | 357 | // expects a voila.image instance 358 | _markImagesAsSanitized: function (images) { 359 | $.each(images, function (i, image) { 360 | var img = image.img; 361 | $(img).data("completed-src", image.img.src); 362 | }); 363 | }, 364 | 365 | _hasAllImagesSanitized: function () { 366 | var sanitizedAll = true; 367 | 368 | // as soon as we find one image that isn't sanitized 369 | // or sanitized based on the wrong source we 370 | // have to sanitize again 371 | this._content.find("img").each(function (_i, img) { 372 | var completedSrc = $(img).data("completed-src"); 373 | if (!(completedSrc && img.src === completedSrc)) { 374 | sanitizedAll = false; 375 | return false; 376 | } 377 | }); 378 | 379 | return sanitizedAll; 380 | }, 381 | 382 | refresh: function () { 383 | if (!this.visible()) return; 384 | 385 | // avoid refreshing while sanitize() still needs to finish up 386 | if (!this.is("sanitized")) { 387 | // mark the need to re-sanitize 388 | this.is("refreshed-before-sanitized", true); 389 | 390 | return; 391 | } 392 | 393 | // mark as refreshing 394 | this.is("refreshing", true); 395 | 396 | // clear potential timers 397 | this.clearTimer("refresh-spinner"); 398 | 399 | if ( 400 | !this.options.voila || 401 | this._content.find("img").length < 1 || 402 | this._hasAllImagesSanitized() 403 | ) { 404 | // still use should-update-dimensions because text could also have updated 405 | this.is("should-update-dimensions", true); 406 | 407 | this.position(); 408 | this.is("refreshing", false); 409 | } else { 410 | // mark as unsanitized so we sanitize again even after a hide 411 | this.is("sanitized", false); 412 | 413 | this._contentWrapper.css({ visibility: "hidden" }); 414 | 415 | this.startLoading(); 416 | 417 | this.sanitize( 418 | function () { 419 | this._contentWrapper.css({ visibility: "visible" }); 420 | 421 | this.stopLoading(); 422 | 423 | // set the update dimensions marker again since a position() call 424 | // on mousemove during refresh could have caused it to be unset 425 | this.is("should-update-dimensions", true); 426 | 427 | this.position(); 428 | this.is("refreshing", false); 429 | }.bind(this) 430 | ); 431 | } 432 | }, 433 | 434 | finishUpdate: function (callback) { 435 | this.is("updated", true); 436 | this.is("updating", false); 437 | 438 | if (typeof this.options.afterUpdate === "function") { 439 | // make sure visibility is visible during this 440 | var isHidden = this._contentWrapper.css("visibility"); 441 | if (isHidden) this._contentWrapper.css({ visibility: "visible" }); 442 | 443 | this.options.afterUpdate(this._content[0], this.element); 444 | 445 | if (isHidden) this._contentWrapper.css({ visibility: "hidden" }); 446 | } 447 | 448 | if (callback) callback(); 449 | }, 450 | }); 451 | -------------------------------------------------------------------------------- /src/js/tooltips.js: -------------------------------------------------------------------------------- 1 | var Tooltips = { 2 | tooltips: {}, 3 | 4 | options: { 5 | defaultSkin: "dark", 6 | startingZIndex: 999999, 7 | }, 8 | 9 | _emptyClickHandler: function () {}, 10 | 11 | init: function () { 12 | this.reset(); 13 | 14 | this._resizeHandler = this.onWindowResize.bind(this); 15 | $(window).bind("resize orientationchange", this._resizeHandler); 16 | 17 | if (Browser.MobileSafari) { 18 | $("body").bind("click", this._emptyClickHandler); 19 | } 20 | }, 21 | 22 | reset: function () { 23 | Tooltips.removeAll(); 24 | 25 | Delegations.removeAll(); 26 | 27 | if (this._resizeHandler) { 28 | $(window).unbind("resize orientationchange", this._resizeHandler); 29 | } 30 | 31 | if (Browser.MobileSafari) { 32 | $("body").unbind("click", this._emptyClickHandler); 33 | } 34 | }, 35 | 36 | onWindowResize: function () { 37 | if (this._resizeTimer) { 38 | window.clearTimeout(this._resizeTimer); 39 | this._resizeTimer = null; 40 | } 41 | 42 | this._resizeTimer = _.delay( 43 | function () { 44 | var visible = this.getVisible(); 45 | $.each(visible, function (i, tooltip) { 46 | tooltip.clearUpdatedTo(); 47 | 48 | tooltip.position(); 49 | }); 50 | }.bind(this), 51 | 15 52 | ); 53 | }, 54 | 55 | _getTooltips: function (element, noClosest) { 56 | var uids = [], 57 | tooltips = [], 58 | u; 59 | 60 | if (_.isElement(element)) { 61 | if ((u = $(element).data("tipped-uids"))) uids = uids.concat(u); 62 | } else { 63 | // selector 64 | $(element).each(function (i, el) { 65 | if ((u = $(el).data("tipped-uids"))) uids = uids.concat(u); 66 | }); 67 | } 68 | 69 | if (!uids[0] && !noClosest) { 70 | // find a uids string 71 | var closestTooltip = this.getTooltipByTooltipElement( 72 | $(element).closest(".tpd-tooltip")[0] 73 | ); 74 | if (closestTooltip && closestTooltip.element) { 75 | u = $(closestTooltip.element).data("tipped-uids") || []; 76 | if (u) uids = uids.concat(u); 77 | } 78 | } 79 | 80 | if (uids.length > 0) { 81 | $.each( 82 | uids, 83 | function (_i, uid) { 84 | var tooltip; 85 | if ((tooltip = this.tooltips[uid])) { 86 | tooltips.push(tooltip); 87 | } 88 | }.bind(this) 89 | ); 90 | } 91 | 92 | return tooltips; 93 | }, 94 | 95 | // Returns the element for which the tooltip was created when given a tooltip element or any element within that tooltip. 96 | findElement: function (element) { 97 | var tooltips = []; 98 | 99 | if (_.isElement(element)) { 100 | tooltips = this._getTooltips(element); 101 | } 102 | 103 | return tooltips[0] && tooltips[0].element; 104 | }, 105 | 106 | get: function (element) { 107 | var options = $.extend( 108 | { 109 | api: false, 110 | }, 111 | arguments[1] || {} 112 | ); 113 | 114 | var matched = []; 115 | if (_.isElement(element)) { 116 | matched = this._getTooltips(element); 117 | } else if (element instanceof $) { 118 | // when a jQuery object, search every element 119 | element.each( 120 | function (_i, el) { 121 | var tooltips = this._getTooltips(el, true); 122 | if (tooltips.length > 0) { 123 | matched = matched.concat(tooltips); 124 | } 125 | }.bind(this) 126 | ); 127 | } else if (typeof element === "string") { 128 | // selector 129 | $.each(this.tooltips, function (i, tooltip) { 130 | if (tooltip.element && $(tooltip.element).is(element)) { 131 | matched.push(tooltip); 132 | } 133 | }); 134 | } 135 | 136 | // if api is set we'll mark the given tooltips as using the API 137 | if (options.api) { 138 | $.each(matched, function (_i, tooltip) { 139 | tooltip.is("api", true); 140 | }); 141 | } 142 | 143 | return matched; 144 | }, 145 | 146 | getTooltipByTooltipElement: function (element) { 147 | if (!element) return null; 148 | var matched = null; 149 | $.each(this.tooltips, function (_i, tooltip) { 150 | if (tooltip.is("build") && tooltip._tooltip[0] === element) { 151 | matched = tooltip; 152 | } 153 | }); 154 | return matched; 155 | }, 156 | 157 | getBySelector: function (selector) { 158 | var matched = []; 159 | $.each(this.tooltips, function (_i, tooltip) { 160 | if (tooltip.element && $(tooltip.element).is(selector)) { 161 | matched.push(tooltip); 162 | } 163 | }); 164 | return matched; 165 | }, 166 | 167 | getNests: function () { 168 | var matched = []; 169 | $.each(this.tooltips, function (_i, tooltip) { 170 | if (tooltip.is("nest")) { 171 | // safe cause when a tooltip is a nest it's already build 172 | matched.push(tooltip); 173 | } 174 | }); 175 | return matched; 176 | }, 177 | 178 | show: function (selector) { 179 | $(this.get(selector)).each(function (_i, tooltip) { 180 | tooltip.show(false, true); // not instant, but without delay 181 | }); 182 | }, 183 | 184 | hide: function (selector) { 185 | $(this.get(selector)).each(function (_i, tooltip) { 186 | tooltip.hide(); 187 | }); 188 | }, 189 | 190 | toggle: function (selector) { 191 | $(this.get(selector)).each(function (_i, tooltip) { 192 | tooltip.toggle(); 193 | }); 194 | }, 195 | 196 | hideAll: function (but) { 197 | $.each(this.getVisible(), function (_i, tooltip) { 198 | if (but && but === tooltip) return; 199 | tooltip.hide(); 200 | }); 201 | }, 202 | 203 | refresh: function (selector) { 204 | // find only those tooltips that are visible 205 | var tooltips; 206 | if (selector) { 207 | // filter out only those visible 208 | tooltips = $.grep(this.get(selector), function (tooltip) { 209 | return tooltip.is("visible"); 210 | }); 211 | } else { 212 | // all visible tooltips 213 | tooltips = this.getVisible(); 214 | } 215 | 216 | $.each(tooltips, function (_i, tooltip) { 217 | tooltip.refresh(); 218 | }); 219 | }, 220 | 221 | getVisible: function () { 222 | var visible = []; 223 | $.each(this.tooltips, function (_i, tooltip) { 224 | if (tooltip.visible()) { 225 | visible.push(tooltip); 226 | } 227 | }); 228 | return visible; 229 | }, 230 | 231 | isVisibleByElement: function (element) { 232 | var visible = false; 233 | if (_.isElement(element)) { 234 | $.each(this.getVisible() || [], function (_i, tooltip) { 235 | if (tooltip.element === element) { 236 | visible = true; 237 | return false; 238 | } 239 | }); 240 | } 241 | return visible; 242 | }, 243 | 244 | getHighestTooltip: function () { 245 | var Z = 0, 246 | h; 247 | $.each(this.tooltips, function (_i, tooltip) { 248 | if (tooltip.zIndex > Z) { 249 | Z = tooltip.zIndex; 250 | h = tooltip; 251 | } 252 | }); 253 | return h; 254 | }, 255 | 256 | resetZ: function () { 257 | // the zIndex only has to be restore when there are no visible tooltip 258 | // use find to $break when a a visible tooltip is found 259 | if (this.getVisible().length <= 1) { 260 | $.each(this.tooltips, function (_i, tooltip) { 261 | // only reset on tooltip that don't have the zIndex option set 262 | if (tooltip.is("build") && !tooltip.options.zIndex) { 263 | tooltip._tooltip.css({ 264 | zIndex: (tooltip.zIndex = +Tooltips.options.startingZIndex), 265 | }); 266 | } 267 | }); 268 | } 269 | }, 270 | 271 | // AjaxCache 272 | clearAjaxCache: function () { 273 | // if there's an _cache.xhr running, abort it for all tooltips 274 | // set updated state to false for all 275 | $.each( 276 | this.tooltips, 277 | function (_i, tooltip) { 278 | if (tooltip.options.ajax) { 279 | // abort possible running request 280 | if (tooltip._cache && tooltip._cache.xhr) { 281 | tooltip._cache.xhr.abort(); 282 | tooltip._cache.xhr = null; 283 | } 284 | 285 | // reset state 286 | tooltip.is("updated", false); 287 | tooltip.is("updating", false); 288 | tooltip.is("sanitized", false); // sanitize again 289 | } 290 | }.bind(this) 291 | ); 292 | 293 | AjaxCache.clear(); 294 | }, 295 | 296 | add: function (tooltip) { 297 | this.tooltips[tooltip.uid] = tooltip; 298 | }, 299 | 300 | remove: function (element) { 301 | var tooltips = this._getTooltips(element); 302 | this.removeTooltips(tooltips); 303 | }, 304 | 305 | removeTooltips: function (tooltips) { 306 | if (!tooltips) return; 307 | 308 | $.each( 309 | tooltips, 310 | function (_i, tooltip) { 311 | var uid = tooltip.uid; 312 | 313 | delete this.tooltips[uid]; 314 | 315 | tooltip.remove(); // also removes uid from element 316 | }.bind(this) 317 | ); 318 | }, 319 | 320 | // remove all tooltips that are not attached to the DOM 321 | removeDetached: function () { 322 | // first find all nests 323 | var nests = this.getNests(), 324 | detached = []; 325 | if (nests.length > 0) { 326 | $.each(nests, function (_i, nest) { 327 | if (nest.is("detached")) { 328 | detached.push(nest); 329 | nest.attach(); 330 | } 331 | }); 332 | } 333 | 334 | $.each( 335 | this.tooltips, 336 | function (i, tooltip) { 337 | if (tooltip.element && !_.element.isAttached(tooltip.element)) { 338 | this.remove(tooltip.element); 339 | } 340 | }.bind(this) 341 | ); 342 | 343 | // restore previously detached nests 344 | // if they haven't been removed 345 | $.each(detached, function (_i, nest) { 346 | nest.detach(); 347 | }); 348 | }, 349 | 350 | removeAll: function () { 351 | $.each( 352 | this.tooltips, 353 | function (_i, tooltip) { 354 | if (tooltip.element) { 355 | this.remove(tooltip.element); 356 | } 357 | }.bind(this) 358 | ); 359 | this.tooltips = {}; 360 | }, 361 | 362 | setDefaultSkin: function (name) { 363 | this.options.defaultSkin = name || "dark"; 364 | }, 365 | 366 | setStartingZIndex: function (index) { 367 | this.options.startingZIndex = index || 0; 368 | }, 369 | }; 370 | 371 | // Extra position functions, used in Options 372 | Tooltips.Position = { 373 | inversedPosition: { 374 | left: "right", 375 | right: "left", 376 | top: "bottom", 377 | bottom: "top", 378 | middle: "middle", 379 | center: "center", 380 | }, 381 | 382 | getInversedPosition: function (position) { 383 | var positions = Position.split(position), 384 | left = positions[1], 385 | right = positions[2], 386 | orientation = Position.getOrientation(position), 387 | options = $.extend( 388 | { 389 | horizontal: true, 390 | vertical: true, 391 | }, 392 | arguments[1] || {} 393 | ); 394 | 395 | if (orientation === "horizontal") { 396 | if (options.vertical) { 397 | left = this.inversedPosition[left]; 398 | } 399 | if (options.horizontal) { 400 | right = this.inversedPosition[right]; 401 | } 402 | } else { 403 | // vertical 404 | if (options.vertical) { 405 | right = this.inversedPosition[right]; 406 | } 407 | if (options.horizontal) { 408 | left = this.inversedPosition[left]; 409 | } 410 | } 411 | 412 | return left + right; 413 | }, 414 | 415 | // what we do here is inverse topleft -> bottomleft instead of bottomright 416 | // and lefttop -> righttop instead of rightbottom 417 | getTooltipPositionFromTarget: function (position) { 418 | var positions = Position.split(position); 419 | return this.getInversedPosition( 420 | positions[1] + this.inversedPosition[positions[2]] 421 | ); 422 | }, 423 | }; 424 | -------------------------------------------------------------------------------- /src/js/umd-head.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Tipped - <%= pkg.description %> - v<%= pkg.version %> 3 | * (c) 2012-<%= grunt.template.today("yyyy") %> Nick Stakenburg 4 | * 5 | * https://github.com/staaky/tipped 6 | * 7 | * @license: https://creativecommons.org/licenses/by/4.0 8 | */ 9 | 10 | // UMD wrapper 11 | (function(root, factory) { 12 | if (typeof define === 'function' && define.amd) { 13 | // AMD. Register as an anonymous module. 14 | define(['jquery'], factory); 15 | } else if (typeof module === 'object' && module.exports) { 16 | // Node/CommonJS 17 | module.exports = factory(require('jquery')); 18 | } else { 19 | // Browser globals 20 | root.Tipped = factory(jQuery); 21 | } 22 | }(this, function($) { 23 | -------------------------------------------------------------------------------- /src/js/umd-tail.js: -------------------------------------------------------------------------------- 1 | Tipped.init(); 2 | 3 | return Tipped; 4 | 5 | })); -------------------------------------------------------------------------------- /src/js/voila/voila.custom.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Voilà - v1.3.0 3 | * (c) 2015 Nick Stakenburg 4 | * 5 | * https://github.com/staaky/voila 6 | * 7 | * MIT License 8 | */ 9 | 10 | var Voila = (function ($) { 11 | function Voila(elements, opts, cb) { 12 | if (!(this instanceof Voila)) { 13 | return new Voila(elements, opts, cb); 14 | } 15 | 16 | var argTypeOne = typeof arguments[1], 17 | options = argTypeOne === "object" ? arguments[1] : {}, 18 | callback = 19 | argTypeOne === "function" 20 | ? arguments[1] 21 | : typeof arguments[2] === "function" 22 | ? arguments[2] 23 | : false; 24 | 25 | this.options = $.extend( 26 | { 27 | method: "onload", 28 | }, 29 | options 30 | ); 31 | 32 | this.deferred = new jQuery.Deferred(); 33 | 34 | // if there's a callback, push it onto the stack 35 | if (callback) { 36 | this.always(callback); 37 | } 38 | 39 | this._processed = 0; 40 | this.images = []; 41 | this._add(elements); 42 | 43 | return this; 44 | } 45 | 46 | $.extend(Voila.prototype, { 47 | _add: function (elements) { 48 | // normalize to an array 49 | var array = 50 | typeof elements === "string" 51 | ? $(elements) // selector 52 | : elements instanceof jQuery || elements.length > 0 53 | ? elements // jQuery obj, Array 54 | : [elements]; // element node 55 | 56 | // subtract the images 57 | $.each( 58 | array, 59 | function (_i, element) { 60 | var images = $(), 61 | $element = $(element); 62 | 63 | // single image 64 | if ($element.is("img")) { 65 | images = images.add($element); 66 | } else { 67 | // nested 68 | images = images.add($element.find("img")); 69 | } 70 | 71 | images.each( 72 | function (i, element) { 73 | this.images.push( 74 | new ImageReady( 75 | element, 76 | // success 77 | function (image) { 78 | this._progress(image); 79 | }.bind(this), 80 | // error 81 | function (image) { 82 | this._progress(image); 83 | }.bind(this), 84 | // options 85 | this.options 86 | ) 87 | ); 88 | }.bind(this) 89 | ); 90 | }.bind(this) 91 | ); 92 | 93 | // no images found 94 | if (this.images.length < 1) { 95 | setTimeout( 96 | function () { 97 | this._resolve(); 98 | }.bind(this) 99 | ); 100 | } 101 | }, 102 | 103 | abort: function () { 104 | // clear callbacks 105 | this._progress = 106 | this._notify = 107 | this._reject = 108 | this._resolve = 109 | function () {}; 110 | 111 | // clear images 112 | $.each(this.images, function (_i, image) { 113 | image.abort(); 114 | }); 115 | this.images = []; 116 | }, 117 | 118 | _progress: function (image) { 119 | this._processed++; 120 | 121 | // when a broken image passes by keep track of it 122 | if (!image.isLoaded) this._broken = true; 123 | 124 | this._notify(image); 125 | 126 | // completed 127 | if (this._processed === this.images.length) { 128 | this[this._broken ? "_reject" : "_resolve"](); 129 | } 130 | }, 131 | 132 | _notify: function (image) { 133 | this.deferred.notify(this, image); 134 | }, 135 | _reject: function () { 136 | this.deferred.reject(this); 137 | }, 138 | _resolve: function () { 139 | this.deferred.resolve(this); 140 | }, 141 | 142 | always: function (callback) { 143 | this.deferred.always(callback); 144 | return this; 145 | }, 146 | 147 | done: function (callback) { 148 | this.deferred.done(callback); 149 | return this; 150 | }, 151 | 152 | fail: function (callback) { 153 | this.deferred.fail(callback); 154 | return this; 155 | }, 156 | 157 | progress: function (callback) { 158 | this.deferred.progress(callback); 159 | return this; 160 | }, 161 | }); 162 | 163 | /* ImageReady (standalone) - part of Voilà 164 | * http://voila.nickstakenburg.com 165 | * MIT License 166 | */ 167 | var ImageReady = (function ($) { 168 | var Poll = function () { 169 | return this.initialize.apply(this, Array.prototype.slice.call(arguments)); 170 | }; 171 | $.extend(Poll.prototype, { 172 | initialize: function () { 173 | this.options = $.extend( 174 | { 175 | test: function () {}, 176 | success: function () {}, 177 | timeout: function () {}, 178 | callAt: false, 179 | intervals: [ 180 | [0, 0], 181 | [1 * 1000, 10], 182 | [2 * 1000, 50], 183 | [4 * 1000, 100], 184 | [20 * 1000, 500], 185 | ], 186 | }, 187 | arguments[0] || {} 188 | ); 189 | 190 | this._test = this.options.test; 191 | this._success = this.options.success; 192 | this._timeout = this.options.timeout; 193 | 194 | this._ipos = 0; 195 | this._time = 0; 196 | this._delay = this.options.intervals[this._ipos][1]; 197 | this._callTimeouts = []; 198 | 199 | this.poll(); 200 | this._createCallsAt(); 201 | }, 202 | 203 | poll: function () { 204 | this._polling = setTimeout( 205 | function () { 206 | if (this._test()) { 207 | this.success(); 208 | return; 209 | } 210 | 211 | // update time 212 | this._time += this._delay; 213 | 214 | // next i within the interval 215 | if (this._time >= this.options.intervals[this._ipos][0]) { 216 | // timeout when no next interval 217 | if (!this.options.intervals[this._ipos + 1]) { 218 | if (typeof this._timeout === "function") { 219 | this._timeout(); 220 | } 221 | return; 222 | } 223 | 224 | this._ipos++; 225 | 226 | // update to the new bracket 227 | this._delay = this.options.intervals[this._ipos][1]; 228 | } 229 | 230 | this.poll(); 231 | }.bind(this), 232 | this._delay 233 | ); 234 | }, 235 | 236 | success: function () { 237 | this.abort(); 238 | this._success(); 239 | }, 240 | 241 | _createCallsAt: function () { 242 | if (!this.options.callAt) return; 243 | 244 | // start a timer for each call 245 | $.each( 246 | this.options.callAt, 247 | function (_i, at) { 248 | var time = at[0], 249 | fn = at[1]; 250 | 251 | var timeout = setTimeout( 252 | function () { 253 | fn(); 254 | }.bind(this), 255 | time 256 | ); 257 | 258 | this._callTimeouts.push(timeout); 259 | }.bind(this) 260 | ); 261 | }, 262 | 263 | _stopCallTimeouts: function () { 264 | $.each(this._callTimeouts, function (i, timeout) { 265 | clearTimeout(timeout); 266 | }); 267 | this._callTimeouts = []; 268 | }, 269 | 270 | abort: function () { 271 | this._stopCallTimeouts(); 272 | 273 | if (this._polling) { 274 | clearTimeout(this._polling); 275 | this._polling = null; 276 | } 277 | }, 278 | }); 279 | 280 | var ImageReady = function () { 281 | return this.initialize.apply(this, Array.prototype.slice.call(arguments)); 282 | }; 283 | $.extend(ImageReady.prototype, { 284 | supports: { 285 | naturalWidth: (function () { 286 | return "naturalWidth" in new Image(); 287 | })(), 288 | }, 289 | 290 | // NOTE: setTimeouts allow callbacks to be attached 291 | initialize: function (img, successCallback, errorCallback) { 292 | this.img = $(img)[0]; 293 | this.successCallback = successCallback; 294 | this.errorCallback = errorCallback; 295 | this.isLoaded = false; 296 | 297 | this.options = $.extend( 298 | { 299 | method: "onload", 300 | pollFallbackAfter: 1000, 301 | }, 302 | arguments[3] || {} 303 | ); 304 | 305 | // onload and a fallback for no naturalWidth support (IE6-7) 306 | if (this.options.method == "onload" || !this.supports.naturalWidth) { 307 | this.load(); 308 | return; 309 | } 310 | 311 | // start polling 312 | this.poll(); 313 | }, 314 | 315 | // NOTE: Polling for naturalWidth is only reliable if the 316 | // .src never changes. naturalWidth isn't always reset 317 | // to 0 after the src changes (depending on how the spec 318 | // was implemented). The spec even seems to be against 319 | // this, making polling unreliable in those cases. 320 | poll: function () { 321 | this._poll = new Poll({ 322 | test: function () { 323 | return this.img.naturalWidth > 0; 324 | }.bind(this), 325 | 326 | success: function () { 327 | this.success(); 328 | }.bind(this), 329 | 330 | timeout: function () { 331 | // error on timeout 332 | this.error(); 333 | }.bind(this), 334 | 335 | callAt: [ 336 | [ 337 | this.options.pollFallbackAfter, 338 | function () { 339 | this.load(); 340 | }.bind(this), 341 | ], 342 | ], 343 | }); 344 | }, 345 | 346 | load: function () { 347 | this._loading = setTimeout( 348 | function () { 349 | var image = new Image(); 350 | this._onloadImage = image; 351 | 352 | image.onload = function () { 353 | image.onload = function () {}; 354 | 355 | if (!this.supports.naturalWidth) { 356 | this.img.naturalWidth = image.width; 357 | this.img.naturalHeight = image.height; 358 | image.naturalWidth = image.width; 359 | image.naturalHeight = image.height; 360 | } 361 | 362 | this.success(); 363 | }.bind(this); 364 | 365 | image.onerror = this.error.bind(this); 366 | 367 | image.src = this.img.src; 368 | }.bind(this) 369 | ); 370 | }, 371 | 372 | success: function () { 373 | if (this._calledSuccess) return; 374 | 375 | this._calledSuccess = true; 376 | 377 | // stop loading/polling 378 | this.abort(); 379 | 380 | // some time to allow layout updates, IE requires this! 381 | this.waitForRender( 382 | function () { 383 | this.isLoaded = true; 384 | this.successCallback(this); 385 | }.bind(this) 386 | ); 387 | }, 388 | 389 | error: function () { 390 | if (this._calledError) return; 391 | 392 | this._calledError = true; 393 | 394 | // stop loading/polling 395 | this.abort(); 396 | 397 | // don't wait for an actual render on error, just timeout 398 | // to give the browser some time to render a broken image icon 399 | this._errorRenderTimeout = setTimeout( 400 | function () { 401 | if (this.errorCallback) this.errorCallback(this); 402 | }.bind(this) 403 | ); 404 | }, 405 | 406 | abort: function () { 407 | this.stopLoading(); 408 | this.stopPolling(); 409 | this.stopWaitingForRender(); 410 | }, 411 | 412 | stopPolling: function () { 413 | if (this._poll) { 414 | this._poll.abort(); 415 | this._poll = null; 416 | } 417 | }, 418 | 419 | stopLoading: function () { 420 | if (this._loading) { 421 | clearTimeout(this._loading); 422 | this._loading = null; 423 | } 424 | 425 | if (this._onloadImage) { 426 | this._onloadImage.onload = function () {}; 427 | this._onloadImage.onerror = function () {}; 428 | } 429 | }, 430 | 431 | // used by success() only 432 | waitForRender: function (callback) { 433 | this._renderTimeout = setTimeout(callback); 434 | }, 435 | 436 | stopWaitingForRender: function () { 437 | if (this._renderTimeout) { 438 | clearTimeout(this._renderTimeout); 439 | this._renderTimeout = null; 440 | } 441 | 442 | if (this._errorRenderTimeout) { 443 | clearTimeout(this._errorRenderTimeout); 444 | this._errorRenderTimeout = null; 445 | } 446 | }, 447 | }); 448 | 449 | return ImageReady; 450 | })(jQuery); 451 | 452 | return Voila; 453 | })(jQuery); 454 | --------------------------------------------------------------------------------