├── .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 | 
7 | 
8 |
9 | 
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 |
--------------------------------------------------------------------------------
/test/auto.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 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 |
--------------------------------------------------------------------------------