UNPKG

6.37 kBJavaScriptView Raw
1
2/**
3 * Module dependencies.
4 */
5
6var Emitter = require('emitter')
7 , o = require('jquery');
8
9/**
10 * Expose `Tip`.
11 */
12
13module.exports = Tip;
14
15/**
16 * Apply the average use-case of simply
17 * showing a tool-tip on `el` hover.
18 *
19 * Options:
20 *
21 * - `delay` hide delay in milliseconds [0]
22 * - `value` defaulting to the element's title attribute
23 *
24 * @param {Mixed} el
25 * @param {Object} options
26 * @api public
27 */
28
29function tip(el, options) {
30 options = options || {};
31 var delay = options.delay || 0;
32
33 o(el).each(function(i, el){
34 el = o(el);
35 var val = options.value || el.attr('title');
36 var tip = new Tip(val);
37 el.attr('title', '');
38 tip.cancelHideOnHover(delay);
39 tip.attach(el, delay);
40 });
41}
42
43/**
44 * Initialize a `Tip` with the given `content`.
45 *
46 * @param {Mixed} content
47 * @api public
48 */
49
50function Tip(content, options) {
51 if (!(this instanceof Tip)) return tip(content, options);
52 Emitter.call(this);
53 this.classname = '';
54 this._content = content;
55 this.el = o(require('./template'));
56 this.inner = this.el.find('.tip-inner');
57 this.position('north');
58 if (Tip.effect) this.effect(Tip.effect);
59}
60
61/**
62 * Inherits from `Emitter.prototype`.
63 */
64
65Tip.prototype.__proto__ = Emitter.prototype;
66
67/**
68 * Attach to the given `el` with optional hide `delay`.
69 *
70 * @param {Element} el
71 * @param {Number} delay
72 * @return {Tip}
73 * @api public
74 */
75
76Tip.prototype.attach = function(el, delay){
77 el.hover(
78 this.show.bind(this, el),
79 this.hide.bind(this, delay || 0));
80 return this;
81};
82
83/**
84 * Cancel hide on hover, hide with the given `delay`.
85 *
86 * @param {Number} delay
87 * @return {Tip}
88 * @api public
89 */
90
91Tip.prototype.cancelHideOnHover = function(delay){
92 this.el.hover(
93 this.cancelHide.bind(this),
94 this.hide.bind(this, delay || 0));
95 return this;
96};
97
98/**
99 * Set the effect to `type`.
100 *
101 * @param {String} type
102 * @return {Tip}
103 * @api public
104 */
105
106Tip.prototype.effect = function(type){
107 this._effect = type;
108 this.el.addClass(type);
109 return this;
110};
111
112/**
113 * Set position `type`:
114 *
115 * - `north`
116 * - `north east`
117 * - `north west`
118 * - `south`
119 * - `south east`
120 * - `south west`
121 * - `east`
122 * - `west`
123 *
124 * @param {String} type
125 * @return {Tip}
126 * @api public
127 */
128
129Tip.prototype.position = function(type){
130 this._position = type;
131 return this;
132};
133
134/**
135 * Show the tip attached to `el`.
136 *
137 * Emits "show" (el) event.
138 *
139 * @param {jQuery|Element} el
140 * @return {Tip}
141 * @api public
142 */
143
144Tip.prototype.show = function(el){
145 if (!el) throw new Error('.show() element required');
146 this.target = o(el);
147 this.inner.empty().append(this._content);
148 this.el.appendTo('body');
149 this.el.addClass('tip-' + this._position);
150 this.reposition();
151 this.el.removeClass('tip-hide');
152 this.emit('show', this.target);
153 this._reposition = this.reposition.bind(this);
154 o(window).bind('resize', this._reposition);
155 o(window).bind('scroll', this._reposition);
156 return this;
157};
158
159/**
160 * Reposition the tip if necessary.
161 *
162 * @api private
163 */
164
165Tip.prototype.reposition = function(){
166 var pos = this._position;
167 var off = this.offset(pos);
168 var newpos = this.suggested(pos, off);
169 if (newpos) off = this.offset(pos = newpos);
170 this.replaceClass(pos);
171 this.el.css(off);
172};
173
174/**
175 * Compute the "suggested" position favouring `pos`.
176 * Returns undefined if no suggestion is made.
177 *
178 * @param {String} pos
179 * @param {Object} offset
180 * @return {String}
181 * @api private
182 */
183
184Tip.prototype.suggested = function(pos, off){
185 var el = this.el;
186
187 var ew = el.outerWidth();
188 var eh = el.outerHeight();
189
190 var win = o(window);
191 var top = win.scrollTop();
192 var left = win.scrollLeft();
193 var w = win.width();
194 var h = win.height();
195
196 // too high
197 if (off.top < top) return 'south';
198
199 // too low
200 if (off.top + eh > top + h) return 'north';
201
202 // too far to the right
203 if (off.left + ew > left + w) return 'west';
204
205 // too far to the left
206 if (off.left < left) return 'east';
207};
208
209/**
210 * Replace position class `name`.
211 *
212 * @param {String} name
213 * @api private
214 */
215
216Tip.prototype.replaceClass = function(name){
217 name = name.split(' ').join('-');
218 this.el.attr('class', this.classname + ' tip tip-' + name + ' ' + this._effect);
219};
220
221/**
222 * Compute the offset for `.target`
223 * based on the given `pos`.
224 *
225 * @param {String} pos
226 * @return {Object}
227 * @api private
228 */
229
230Tip.prototype.offset = function(pos){
231 var el = this.el;
232 var target = this.target;
233
234 var ew = el.outerWidth();
235 var eh = el.outerHeight();
236
237 var to = target.offset();
238 var tw = target.outerWidth();
239 var th = target.outerHeight();
240
241 switch (pos) {
242 case 'north':
243 return {
244 top: to.top - eh,
245 left: to.left + tw / 2 - ew / 2
246 }
247 case 'north west':
248 return {
249 top: to.top,
250 left: to.left - ew
251 }
252 case 'north east':
253 return {
254 top: to.top,
255 left: to.left + tw
256 }
257 case 'south':
258 return {
259 top: to.top + th,
260 left: to.left + tw / 2 - ew / 2
261 }
262 case 'south west':
263 return {
264 top: to.top + th - eh * .85,
265 left: to.left - ew
266 }
267 case 'south east':
268 return {
269 top: to.top + th - eh * .85,
270 left: to.left + tw
271 }
272 case 'east':
273 return {
274 top: to.top + th / 2 - eh / 2,
275 left: to.left + tw
276 }
277 case 'west':
278 return {
279 top: to.top + th / 2 - eh / 2,
280 left: to.left - ew
281 }
282 default:
283 throw new Error('invalid position "' + pos + '"');
284 }
285};
286
287/**
288 * Cancel the `.hide()` timeout.
289 *
290 * @api private
291 */
292
293Tip.prototype.cancelHide = function(){
294 clearTimeout(this._hide);
295};
296
297/**
298 * Hide the tip with optional `ms` delay.
299 *
300 * Emits "hide" event.
301 *
302 * @param {Number} ms
303 * @return {Tip}
304 * @api public
305 */
306
307Tip.prototype.hide = function(ms){
308 var self = this;
309
310 // duration
311 if (ms) {
312 this._hide = setTimeout(this.hide.bind(this), ms);
313 return this;
314 }
315
316 // hide
317 this.el.addClass('tip-hide');
318 if (this._effect) {
319 setTimeout(this.remove.bind(this), 300);
320 } else {
321 self.remove();
322 }
323
324 return this;
325};
326
327/**
328 * Hide the tip without potential animation.
329 *
330 * @return {Tip}
331 * @api
332 */
333
334Tip.prototype.remove = function(){
335 o(window).unbind('resize', this._reposition);
336 o(window).unbind('scroll', this._reposition);
337 this.emit('hide');
338 this.el.detach();
339 return this;
340};