├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── component.json ├── index.js ├── package.json ├── template.html ├── test ├── auto.html └── index.html └── tip.css /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /components 3 | /build 4 | 5 | test/*.js 6 | test/*.css 7 | 8 | # root level development temp files 9 | /?.html 10 | /?.js 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 3.0.3 / 2016-04-01 3 | ================== 4 | 5 | * component, package: bump component-events dependency 6 | * index: fall back to require('component-*') for node compat 7 | 8 | 3.0.2 / 2016-04-01 9 | ================== 10 | 11 | * component, package: bump component-classes and -css dependencies 12 | 13 | 3.0.1 / 2016-03-29 14 | ================== 15 | 16 | * prevent the auto-picker from picking invalid combos (#57, @blowery) 17 | 18 | 3.0.0 / 2016-03-25 19 | ================== 20 | 21 | * test: fix "maru" image link 22 | * use `component-raf` dep 23 | * fix package, remove viewport from component 24 | * replace viewport with a custom listener and raf events 25 | * Constrain the tip to the screen left and right, as best as possible (#56) 26 | * improve positioning (#53) 27 | 28 | 2.5.0 / 2015-08-04 29 | ================== 30 | 31 | * core: add `static` support 32 | 33 | 2.4.1 / 2015-02-26 34 | ================== 35 | 36 | * update "bounding-client-rect" to v1.0.5 37 | 38 | 2.4.0 / 2015-01-28 39 | ================== 40 | 41 | * index: add support for `pad` option 42 | 43 | 2.3.4 / 2015-01-20 44 | ================== 45 | 46 | * index: fix "bottom right" (bi-directional) position case in `suggested()` 47 | 48 | 2.3.3 / 2015-01-20 49 | ================== 50 | 51 | * package: update "bounding-client-rect" to v1.0.4 52 | 53 | 2.3.2 / 2015-01-14 54 | ================== 55 | 56 | * use an elimination strategy for the `suggested()` function (#50, @TooTallNate) 57 | 58 | 2.3.1 / 2014-12-17 59 | ================== 60 | 61 | * update "bounding-client-rect" to v1.0.2 62 | 63 | 2.3.0 / 2014-12-16 64 | ================== 65 | 66 | * use "bounding-client-rect" module (#49, @TooTallNate) 67 | * index: rearrange require calls 68 | 69 | 2.2.0 / 2014-12-11 70 | ================== 71 | 72 | * update "document-offset" to v1.0.3 73 | * accept elements and html string for content (#45, @bmcmahen) 74 | * add "hiding" event 75 | 76 | 2.1.2 / 2014-08-01 77 | ================== 78 | 79 | * component, package: update "events" to v1.0.9 80 | 81 | 2.1.1 / 2014-07-14 82 | ================== 83 | 84 | * package: use "document-offset" module 85 | * component: update deps 86 | * package: update deps 87 | 88 | 2.1.0 / 2014-06-21 89 | ================== 90 | 91 | * index: use dimensions() to get the `target` dims 92 | * index: add `dimensions()` calculation function 93 | * index: fix @api JSDoc for remove() 94 | * test: remove redundant `` element 95 | * package: update "timoxley-offset" to v1.0.1 96 | 97 | 2.0.0 / 2014-05-27 98 | ================== 99 | 100 | * gitignore: ignore root level development temp files 101 | * component, package: update "css" and "events" 102 | * package: initial browserify support 103 | * package: make dependencies be equivalent to component.json 104 | * package: update name to "component-tip" 105 | * Readme: update Installation section 106 | * component: add "browser" keyword 107 | 108 | 1.0.3 / 2014-04-08 109 | ================== 110 | 111 | * improve binding of 'scroll' and 'resize' handlers 112 | 113 | 1.0.2 / 2014-04-08 114 | ================== 115 | 116 | * fix Tip position for elements with borders 117 | * add live demo links 118 | * remove jquery from examples 119 | * remove component-convert step 120 | * update pinned dependencies 121 | 122 | 1.0.1 / 2014-04-07 123 | ================== 124 | 125 | * update `component/events` 126 | * instantiate Tip with options 127 | * pin `component/css@0.0.4` 128 | * add attach to API documentation 129 | * fix IE8 compatibility 130 | 131 | 1.0.0 / 2014-01-17 132 | ================== 133 | 134 | * change direction keywords 135 | 136 | 0.3.1 / 2014-01-02 137 | ================== 138 | 139 | * emit `reposition` event 140 | * fix setting `undefined` css classname 141 | * update emitter 142 | 143 | 0.3.0 / 2013-10-24 144 | ================== 145 | 146 | * undo 'fix' for cancelOnHide 147 | * removed jquery dep 148 | 149 | 0.2.1 / 2013-05-27 150 | ================== 151 | 152 | * pin deps 153 | * change default position back to above the element 154 | * remove silly inherit dep 155 | * fix for offset bug on 100% pages 156 | 157 | 0.2.0 / 2013-03-25 158 | ================== 159 | 160 | * add .position() .auto option to completely disable auto positioning 161 | 162 | 0.1.5 / 2013-03-05 163 | ================== 164 | 165 | * add explicit Tip#message() call 166 | 167 | 0.1.4 / 2013-02-28 168 | ================== 169 | 170 | * rename .content() to .message() for inheritance bullshit 171 | 172 | 0.1.3 / 2013-02-28 173 | ================== 174 | 175 | * fix backwards positioning 176 | 177 | 0.1.2 / 2013-02-21 178 | ================== 179 | 180 | * add inherit dependency 181 | 182 | 0.1.1 / 2012-12-18 183 | ================== 184 | 185 | * fix .position(), replace class immediately 186 | 187 | 0.1.0 / 2012-12-03 188 | ================== 189 | 190 | * add absolute positioning support via .show(x, y) 191 | * add Tip#cancelHideOnHover() 192 | 193 | 0.0.5 / 2012-08-31 194 | ================== 195 | 196 | * fix hiding of tip when hover back over the target 197 | 198 | 0.0.4 / 2012-08-22 199 | ================== 200 | 201 | * fix unnecessary applying of content on .show() [guille] 202 | 203 | 0.0.3 / 2012-08-22 204 | ================== 205 | 206 | * add `Tip#attach(el, [delay])` 207 | * add `.value` option 208 | * change `Tip#cancelHideOnHover()` to be public 209 | * fix npm template.js usage 210 | 211 | 0.0.2 / 2012-08-22 212 | ================== 213 | 214 | * add `Tip#cancelHideOnHover()` 215 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: tip.css index.js template.html components 3 | @component build --dev 4 | 5 | components: component.json 6 | @component install --dev 7 | 8 | clean: 9 | rm -fr build components 10 | 11 | test: build 12 | @open test/index.html 13 | 14 | .PHONY: clean test 15 | -------------------------------------------------------------------------------- /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 | Live demo is [here](http://component.github.io/tip/). 12 | 13 | ## Installation 14 | 15 | ``` bash 16 | $ npm install component-tip 17 | ``` 18 | 19 | ## Features 20 | 21 | - events for composition 22 | - "auto" positioning on window resize / scroll 23 | - fluent API 24 | 25 | ## Events 26 | 27 | - `show` the tip is shown 28 | - `hide` the tip is hidden 29 | 30 | ## API 31 | 32 | ### Tip(el, string) 33 | 34 | Equivalent to `Tip(el, { value: string })`. 35 | 36 | ### Tip(el, [options]) 37 | 38 | Attach a `Tip` to an element, and display the `title` 39 | attribute's contents on hover. Optionally apply a hide `delay` 40 | in milliseconds. 41 | Also if `static` is true the tip will be fixed to its initial position. 42 | 43 | ```js 44 | var tip = require('tip'); 45 | tip('a[title]', { delay: 300 }); 46 | ``` 47 | 48 | ### new Tip(content, [options]) 49 | 50 | Create a new tip with `content` being 51 | either a string, html, element, etc. 52 | 53 | ```js 54 | var Tip = require('tip'); 55 | var tip = new Tip('Hello!'); 56 | tip.show('#mylink'); 57 | ``` 58 | 59 | ### Tip#position(type, [options]) 60 | 61 | - `top` 62 | - `top right` 63 | - `top left` 64 | - `bottom` 65 | - `bottom right` 66 | - `bottom left` 67 | - `right` 68 | - `left` 69 | 70 | Options: 71 | 72 | - `auto` set to __false__ to disable auto-positioning 73 | 74 | ### Tip#show(el) 75 | 76 | Show the tip attached to `el`, where `el` 77 | may be a selector or element. 78 | 79 | ### Tip#show(x, y) 80 | 81 | Show the tip at the absolute position `(x, y)`. 82 | 83 | ### Tip#hide([ms]) 84 | 85 | Hide the tip immediately or wait `ms`. 86 | 87 | ### Tip#attach(el) 88 | 89 | Attach the tip to the given `el`, showing on `mouseover` and hiding on `mouseout`. 90 | 91 | ### Tip#effect(name) 92 | 93 | Use effect `name`. Default with `Tip.effect = 'fade'` for example. 94 | 95 | ### Themes 96 | 97 | - [Aurora](https://github.com/component/aurora-tip) 98 | - [Nightrider](https://github.com/jb55/nightrider-tip) 99 | 100 | ## License 101 | 102 | MIT 103 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tip", 3 | "description": "Tip component", 4 | "version": "3.0.3", 5 | "keywords": [ 6 | "browser", 7 | "component", 8 | "tooltip", 9 | "tip", 10 | "ui" 11 | ], 12 | "dependencies": { 13 | "component/bind": "*", 14 | "component/emitter": "1.1.3", 15 | "component/query": "0.0.3", 16 | "component/events": "1.0.10", 17 | "component/domify": "1.3.0", 18 | "component/classes": "1.2.6", 19 | "component/css": "0.0.8", 20 | "component/raf": "1.2.0", 21 | "webmodules/bounding-client-rect": "1.0.5" 22 | }, 23 | "development": { 24 | "component/aurora-tip": "*", 25 | "component/event": "*" 26 | }, 27 | "scripts": [ 28 | "index.js" 29 | ], 30 | "styles": [ 31 | "tip.css" 32 | ], 33 | "templates": [ 34 | "template.html" 35 | ], 36 | "demo": [ 37 | "http://component.github.io/tip/" 38 | ], 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | try { 6 | var css = require('css'); 7 | } catch (err) { 8 | var css = require('component-css'); 9 | } 10 | 11 | try { 12 | var bind = require('bind'); 13 | } catch (err) { 14 | var bind = require('component-bind'); 15 | } 16 | 17 | try { 18 | var query = require('query'); 19 | } catch (err) { 20 | var query = require('component-query'); 21 | } 22 | 23 | try { 24 | var events = require('events'); 25 | } catch (err) { 26 | var events = require('component-events'); 27 | } 28 | 29 | try { 30 | var Emitter = require('emitter'); 31 | } catch (err) { 32 | var Emitter = require('component-emitter'); 33 | } 34 | 35 | try { 36 | var classes = require('classes'); 37 | } catch (err) { 38 | var classes = require('component-classes'); 39 | } 40 | 41 | try { 42 | var raf = require('raf'); 43 | } catch (err) { 44 | var raf = require('component-raf'); 45 | } 46 | 47 | var domify = require('domify'); 48 | var getBoundingClientRect = require('bounding-client-rect'); 49 | 50 | var html = domify(require('./template.html')); 51 | 52 | 53 | // inspired by https://github.com/jkroso/viewport 54 | function updateViewport( v ) { 55 | v.top = window.scrollY; 56 | v.left = window.scrollX; 57 | v.width = window.innerWidth; 58 | v.height = window.innerHeight; 59 | v.right = v.left + v.width; 60 | v.bottom = v.top + v.height; 61 | return v; 62 | } 63 | 64 | var viewport = updateViewport({}); 65 | 66 | function onViewportChange() { 67 | updateViewport( viewport ); 68 | } 69 | 70 | // don't debounce these because they don't so any work that requires layout 71 | window.addEventListener('resize', onViewportChange, true) 72 | window.addEventListener('scroll', onViewportChange, true) 73 | 74 | 75 | /** 76 | * Expose `Tip`. 77 | */ 78 | 79 | module.exports = Tip; 80 | 81 | /** 82 | * Apply the average use-case of simply 83 | * showing a tool-tip on `el` hover. 84 | * 85 | * Options: 86 | * 87 | * - `delay` hide delay in milliseconds [0] 88 | * - `value` defaulting to the element's title attribute 89 | * 90 | * @param {Mixed} elem 91 | * @param {Object|String} options or value 92 | * @api public 93 | */ 94 | 95 | function tip(elem, options) { 96 | if ('string' == typeof options) options = { value : options }; 97 | var els = ('string' == typeof elem) ? query.all(elem) : [elem]; 98 | for(var i = 0, el; el = els[i]; i++) { 99 | var val = options.value || el.getAttribute('title'); 100 | var tip = new Tip(val, options); 101 | el.setAttribute('title', ''); 102 | tip.cancelHideOnHover(); 103 | tip.attach(el); 104 | } 105 | } 106 | 107 | /** 108 | * Initialize a `Tip` with the given `content`. 109 | * 110 | * @param {Mixed} content 111 | * @api public 112 | */ 113 | 114 | function Tip(content, options) { 115 | options = options || {}; 116 | if (!(this instanceof Tip)) return tip(content, options); 117 | Emitter.call(this); 118 | this.classname = ''; 119 | this.delay = options.delay || 300; 120 | this.pad = null == options.pad ? 15 : options.pad; 121 | this.el = html.cloneNode(true); 122 | this.events = events(this.el, this); 123 | this.classes = classes(this.el); 124 | this.reposition = bind( this, Tip.prototype.reposition ); 125 | this.inner = query('.tip-inner', this.el); 126 | this.message(content); 127 | this.position('top'); 128 | this.static = !!options.static; 129 | if (Tip.effect) this.effect(Tip.effect); 130 | } 131 | 132 | /** 133 | * Mixin emitter. 134 | */ 135 | 136 | Emitter(Tip.prototype); 137 | 138 | /** 139 | * Set tip `content`. 140 | * 141 | * @param {String|Element} content 142 | * @return {Tip} self 143 | * @api public 144 | */ 145 | 146 | Tip.prototype.message = function(content){ 147 | if ('string' == typeof content) content = domify(content); 148 | this.inner.appendChild(content); 149 | return this; 150 | }; 151 | 152 | /** 153 | * Attach to the given `el` with optional hide `delay`. 154 | * 155 | * @param {Element} el 156 | * @param {Number} delay 157 | * @return {Tip} 158 | * @api public 159 | */ 160 | 161 | Tip.prototype.attach = function(el){ 162 | this.target = el; 163 | this.handleEvents = events(el, this); 164 | this.handleEvents.bind('mouseover'); 165 | this.handleEvents.bind('mouseout'); 166 | return this; 167 | }; 168 | 169 | /** 170 | * On mouse over 171 | * 172 | * @param {Event} e 173 | * @return {Tip} 174 | * @api private 175 | */ 176 | 177 | Tip.prototype.onmouseover = function() { 178 | this.show(this.target); 179 | this.cancelHide(); 180 | }; 181 | 182 | /** 183 | * On mouse out 184 | * 185 | * @param {Event} e 186 | * @return {Tip} 187 | * @api private 188 | */ 189 | 190 | Tip.prototype.onmouseout = function() { 191 | this.hide(this.delay); 192 | }; 193 | 194 | /** 195 | * Cancel hide on hover, hide with the given `delay`. 196 | * 197 | * @param {Number} delay 198 | * @return {Tip} 199 | * @api public 200 | */ 201 | 202 | Tip.prototype.cancelHideOnHover = function(){ 203 | this.events.bind('mouseover', 'cancelHide'); 204 | this.events.bind('mouseout', 'hide'); 205 | return this; 206 | }; 207 | 208 | /** 209 | * Set the effect to `type`. 210 | * 211 | * @param {String} type 212 | * @return {Tip} 213 | * @api public 214 | */ 215 | 216 | Tip.prototype.effect = function(type){ 217 | this._effect = type; 218 | this.classes.add(type); 219 | return this; 220 | }; 221 | 222 | /** 223 | * Set position: 224 | * 225 | * - `top` 226 | * - `top left` 227 | * - `top right` 228 | * - `bottom` 229 | * - `bottom left` 230 | * - `bottom right` 231 | * - `left` 232 | * - `right` 233 | * 234 | * @param {String} pos 235 | * @param {Object} options 236 | * @return {Tip} 237 | * @api public 238 | */ 239 | 240 | Tip.prototype.position = function(pos, options){ 241 | options = options || {}; 242 | this._position = pos; 243 | this._auto = false != options.auto; 244 | this.replaceClass(pos); 245 | this.emit('reposition'); 246 | return this; 247 | }; 248 | 249 | /** 250 | * Show the tip attached to `el`. 251 | * 252 | * Emits "show" (el) event. 253 | * 254 | * @param {String|Element|Number} el or x 255 | * @param {Number} [y] 256 | * @return {Tip} 257 | * @api public 258 | */ 259 | 260 | Tip.prototype.show = function(el){ 261 | if ('string' == typeof el) el = query(el); 262 | 263 | // show it 264 | document.body.appendChild(this.el); 265 | this.classes.add('tip-' + this._position.replace(/\s+/g, '-')); 266 | this.classes.remove('tip-hide'); 267 | 268 | // x,y 269 | if ('number' == typeof el) { 270 | var x = arguments[0]; 271 | var y = arguments[1]; 272 | this.emit('show'); 273 | css(this.el, { 274 | top: y, 275 | left: x 276 | }); 277 | return this; 278 | } 279 | 280 | // el 281 | this.target = el; 282 | this.reposition(); 283 | this.emit('show', this.target); 284 | 285 | if (!this.winEvents && !this.static) { 286 | this.winEvents = events(window, this); 287 | this.winEvents.bind('resize', 'debouncedReposition'); 288 | this.winEvents.bind('scroll', 'debouncedReposition'); 289 | } 290 | 291 | return this; 292 | }; 293 | 294 | /** 295 | * Reposition the tip if necessary. 296 | * 297 | * @api private 298 | */ 299 | 300 | Tip.prototype.reposition = function(){ 301 | this.willReposition = null; 302 | var pos = this._position; 303 | if (this._auto) pos = this.suggested(pos); 304 | this.replaceClass(pos); 305 | this.emit('reposition'); 306 | css(this.el, constrainLeft( this.offset(pos), this.el ) ); 307 | }; 308 | 309 | /** 310 | * Reposition the tip on the next available animation frame 311 | * 312 | * @api private 313 | */ 314 | Tip.prototype.debouncedReposition = function() { 315 | this.willReposition = raf( this.reposition ); 316 | } 317 | 318 | /** 319 | * Compute the "suggested" position favouring `pos`. 320 | * 321 | * Returns `pos` if no suggestion can be determined. 322 | * 323 | * @param {String} pos 324 | * @param {Object} offset 325 | * @return {String} 326 | * @api private 327 | */ 328 | 329 | Tip.prototype.suggested = function(pos){ 330 | var target = getBoundingClientRect(this.target); 331 | var h = this.el.clientHeight; 332 | var w = this.el.clientWidth; 333 | 334 | // see where we have spare room 335 | var room = { 336 | top: target.top - h, 337 | bottom: viewport.height - target.bottom - h, 338 | left: target.left - w, 339 | right: viewport.width - target.right - w 340 | }; 341 | 342 | var positions = pos.split(/\s+/); 343 | var primary = choosePrimary(positions[0], room); 344 | if( positions[1] === primary || positions[1] === opposite[primary] ) { 345 | positions[1] = null; 346 | } 347 | return chooseSecondary(primary, positions[1], this, w, h) || pos; 348 | }; 349 | 350 | function choosePrimary(prefered, room){ 351 | // top, bottom, left, right in order of preference 352 | var order = [prefered, opposite[prefered], adjacent[prefered], opposite[adjacent[prefered]]]; 353 | var best = -Infinity; 354 | var bestPos 355 | for (var i = 0, len = order.length; i < len; i++) { 356 | var prefered = order[i]; 357 | var space = room[prefered]; 358 | // the first side it fits completely 359 | if (space > 0) return prefered; 360 | // less chopped of than other sides 361 | if (space > best) best = space, bestPos = prefered; 362 | } 363 | return bestPos; 364 | } 365 | 366 | function chooseSecondary(primary, prefered, tip, w, h){ 367 | // top, top left, top right in order of preference 368 | var order = prefered 369 | ? [primary + ' ' + prefered, primary, primary + ' ' + opposite[prefered]] 370 | : [primary, primary + ' ' + adjacent[primary], primary + ' ' + opposite[adjacent[primary]]]; 371 | var bestPos; 372 | var best = 0; 373 | var max = w * h; 374 | for (var i = 0, len = order.length; i < len; i++) { 375 | var pos = order[i]; 376 | var off = tip.offset(pos); 377 | var offRight = off.left + w; 378 | var offBottom = off.top + h; 379 | var yVisible = Math.min(off.top < viewport.top ? offBottom - viewport.top : viewport.bottom - off.top, h); 380 | var xVisible = Math.min(off.left < viewport.left ? offRight - viewport.left : viewport.right - off.left, w); 381 | var area = xVisible * yVisible; 382 | // the first position that shows all the tip 383 | if (area == max) return pos; 384 | // shows more of the tip than the other positions 385 | if (area > best) best = area, bestPos = pos; 386 | } 387 | return bestPos; 388 | } 389 | 390 | var opposite = { 391 | top: 'bottom', bottom: 'top', 392 | left: 'right', right: 'left' 393 | }; 394 | 395 | var adjacent = { 396 | top: 'right', 397 | left: 'top', 398 | bottom: 'left', 399 | right: 'bottom' 400 | }; 401 | 402 | /** 403 | * Replace position class `name`. 404 | * 405 | * @param {String} name 406 | * @api private 407 | */ 408 | 409 | Tip.prototype.replaceClass = function(name){ 410 | name = name.split(' ').join('-'); 411 | var classname = this.classname + ' tip tip-' + name; 412 | if (this._effect) classname += ' ' + this._effect; 413 | this.el.setAttribute('class', classname); 414 | }; 415 | 416 | /** 417 | * Compute the offset for `.target` 418 | * based on the given `pos`. 419 | * 420 | * @param {String} pos 421 | * @return {Object} 422 | * @api private 423 | */ 424 | 425 | Tip.prototype.offset = function(pos){ 426 | var pad = this.pad; 427 | 428 | var tipRect = getBoundingClientRect(this.el); 429 | if (!tipRect) throw new Error('could not get bounding client rect of Tip element'); 430 | var ew = tipRect.width; 431 | var eh = tipRect.height; 432 | 433 | var targetRect = getBoundingClientRect(this.target); 434 | if (!targetRect) throw new Error('could not get bounding client rect of `target`'); 435 | var tw = targetRect.width; 436 | var th = targetRect.height; 437 | 438 | var to = offset(targetRect, document); 439 | if (!to) throw new Error('could not determine page offset of `target`'); 440 | 441 | var pos; 442 | switch (pos) { 443 | case 'top': 444 | pos = { 445 | top: to.top - eh, 446 | left: to.left + tw / 2 - ew / 2 447 | }; 448 | break; 449 | case 'bottom': 450 | pos = { 451 | top: to.top + th, 452 | left: to.left + tw / 2 - ew / 2 453 | }; 454 | break; 455 | case 'right': 456 | pos = { 457 | top: to.top + th / 2 - eh / 2, 458 | left: to.left + tw 459 | }; 460 | break; 461 | case 'left': 462 | pos = { 463 | top: to.top + th / 2 - eh / 2, 464 | left: to.left - ew 465 | }; 466 | break; 467 | case 'top left': 468 | pos = { 469 | top: to.top - eh, 470 | left: to.left + tw / 2 - ew + pad 471 | }; 472 | break; 473 | case 'top right': 474 | pos = { 475 | top: to.top - eh, 476 | left: to.left + tw / 2 - pad 477 | }; 478 | break; 479 | case 'bottom left': 480 | pos = { 481 | top: to.top + th, 482 | left: to.left + tw / 2 - ew + pad 483 | }; 484 | break; 485 | case 'bottom right': 486 | pos = { 487 | top: to.top + th, 488 | left: to.left + tw / 2 - pad 489 | }; 490 | break; 491 | case 'left top': 492 | pos = { 493 | top: to.top + th / 2 - eh, 494 | left: to.left - ew 495 | }; 496 | break; 497 | case 'left bottom': 498 | pos = { 499 | top: to.top + th / 2, 500 | left: to.left - ew 501 | }; 502 | break; 503 | case 'right top': 504 | pos = { 505 | top: to.top + th / 2 - eh, 506 | left: to.left + tw 507 | }; 508 | break; 509 | case 'right bottom': 510 | pos = { 511 | top: to.top + th / 2, 512 | left: to.left + tw 513 | }; 514 | break; 515 | default: 516 | throw new Error('invalid position "' + pos + '"'); 517 | } 518 | return pos; 519 | }; 520 | 521 | /** 522 | * Cancel the `.hide()` timeout. 523 | * 524 | * @api private 525 | */ 526 | 527 | Tip.prototype.cancelHide = function(){ 528 | clearTimeout(this._hide); 529 | }; 530 | 531 | /** 532 | * Hide the tip with optional `ms` delay. 533 | * 534 | * Emits "hide" event. 535 | * 536 | * @param {Number} ms 537 | * @return {Tip} 538 | * @api public 539 | */ 540 | 541 | Tip.prototype.hide = function(ms){ 542 | var self = this; 543 | 544 | this.emit('hiding'); 545 | 546 | // duration 547 | if (ms) { 548 | this._hide = setTimeout(bind(this, this.hide), ms); 549 | return this; 550 | } 551 | 552 | // hide 553 | this.classes.add('tip-hide'); 554 | if (this._effect) { 555 | setTimeout(bind(this, this.remove), 300); 556 | } else { 557 | self.remove(); 558 | } 559 | 560 | return this; 561 | }; 562 | 563 | /** 564 | * Hide the tip without potential animation. 565 | * 566 | * @return {Tip} 567 | * @api public 568 | */ 569 | 570 | Tip.prototype.remove = function(){ 571 | if (this.winEvents) { 572 | this.winEvents.unbind(); 573 | this.winEvents = null; 574 | } 575 | if (this._willReposition) { 576 | raf.cancel( this.willReposition ); 577 | this.willReposition = null; 578 | } 579 | this.emit('hide'); 580 | 581 | var parent = this.el.parentNode; 582 | if (parent) parent.removeChild(this.el); 583 | return this; 584 | }; 585 | 586 | /** 587 | * Extracted from `timoxley/offset`, but directly using a 588 | * TextRectangle instead of getting another version. 589 | * 590 | * @param {TextRectangle} box - result from a `getBoundingClientRect()` call 591 | * @param {Document} doc - Document instance to use 592 | * @return {Object} an object with `top` and `left` Number properties 593 | * @api private 594 | */ 595 | 596 | function offset (box, doc) { 597 | var body = doc.body || doc.getElementsByTagName('body')[0]; 598 | var docEl = doc.documentElement || body.parentNode; 599 | var clientTop = docEl.clientTop || body.clientTop || 0; 600 | var clientLeft = docEl.clientLeft || body.clientLeft || 0; 601 | var scrollTop = window.pageYOffset || docEl.scrollTop; 602 | var scrollLeft = window.pageXOffset || docEl.scrollLeft; 603 | 604 | return { 605 | top: box.top + scrollTop - clientTop, 606 | left: box.left + scrollLeft - clientLeft 607 | }; 608 | } 609 | 610 | /** 611 | * Constrain a left to keep the element in the window 612 | * @param {Object} pl proposed left 613 | * @param {Number} ew tip element width 614 | * @return {Number} the best width 615 | */ 616 | function constrainLeft ( off, el ) { 617 | var ew = getBoundingClientRect(el).width; 618 | off.left = Math.max( 0, Math.min( off.left, viewport.width - ew ) ); 619 | return off; 620 | } 621 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "component-tip", 3 | "description": "Tip component", 4 | "version": "3.0.3", 5 | "keywords": [ 6 | "browser", 7 | "component", 8 | "tooltip", 9 | "tip", 10 | "ui" 11 | ], 12 | "dependencies": { 13 | "bounding-client-rect": "1.0.5", 14 | "component-bind": "*", 15 | "component-classes": "1.2.6", 16 | "component-css": "0.0.8", 17 | "component-emitter": "1.1.3", 18 | "component-events": "1.0.10", 19 | "component-query": "0.0.3", 20 | "component-raf": "1.2.0", 21 | "domify": "1.3.0", 22 | "html-browserify": "0.0.4" 23 | }, 24 | "browser": { 25 | "bind": "component-bind", 26 | "classes": "component-classes", 27 | "css": "component-css", 28 | "emitter": "component-emitter", 29 | "events": "component-events", 30 | "query": "component-query", 31 | "raf": "component-raf" 32 | }, 33 | "browserify": { 34 | "transform": "html-browserify" 35 | }, 36 | "style": "tip.css", 37 | "component": { 38 | "styles": [ 39 | "tip.css" 40 | ], 41 | "templates": [ 42 | "template.html" 43 | ], 44 | "scripts": { 45 | "tip/index.js": "index.js" 46 | } 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/component/tip.git" 51 | }, 52 | "license": "MIT" 53 | } 54 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | <div class="tip tip-hide"> 2 | <div class="tip-arrow"></div> 3 | <div class="tip-inner"></div> 4 | </div> -------------------------------------------------------------------------------- /test/auto.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE 5> 2 | <html> 3 | <head> 4 | <title>Tip 5 | 6 | 7 | 32 | 33 | 34 | Tip 35 | 36 | 39 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tip 5 | 6 | 7 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | 76 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------