commit | author | age
|
1759c2
|
1 |
/** |
RM |
2 |
* angular-strap |
|
3 |
* @version v2.0.3 - 2014-05-30 |
|
4 |
* @link http://mgcrea.github.io/angular-strap |
|
5 |
* @author Olivier Louvignes (olivier@mg-crea.com) |
|
6 |
* @license MIT License, http://www.opensource.org/licenses/MIT |
|
7 |
*/ |
|
8 |
'use strict'; |
|
9 |
angular.module('mgcrea.ngStrap.tooltip', ['mgcrea.ngStrap.helpers.dimensions']).provider('$tooltip', function () { |
|
10 |
var defaults = this.defaults = { |
|
11 |
animation: 'am-fade', |
|
12 |
prefixClass: 'tooltip', |
|
13 |
prefixEvent: 'tooltip', |
|
14 |
container: false, |
|
15 |
target: false, |
|
16 |
placement: 'top', |
|
17 |
template: 'tooltip/tooltip.tpl.html', |
|
18 |
contentTemplate: false, |
|
19 |
trigger: 'hover focus', |
|
20 |
keyboard: false, |
|
21 |
html: false, |
|
22 |
show: false, |
|
23 |
title: '', |
|
24 |
type: '', |
|
25 |
delay: 0 |
|
26 |
}; |
|
27 |
this.$get = [ |
|
28 |
'$window', |
|
29 |
'$rootScope', |
|
30 |
'$compile', |
|
31 |
'$q', |
|
32 |
'$templateCache', |
|
33 |
'$http', |
|
34 |
'$animate', |
|
35 |
'dimensions', |
|
36 |
'$$rAF', |
|
37 |
function ($window, $rootScope, $compile, $q, $templateCache, $http, $animate, dimensions, $$rAF) { |
|
38 |
var trim = String.prototype.trim; |
|
39 |
var isTouch = 'createTouch' in $window.document; |
|
40 |
var htmlReplaceRegExp = /ng-bind="/gi; |
|
41 |
function TooltipFactory(element, config) { |
|
42 |
var $tooltip = {}; |
|
43 |
// Common vars |
|
44 |
var nodeName = element[0].nodeName.toLowerCase(); |
|
45 |
var options = $tooltip.$options = angular.extend({}, defaults, config); |
|
46 |
$tooltip.$promise = fetchTemplate(options.template); |
|
47 |
var scope = $tooltip.$scope = options.scope && options.scope.$new() || $rootScope.$new(); |
|
48 |
if (options.delay && angular.isString(options.delay)) { |
|
49 |
options.delay = parseFloat(options.delay); |
|
50 |
} |
|
51 |
// Support scope as string options |
|
52 |
if (options.title) { |
|
53 |
$tooltip.$scope.title = options.title; |
|
54 |
} |
|
55 |
// Provide scope helpers |
|
56 |
scope.$hide = function () { |
|
57 |
scope.$$postDigest(function () { |
|
58 |
$tooltip.hide(); |
|
59 |
}); |
|
60 |
}; |
|
61 |
scope.$show = function () { |
|
62 |
scope.$$postDigest(function () { |
|
63 |
$tooltip.show(); |
|
64 |
}); |
|
65 |
}; |
|
66 |
scope.$toggle = function () { |
|
67 |
scope.$$postDigest(function () { |
|
68 |
$tooltip.toggle(); |
|
69 |
}); |
|
70 |
}; |
|
71 |
$tooltip.$isShown = scope.$isShown = false; |
|
72 |
// Private vars |
|
73 |
var timeout, hoverState; |
|
74 |
// Support contentTemplate option |
|
75 |
if (options.contentTemplate) { |
|
76 |
$tooltip.$promise = $tooltip.$promise.then(function (template) { |
|
77 |
var templateEl = angular.element(template); |
|
78 |
return fetchTemplate(options.contentTemplate).then(function (contentTemplate) { |
|
79 |
var contentEl = findElement('[ng-bind="content"]', templateEl[0]); |
|
80 |
if (!contentEl.length) |
|
81 |
contentEl = findElement('[ng-bind="title"]', templateEl[0]); |
|
82 |
contentEl.removeAttr('ng-bind').html(contentTemplate); |
|
83 |
return templateEl[0].outerHTML; |
|
84 |
}); |
|
85 |
}); |
|
86 |
} |
|
87 |
// Fetch, compile then initialize tooltip |
|
88 |
var tipLinker, tipElement, tipTemplate, tipContainer; |
|
89 |
$tooltip.$promise.then(function (template) { |
|
90 |
if (angular.isObject(template)) |
|
91 |
template = template.data; |
|
92 |
if (options.html) |
|
93 |
template = template.replace(htmlReplaceRegExp, 'ng-bind-html="'); |
|
94 |
template = trim.apply(template); |
|
95 |
tipTemplate = template; |
|
96 |
tipLinker = $compile(template); |
|
97 |
$tooltip.init(); |
|
98 |
}); |
|
99 |
$tooltip.init = function () { |
|
100 |
// Options: delay |
|
101 |
if (options.delay && angular.isNumber(options.delay)) { |
|
102 |
options.delay = { |
|
103 |
show: options.delay, |
|
104 |
hide: options.delay |
|
105 |
}; |
|
106 |
} |
|
107 |
// Replace trigger on touch devices ? |
|
108 |
// if(isTouch && options.trigger === defaults.trigger) { |
|
109 |
// options.trigger.replace(/hover/g, 'click'); |
|
110 |
// } |
|
111 |
// Options : container |
|
112 |
if (options.container === 'self') { |
|
113 |
tipContainer = element; |
|
114 |
} else if (options.container) { |
|
115 |
tipContainer = findElement(options.container); |
|
116 |
} |
|
117 |
// Options: trigger |
|
118 |
var triggers = options.trigger.split(' '); |
|
119 |
angular.forEach(triggers, function (trigger) { |
|
120 |
if (trigger === 'click') { |
|
121 |
element.on('click', $tooltip.toggle); |
|
122 |
} else if (trigger !== 'manual') { |
|
123 |
element.on(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter); |
|
124 |
element.on(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave); |
|
125 |
nodeName === 'button' && trigger !== 'hover' && element.on(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown); |
|
126 |
} |
|
127 |
}); |
|
128 |
// Options: target |
|
129 |
if (options.target) { |
|
130 |
options.target = angular.isElement(options.target) ? options.target : findElement(options.target)[0]; |
|
131 |
} |
|
132 |
// Options: show |
|
133 |
if (options.show) { |
|
134 |
scope.$$postDigest(function () { |
|
135 |
options.trigger === 'focus' ? element[0].focus() : $tooltip.show(); |
|
136 |
}); |
|
137 |
} |
|
138 |
}; |
|
139 |
$tooltip.destroy = function () { |
|
140 |
// Unbind events |
|
141 |
var triggers = options.trigger.split(' '); |
|
142 |
for (var i = triggers.length; i--;) { |
|
143 |
var trigger = triggers[i]; |
|
144 |
if (trigger === 'click') { |
|
145 |
element.off('click', $tooltip.toggle); |
|
146 |
} else if (trigger !== 'manual') { |
|
147 |
element.off(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter); |
|
148 |
element.off(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave); |
|
149 |
nodeName === 'button' && trigger !== 'hover' && element.off(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown); |
|
150 |
} |
|
151 |
} |
|
152 |
// Remove element |
|
153 |
if (tipElement) { |
|
154 |
tipElement.remove(); |
|
155 |
tipElement = null; |
|
156 |
} |
|
157 |
// Cancel pending callbacks |
|
158 |
clearTimeout(timeout); |
|
159 |
// Destroy scope |
|
160 |
scope.$destroy(); |
|
161 |
}; |
|
162 |
$tooltip.enter = function () { |
|
163 |
clearTimeout(timeout); |
|
164 |
hoverState = 'in'; |
|
165 |
if (!options.delay || !options.delay.show) { |
|
166 |
return $tooltip.show(); |
|
167 |
} |
|
168 |
timeout = setTimeout(function () { |
|
169 |
if (hoverState === 'in') |
|
170 |
$tooltip.show(); |
|
171 |
}, options.delay.show); |
|
172 |
}; |
|
173 |
$tooltip.show = function () { |
|
174 |
scope.$emit(options.prefixEvent + '.show.before', $tooltip); |
|
175 |
var parent = options.container ? tipContainer : null; |
|
176 |
var after = options.container ? null : element; |
|
177 |
// Hide any existing tipElement |
|
178 |
if (tipElement) |
|
179 |
tipElement.remove(); |
|
180 |
// Fetch a cloned element linked from template |
|
181 |
tipElement = $tooltip.$element = tipLinker(scope, function (clonedElement, scope) { |
|
182 |
}); |
|
183 |
// Set the initial positioning. |
|
184 |
tipElement.css({ |
|
185 |
top: '-9999px', |
|
186 |
left: '-9999px', |
|
187 |
display: 'block' |
|
188 |
}).addClass(options.placement); |
|
189 |
// Options: animation |
|
190 |
if (options.animation) |
|
191 |
tipElement.addClass(options.animation); |
|
192 |
// Options: type |
|
193 |
if (options.type) |
|
194 |
tipElement.addClass(options.prefixClass + '-' + options.type); |
|
195 |
$animate.enter(tipElement, parent, after, function () { |
|
196 |
scope.$emit(options.prefixEvent + '.show', $tooltip); |
|
197 |
}); |
|
198 |
$tooltip.$isShown = scope.$isShown = true; |
|
199 |
scope.$$phase || scope.$root && scope.$root.$$phase || scope.$digest(); |
|
200 |
$$rAF($tooltip.$applyPlacement); |
|
201 |
// var a = bodyEl.offsetWidth + 1; ? |
|
202 |
// Bind events |
|
203 |
if (options.keyboard) { |
|
204 |
if (options.trigger !== 'focus') { |
|
205 |
$tooltip.focus(); |
|
206 |
tipElement.on('keyup', $tooltip.$onKeyUp); |
|
207 |
} else { |
|
208 |
element.on('keyup', $tooltip.$onFocusKeyUp); |
|
209 |
} |
|
210 |
} |
|
211 |
}; |
|
212 |
$tooltip.leave = function () { |
|
213 |
clearTimeout(timeout); |
|
214 |
hoverState = 'out'; |
|
215 |
if (!options.delay || !options.delay.hide) { |
|
216 |
return $tooltip.hide(); |
|
217 |
} |
|
218 |
timeout = setTimeout(function () { |
|
219 |
if (hoverState === 'out') { |
|
220 |
$tooltip.hide(); |
|
221 |
} |
|
222 |
}, options.delay.hide); |
|
223 |
}; |
|
224 |
$tooltip.hide = function (blur) { |
|
225 |
if (!$tooltip.$isShown) |
|
226 |
return; |
|
227 |
scope.$emit(options.prefixEvent + '.hide.before', $tooltip); |
|
228 |
$animate.leave(tipElement, function () { |
|
229 |
scope.$emit(options.prefixEvent + '.hide', $tooltip); |
|
230 |
}); |
|
231 |
$tooltip.$isShown = scope.$isShown = false; |
|
232 |
scope.$$phase || scope.$root && scope.$root.$$phase || scope.$digest(); |
|
233 |
// Unbind events |
|
234 |
if (options.keyboard && tipElement !== null) { |
|
235 |
tipElement.off('keyup', $tooltip.$onKeyUp); |
|
236 |
} |
|
237 |
// Allow to blur the input when hidden, like when pressing enter key |
|
238 |
if (blur && options.trigger === 'focus') { |
|
239 |
return element[0].blur(); |
|
240 |
} |
|
241 |
}; |
|
242 |
$tooltip.toggle = function () { |
|
243 |
$tooltip.$isShown ? $tooltip.leave() : $tooltip.enter(); |
|
244 |
}; |
|
245 |
$tooltip.focus = function () { |
|
246 |
tipElement[0].focus(); |
|
247 |
}; |
|
248 |
// Protected methods |
|
249 |
$tooltip.$applyPlacement = function () { |
|
250 |
if (!tipElement) |
|
251 |
return; |
|
252 |
// Get the position of the tooltip element. |
|
253 |
var elementPosition = getPosition(); |
|
254 |
// Get the height and width of the tooltip so we can center it. |
|
255 |
var tipWidth = tipElement.prop('offsetWidth'), tipHeight = tipElement.prop('offsetHeight'); |
|
256 |
// Get the tooltip's top and left coordinates to center it with this directive. |
|
257 |
var tipPosition = getCalculatedOffset(options.placement, elementPosition, tipWidth, tipHeight); |
|
258 |
// Now set the calculated positioning. |
|
259 |
tipPosition.top += 'px'; |
|
260 |
tipPosition.left += 'px'; |
|
261 |
tipElement.css(tipPosition); |
|
262 |
}; |
|
263 |
$tooltip.$onKeyUp = function (evt) { |
|
264 |
evt.which === 27 && $tooltip.hide(); |
|
265 |
}; |
|
266 |
$tooltip.$onFocusKeyUp = function (evt) { |
|
267 |
evt.which === 27 && element[0].blur(); |
|
268 |
}; |
|
269 |
$tooltip.$onFocusElementMouseDown = function (evt) { |
|
270 |
evt.preventDefault(); |
|
271 |
evt.stopPropagation(); |
|
272 |
// Some browsers do not auto-focus buttons (eg. Safari) |
|
273 |
$tooltip.$isShown ? element[0].blur() : element[0].focus(); |
|
274 |
}; |
|
275 |
// Private methods |
|
276 |
function getPosition() { |
|
277 |
if (options.container === 'body') { |
|
278 |
return dimensions.offset(options.target || element[0]); |
|
279 |
} else { |
|
280 |
return dimensions.position(options.target || element[0]); |
|
281 |
} |
|
282 |
} |
|
283 |
function getCalculatedOffset(placement, position, actualWidth, actualHeight) { |
|
284 |
var offset; |
|
285 |
var split = placement.split('-'); |
|
286 |
switch (split[0]) { |
|
287 |
case 'right': |
|
288 |
offset = { |
|
289 |
top: position.top + position.height / 2 - actualHeight / 2, |
|
290 |
left: position.left + position.width |
|
291 |
}; |
|
292 |
break; |
|
293 |
case 'bottom': |
|
294 |
offset = { |
|
295 |
top: position.top + position.height, |
|
296 |
left: position.left + position.width / 2 - actualWidth / 2 |
|
297 |
}; |
|
298 |
break; |
|
299 |
case 'left': |
|
300 |
offset = { |
|
301 |
top: position.top + position.height / 2 - actualHeight / 2, |
|
302 |
left: position.left - actualWidth |
|
303 |
}; |
|
304 |
break; |
|
305 |
default: |
|
306 |
offset = { |
|
307 |
top: position.top - actualHeight, |
|
308 |
left: position.left + position.width / 2 - actualWidth / 2 |
|
309 |
}; |
|
310 |
break; |
|
311 |
} |
|
312 |
if (!split[1]) { |
|
313 |
return offset; |
|
314 |
} |
|
315 |
// Add support for corners @todo css |
|
316 |
if (split[0] === 'top' || split[0] === 'bottom') { |
|
317 |
switch (split[1]) { |
|
318 |
case 'left': |
|
319 |
offset.left = position.left; |
|
320 |
break; |
|
321 |
case 'right': |
|
322 |
offset.left = position.left + position.width - actualWidth; |
|
323 |
} |
|
324 |
} else if (split[0] === 'left' || split[0] === 'right') { |
|
325 |
switch (split[1]) { |
|
326 |
case 'top': |
|
327 |
offset.top = position.top - actualHeight; |
|
328 |
break; |
|
329 |
case 'bottom': |
|
330 |
offset.top = position.top + position.height; |
|
331 |
} |
|
332 |
} |
|
333 |
return offset; |
|
334 |
} |
|
335 |
return $tooltip; |
|
336 |
} |
|
337 |
// Helper functions |
|
338 |
function findElement(query, element) { |
|
339 |
return angular.element((element || document).querySelectorAll(query)); |
|
340 |
} |
|
341 |
function fetchTemplate(template) { |
|
342 |
return $q.when($templateCache.get(template) || $http.get(template)).then(function (res) { |
|
343 |
if (angular.isObject(res)) { |
|
344 |
$templateCache.put(template, res.data); |
|
345 |
return res.data; |
|
346 |
} |
|
347 |
return res; |
|
348 |
}); |
|
349 |
} |
|
350 |
return TooltipFactory; |
|
351 |
} |
|
352 |
]; |
|
353 |
}).directive('bsTooltip', [ |
|
354 |
'$window', |
|
355 |
'$location', |
|
356 |
'$sce', |
|
357 |
'$tooltip', |
|
358 |
'$$rAF', |
|
359 |
function ($window, $location, $sce, $tooltip, $$rAF) { |
|
360 |
return { |
|
361 |
restrict: 'EAC', |
|
362 |
scope: true, |
|
363 |
link: function postLink(scope, element, attr, transclusion) { |
|
364 |
// Directive options |
|
365 |
var options = { scope: scope }; |
|
366 |
angular.forEach([ |
|
367 |
'template', |
|
368 |
'contentTemplate', |
|
369 |
'placement', |
|
370 |
'container', |
|
371 |
'target', |
|
372 |
'delay', |
|
373 |
'trigger', |
|
374 |
'keyboard', |
|
375 |
'html', |
|
376 |
'animation', |
|
377 |
'type' |
|
378 |
], function (key) { |
|
379 |
if (angular.isDefined(attr[key])) |
|
380 |
options[key] = attr[key]; |
|
381 |
}); |
|
382 |
// Observe scope attributes for change |
|
383 |
angular.forEach(['title'], function (key) { |
|
384 |
attr[key] && attr.$observe(key, function (newValue, oldValue) { |
|
385 |
scope[key] = $sce.trustAsHtml(newValue); |
|
386 |
angular.isDefined(oldValue) && $$rAF(function () { |
|
387 |
tooltip && tooltip.$applyPlacement(); |
|
388 |
}); |
|
389 |
}); |
|
390 |
}); |
|
391 |
// Support scope as an object |
|
392 |
attr.bsTooltip && scope.$watch(attr.bsTooltip, function (newValue, oldValue) { |
|
393 |
if (angular.isObject(newValue)) { |
|
394 |
angular.extend(scope, newValue); |
|
395 |
} else { |
|
396 |
scope.title = newValue; |
|
397 |
} |
|
398 |
angular.isDefined(oldValue) && $$rAF(function () { |
|
399 |
tooltip && tooltip.$applyPlacement(); |
|
400 |
}); |
|
401 |
}, true); |
|
402 |
// Initialize popover |
|
403 |
var tooltip = $tooltip(element, options); |
|
404 |
// Garbage collection |
|
405 |
scope.$on('$destroy', function () { |
|
406 |
tooltip.destroy(); |
|
407 |
options = null; |
|
408 |
tooltip = null; |
|
409 |
}); |
|
410 |
} |
|
411 |
}; |
|
412 |
} |
|
413 |
]); |