├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── component.json ├── index.js ├── package.json ├── template.html ├── template.js ├── test ├── auto.html └── index.html └── tip.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/*.js 3 | test/*.css 4 | components 5 | build 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.0.1 / 2015-02-12 3 | ================== 4 | 5 | * remove mouseover event listeners on hide 6 | 7 | 1.0.0 / 2014-01-17 8 | ================== 9 | 10 | * change direction keywords 11 | 12 | 0.3.0 / 2013-10-24 13 | ================== 14 | 15 | * undo 'fix' for cancelOnHide 16 | * removed jquery dep 17 | 18 | 0.2.1 / 2013-05-27 19 | ================== 20 | 21 | * pin deps 22 | * change default position back to above the element 23 | * remove silly inherit dep 24 | * fix for offset bug on 100% pages 25 | 26 | 0.2.0 / 2013-03-25 27 | ================== 28 | 29 | * add .position() .auto option to completely disable auto positioning 30 | 31 | 0.1.5 / 2013-03-05 32 | ================== 33 | 34 | * add explicit Tip#message() call 35 | 36 | 0.1.4 / 2013-02-28 37 | ================== 38 | 39 | * rename .content() to .message() for inheritance bullshit 40 | 41 | 0.1.3 / 2013-02-28 42 | ================== 43 | 44 | * fix backwards positioning 45 | 46 | 0.1.2 / 2013-02-21 47 | ================== 48 | 49 | * add inherit dependency 50 | 51 | 0.1.1 / 2012-12-18 52 | ================== 53 | 54 | * fix .position(), replace class immediately 55 | 56 | 0.1.0 / 2012-12-03 57 | ================== 58 | 59 | * add absolute positioning support via .show(x, y) 60 | * add Tip#cancelHideOnHover() 61 | 62 | 0.0.5 / 2012-08-31 63 | ================== 64 | 65 | * fix hiding of tip when hover back over the target 66 | 67 | 0.0.4 / 2012-08-22 68 | ================== 69 | 70 | * fix unnecessary applying of content on .show() [guille] 71 | 72 | 0.0.3 / 2012-08-22 73 | ================== 74 | 75 | * add `Tip#attach(el, [delay])` 76 | * add `.value` option 77 | * change `Tip#cancelHideOnHover()` to be public 78 | * fix npm template.js usage 79 | 80 | 0.0.2 / 2012-08-22 81 | ================== 82 | 83 | * add `Tip#cancelHideOnHover()` 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: tip.css index.js template.js components 3 | @component build --dev 4 | 5 | template.js: template.html 6 | @component convert $< 7 | 8 | components: component.json 9 | @component install --dev 10 | 11 | clean: 12 | rm -fr build components 13 | 14 | test: build 15 | @open test/index.html 16 | 17 | .PHONY: clean test 18 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Tip 2 | 3 | Tip component. Inspired by [tipsy](https://github.com/jaz303/tipsy) without the weird jQuery 4 | API. 5 | 6 | ![js tip component](http://f.cl.ly/items/2H1D232Y0g1T3g1G0l3s/Screen%20Shot%202012-08-02%20at%202.31.50%20PM.png) 7 | ![js tip with markup](http://f.cl.ly/items/2h1F2B1P1C3M0g0a0M0n/Screen%20Shot%202012-08-02%20at%203.34.06%20PM.png) 8 | 9 | ![js maru](http://f.cl.ly/items/1I2V2o0q3M2p1E2H183w/Screen%20Shot%202012-08-02%20at%206.48.28%20PM.png) 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm install tip-component 15 | ``` 16 | 17 | ## Features 18 | 19 | - events for composition 20 | - "auto" positioning on window resize / scroll 21 | - fluent API 22 | 23 | ## Events 24 | 25 | - `show` the tip is shown 26 | - `hide` the tip is hidden 27 | 28 | ## API 29 | 30 | ### Tip(el, string) 31 | 32 | Equivalent to `Tip(el, { value: string })`. 33 | 34 | ### Tip(el, [options]) 35 | 36 | Attach a `Tip` to an element, and display the `title` 37 | attribute's contents on hover. Optionally apply a hide `delay` 38 | in milliseconds. 39 | 40 | ```js 41 | var tip = require('tip'); 42 | tip('a[title]', { delay: 300 }); 43 | ``` 44 | 45 | ### new Tip(content) 46 | 47 | Create a new tip with `content` being 48 | either a string, html, element, etc. 49 | 50 | ```js 51 | var Tip = require('tip'); 52 | var tip = new Tip('Hello!'); 53 | tip.show('#mylink'); 54 | ``` 55 | 56 | ### Tip#position(type, [options]) 57 | 58 | - `top` 59 | - `top right` 60 | - `top left` 61 | - `bottom` 62 | - `bottom right` 63 | - `bottom left` 64 | - `right` 65 | - `left` 66 | 67 | Options: 68 | 69 | - `auto` set to __false__ to disable auto-positioning 70 | 71 | ### Tip#show(el) 72 | 73 | Show the tip attached to `el`, where `el` 74 | may be a selector or element. 75 | 76 | ### Tip#show(x, y) 77 | 78 | Show the tip at the absolute position `(x, y)`. 79 | 80 | ### Tip#hide([ms]) 81 | 82 | Hide the tip immediately or wait `ms`. 83 | 84 | ### Tip#attach(el) 85 | 86 | Attach the tip to the given `el`, showing on `mouseover` and hiding on `mouseout`. 87 | 88 | ### Tip#effect(name) 89 | 90 | Use effect `name`. Default with `Tip.effect = 'fade'` for example. 91 | 92 | ### Themes 93 | 94 | - [Aurora](https://github.com/component/aurora-tip) 95 | - [Nightrider](https://github.com/jb55/nightrider-tip) 96 | 97 | ## License 98 | 99 | MIT 100 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tip", 3 | "description": "Tip component", 4 | "version": "1.1.3", 5 | "keywords": [ 6 | "tooltip", 7 | "tip", 8 | "ui" 9 | ], 10 | "dependencies": { 11 | "component/bind": "*", 12 | "component/emitter": "1.1.0", 13 | "component/query": "0.0.1", 14 | "component/events": "1.0.4", 15 | "component/domify": "1.0.0", 16 | "component/classes": "1.1.2", 17 | "component/css": "0.0.4", 18 | "component/raf": "*", 19 | "yields/after-transition": "*", 20 | "timoxley/offset": "0.0.2" 21 | }, 22 | "development": { 23 | "component/aurora-tip": "*", 24 | "component/jquery": "*" 25 | }, 26 | "scripts": [ 27 | "index.js" 28 | ], 29 | "templates": [ 30 | "template.html" 31 | ], 32 | "styles": [ 33 | "tip.css" 34 | ], 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var bind = require('bind'); 6 | var Emitter = require('emitter'); 7 | var events = require('events'); 8 | var query = require('query'); 9 | var domify = require('domify'); 10 | var classes = require('classes'); 11 | var css = require('css'); 12 | var html = domify(require('./template.html')); 13 | var offset = require('offset'); 14 | var raf = require('raf'); 15 | var after = require('after-transition'); 16 | 17 | /** 18 | * Expose `Tip`. 19 | */ 20 | 21 | module.exports = Tip; 22 | 23 | /** 24 | * Apply the average use-case of simply 25 | * showing a tool-tip on `el` hover. 26 | * 27 | * Options: 28 | * 29 | * - `delay` hide delay in milliseconds [0] 30 | * - `value` defaulting to the element's title attribute 31 | * 32 | * @param {Mixed} elem 33 | * @param {Object|String} options or value 34 | * @api public 35 | */ 36 | 37 | function tip(elem, options) { 38 | if ('string' == typeof options) options = { value : options }; 39 | var els = ('string' == typeof elem) ? query.all(elem) : [elem]; 40 | for(var i = 0, el; el = els[i]; i++) { 41 | var val = options.value || el.getAttribute('title'); 42 | var tip = new Tip(val, options); 43 | el.setAttribute('title', ''); 44 | tip.cancelHideOnHover(); 45 | tip.attach(el); 46 | } 47 | } 48 | 49 | /** 50 | * Initialize a `Tip` with the given `content`. 51 | * 52 | * @param {Mixed} content 53 | * @api public 54 | */ 55 | 56 | function Tip(content, options) { 57 | options = options || {}; 58 | if (!(this instanceof Tip)) return tip(content, options); 59 | Emitter.call(this); 60 | this.classname = ''; 61 | this.delay = options.delay || 300; 62 | this.el = html.cloneNode(true); 63 | this.events = events(this.el, this); 64 | this.winEvents = events(window, this); 65 | this.classes = classes(this.el); 66 | this.inner = query('.tip-inner', this.el); 67 | this.message(content); 68 | this.position('top'); 69 | if (Tip.effect) this.effect(Tip.effect); 70 | } 71 | 72 | /** 73 | * Mixin emitter. 74 | */ 75 | 76 | Emitter(Tip.prototype); 77 | 78 | /** 79 | * Set tip `content`. 80 | * 81 | * @param {String|jQuery|Element} content 82 | * @return {Tip} self 83 | * @api public 84 | */ 85 | 86 | Tip.prototype.message = function(content){ 87 | this.inner.innerHTML = content; 88 | return this; 89 | }; 90 | 91 | /** 92 | * Attach to the given `el` with optional hide `delay`. 93 | * 94 | * @param {Element} el 95 | * @param {Number} delay 96 | * @return {Tip} 97 | * @api public 98 | */ 99 | 100 | Tip.prototype.attach = function(el){ 101 | var self = this; 102 | this.target = el; 103 | this.handleEvents = events(el, this); 104 | this.handleEvents.bind('mouseover'); 105 | this.handleEvents.bind('mouseout'); 106 | return this; 107 | }; 108 | 109 | /** 110 | * On mouse over 111 | * 112 | * @param {Event} e 113 | * @return {Tip} 114 | * @api private 115 | */ 116 | 117 | Tip.prototype.onmouseover = function() { 118 | this.show(this.target); 119 | this.cancelHide(); 120 | }; 121 | 122 | /** 123 | * On mouse out 124 | * 125 | * @param {Event} e 126 | * @return {Tip} 127 | * @api private 128 | */ 129 | 130 | Tip.prototype.onmouseout = function() { 131 | this.hide(this.delay); 132 | }; 133 | 134 | /** 135 | * Cancel hide on hover, hide with the given `delay`. 136 | * 137 | * @param {Number} delay 138 | * @return {Tip} 139 | * @api public 140 | */ 141 | 142 | Tip.prototype.cancelHideOnHover = function(){ 143 | this.events.bind('mouseover', 'cancelHide'); 144 | this.events.bind('mouseout', 'hide'); 145 | return this; 146 | }; 147 | 148 | /** 149 | * Set the effect to `type`. 150 | * 151 | * @param {String} type 152 | * @return {Tip} 153 | * @api public 154 | */ 155 | 156 | Tip.prototype.effect = function(type){ 157 | this._effect = type; 158 | this.classes.add(type); 159 | return this; 160 | }; 161 | 162 | /** 163 | * Set position: 164 | * 165 | * - `top` 166 | * - `top left` 167 | * - `top right` 168 | * - `bottom` 169 | * - `bottom left` 170 | * - `bottom right` 171 | * - `left` 172 | * - `right` 173 | * 174 | * @param {String} pos 175 | * @param {Object} options 176 | * @return {Tip} 177 | * @api public 178 | */ 179 | 180 | Tip.prototype.position = function(pos, options){ 181 | options = options || {}; 182 | this._position = pos; 183 | this._auto = false != options.auto; 184 | this.replaceClass(pos); 185 | this.emit('reposition'); 186 | return this; 187 | }; 188 | 189 | /** 190 | * Show the tip attached to `el`. 191 | * 192 | * Emits "show" (el) event. 193 | * 194 | * @param {String|Element|Number} el or x 195 | * @param {Number} [y] 196 | * @return {Tip} 197 | * @api public 198 | */ 199 | 200 | Tip.prototype.show = function(el){ 201 | var self = this; 202 | if ('string' == typeof el) el = query(el); 203 | 204 | // show it 205 | this.target = el; 206 | document.body.appendChild(this.el); 207 | this.classes.add('tip-' + this._position.replace(/\s+/g, '-')); 208 | 209 | raf(function(){ 210 | self.classes.remove('tip-hide'); 211 | }); 212 | 213 | // x,y 214 | if ('number' == typeof el) { 215 | var x = arguments[0]; 216 | var y = arguments[1]; 217 | this.emit('show'); 218 | css(this.el, { 219 | top: y, 220 | left: x 221 | }); 222 | return this; 223 | } 224 | 225 | // el 226 | this.reposition(); 227 | this.el.offsetHeight; 228 | this.emit('show', this.target); 229 | 230 | this.winEvents.bind('resize', 'reposition'); 231 | this.winEvents.bind('scroll', 'reposition'); 232 | 233 | return this; 234 | }; 235 | 236 | /** 237 | * Reposition the tip if necessary. 238 | * 239 | * @api private 240 | */ 241 | 242 | Tip.prototype.reposition = function(){ 243 | var pos = this._position; 244 | var off = this.offset(pos); 245 | var newpos = this._auto && this.suggested(pos, off); 246 | if (newpos) off = this.offset(pos = newpos); 247 | this.replaceClass(pos); 248 | this.emit('reposition'); 249 | css(this.el, off); 250 | }; 251 | 252 | /** 253 | * Compute the "suggested" position favouring `pos`. 254 | * Returns undefined if no suggestion is made. 255 | * 256 | * @param {String} pos 257 | * @param {Object} offset 258 | * @return {String} 259 | * @api private 260 | */ 261 | 262 | Tip.prototype.suggested = function(pos, off){ 263 | var el = this.el; 264 | 265 | var ew = el.clientWidth; 266 | var eh = el.clientHeight; 267 | var top = window.scrollY; 268 | var left = window.scrollX; 269 | var w = window.innerWidth; 270 | var h = window.innerHeight; 271 | 272 | // too low 273 | if (off.top + eh > top + h) return 'top'; 274 | 275 | // too high 276 | if (off.top < top) return 'bottom'; 277 | 278 | // too far to the right 279 | if (off.left + ew > left + w) return 'left'; 280 | 281 | // too far to the left 282 | if (off.left < left) return 'right'; 283 | }; 284 | 285 | /** 286 | * Replace position class `name`. 287 | * 288 | * @param {String} name 289 | * @api private 290 | */ 291 | 292 | Tip.prototype.replaceClass = function(name){ 293 | this.classes 294 | .remove('tip-' + this._position.split(' ').join('-')) 295 | .add('tip-' + name.split(' ').join('-')); 296 | }; 297 | 298 | /** 299 | * Compute the offset for `.target` 300 | * based on the given `pos`. 301 | * 302 | * @param {String} pos 303 | * @return {Object} 304 | * @api private 305 | */ 306 | 307 | Tip.prototype.offset = function(pos){ 308 | var pad = 15; 309 | var el = this.el; 310 | var target = this.target; 311 | 312 | var ew = el.clientWidth; 313 | var eh = el.clientHeight; 314 | 315 | var to = offset(target); 316 | var tw = target.clientWidth; 317 | var th = target.clientHeight; 318 | 319 | switch (pos) { 320 | case 'top': 321 | return { 322 | top: to.top - eh, 323 | left: to.left + tw / 2 - ew / 2 324 | } 325 | case 'bottom': 326 | return { 327 | top: to.top + th, 328 | left: to.left + tw / 2 - ew / 2 329 | } 330 | case 'right': 331 | return { 332 | top: to.top + th / 2 - eh / 2, 333 | left: to.left + tw 334 | } 335 | case 'left': 336 | return { 337 | top: to.top + th / 2 - eh / 2, 338 | left: to.left - ew 339 | } 340 | case 'top left': 341 | return { 342 | top: to.top - eh, 343 | left: to.left + tw / 2 - ew + pad 344 | } 345 | case 'top right': 346 | return { 347 | top: to.top - eh, 348 | left: to.left + tw / 2 - pad 349 | } 350 | case 'bottom left': 351 | return { 352 | top: to.top + th, 353 | left: to.left + tw / 2 - ew + pad 354 | } 355 | case 'bottom right': 356 | return { 357 | top: to.top + th, 358 | left: to.left + tw / 2 - pad 359 | } 360 | default: 361 | throw new Error('invalid position "' + pos + '"'); 362 | } 363 | }; 364 | 365 | /** 366 | * Cancel the `.hide()` timeout. 367 | * 368 | * @api private 369 | */ 370 | 371 | Tip.prototype.cancelHide = function(){ 372 | clearTimeout(this._hide); 373 | }; 374 | 375 | /** 376 | * Hide the tip with optional `ms` delay. 377 | * 378 | * Emits "hide" event. 379 | * 380 | * @param {Number} ms 381 | * @return {Tip} 382 | * @api public 383 | */ 384 | 385 | Tip.prototype.hide = function(ms){ 386 | var self = this; 387 | 388 | // duration 389 | if (ms) { 390 | this._hide = setTimeout(bind(this, this.hide), ms); 391 | return this; 392 | } 393 | 394 | // hide 395 | raf(function(){ 396 | after.once(self.el, function(){ 397 | self.remove(); 398 | }); 399 | self.classes.add('tip-hide'); 400 | }); 401 | 402 | return this; 403 | }; 404 | 405 | /** 406 | * Hide the tip without potential animation. 407 | * 408 | * @return {Tip} 409 | * @api 410 | */ 411 | 412 | Tip.prototype.remove = function(){ 413 | this.winEvents.unbind('resize', 'reposition'); 414 | this.winEvents.unbind('scroll', 'reposition'); 415 | this.emit('hide'); 416 | 417 | var parent = this.el.parentNode; 418 | if (parent) parent.removeChild(this.el); 419 | return this; 420 | }; 421 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tip-component", 3 | "description": "Tip component", 4 | "version": "1.1.3", 5 | "keywords": [ 6 | "tip", 7 | "component" 8 | ], 9 | "dependencies": { 10 | "emitter-component": "1.0.0", 11 | "jquery-component": "*" 12 | }, 13 | "component": { 14 | "styles": [ 15 | "tip.css" 16 | ], 17 | "scripts": { 18 | "tip/index.js": "index.js", 19 | "tip/template.js": "template.js" 20 | } 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/component/tip.git" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /template.js: -------------------------------------------------------------------------------- 1 | module.exports = '
\n
\n
\n
'; -------------------------------------------------------------------------------- /test/auto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tip 5 | 6 | 7 | 32 | 33 | 34 | Tip 35 | 36 | 39 | 48 | 49 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tip 5 | 6 | 7 | 51 | 52 | 53 | Tip 54 | 55 | 74 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /tip.css: -------------------------------------------------------------------------------- 1 | .tip { 2 | position: absolute; 3 | padding: 5px; 4 | z-index: 1000; 5 | /* default offset for edge-cases: https://github.com/component/tip/pull/12 */ 6 | top: 0; 7 | left: 0; 8 | } 9 | 10 | /* effects */ 11 | 12 | .tip.fade { 13 | transition: opacity 100ms; 14 | -moz-transition: opacity 100ms; 15 | -webkit-transition: opacity 100ms; 16 | } 17 | 18 | .tip-hide { 19 | opacity: 0; 20 | } 21 | --------------------------------------------------------------------------------