UNPKG

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