##// END OF EJS Templates
hovercacrds: added new tooltips and hovercards to expose certain information for objects shown in UI
marcink -
r4026:ed756817 default
parent child Browse files
Show More
@@ -0,0 +1,38 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2018-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.config import routing_links
21
22
23 def includeme(config):
24
25 config.add_route(
26 name='hovercard_user',
27 pattern='/_hovercard/user/{user_id}')
28
29 config.add_route(
30 name='hovercard_user_group',
31 pattern='/_hovercard/user_group/{user_group_id}')
32
33 config.add_route(
34 name='hovercard_commit',
35 pattern='/_hovercard/commit/{repo_name}/{user_id}')
36
37 # Scan module for configuration decorators.
38 config.scan('.views', ignore='.tests')
@@ -0,0 +1,71 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import re
22 import logging
23 import collections
24
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
32 from rhodecode.lib.index import searcher_from_config
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.model.db import (
37 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
38 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.repo_group import RepoGroupModel
40 from rhodecode.model.scm import RepoGroupList, RepoList
41 from rhodecode.model.user import UserModel
42 from rhodecode.model.user_group import UserGroupModel
43
44 log = logging.getLogger(__name__)
45
46
47 class HoverCardsView(BaseAppView):
48
49 def load_default_context(self):
50 c = self._get_local_tmpl_context()
51 return c
52
53 @LoginRequired()
54 @view_config(
55 route_name='hovercard_user', request_method='GET', xhr=True,
56 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
57 def hovercard_user(self):
58 c = self.load_default_context()
59 user_id = self.request.matchdict['user_id']
60 c.user = User.get_or_404(user_id)
61 return self._get_template_context(c)
62
63 @LoginRequired()
64 @view_config(
65 route_name='hovercard_user_group', request_method='GET', xhr=True,
66 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
67 def hovercard_user_group(self):
68 c = self.load_default_context()
69 user_group_id = self.request.matchdict['user_group_id']
70 c.user_group = UserGroup.get_or_404(user_group_id)
71 return self._get_template_context(c)
@@ -0,0 +1,460 b''
1 /* This is the core CSS of Tooltipster */
2
3 /* GENERAL STRUCTURE RULES (do not edit this section) */
4
5 .tooltipster-base {
6 /* this ensures that a constrained height set by functionPosition,
7 if greater that the natural height of the tooltip, will be enforced
8 in browsers that support display:flex */
9 display: flex;
10 pointer-events: none;
11 /* this may be overriden in JS for fixed position origins */
12 position: absolute;
13 }
14
15 .tooltipster-box {
16 /* see .tooltipster-base. flex-shrink 1 is only necessary for IE10-
17 and flex-basis auto for IE11- (at least) */
18 flex: 1 1 auto;
19 }
20
21 .tooltipster-content {
22 /* prevents an overflow if the user adds padding to the div */
23 box-sizing: border-box;
24 /* these make sure we'll be able to detect any overflow */
25 max-height: 100%;
26 max-width: 100%;
27 overflow: auto;
28 }
29
30 .tooltipster-ruler {
31 /* these let us test the size of the tooltip without overflowing the window */
32 bottom: 0;
33 left: 0;
34 overflow: hidden;
35 position: fixed;
36 right: 0;
37 top: 0;
38 visibility: hidden;
39 }
40
41 /* ANIMATIONS */
42
43 /* Open/close animations */
44
45 /* fade */
46
47 .tooltipster-fade {
48 opacity: 0;
49 -webkit-transition-property: opacity;
50 -moz-transition-property: opacity;
51 -o-transition-property: opacity;
52 -ms-transition-property: opacity;
53 transition-property: opacity;
54 }
55 .tooltipster-fade.tooltipster-show {
56 opacity: 1;
57 }
58
59 /* grow */
60
61 .tooltipster-grow {
62 -webkit-transform: scale(0,0);
63 -moz-transform: scale(0,0);
64 -o-transform: scale(0,0);
65 -ms-transform: scale(0,0);
66 transform: scale(0,0);
67 -webkit-transition-property: -webkit-transform;
68 -moz-transition-property: -moz-transform;
69 -o-transition-property: -o-transform;
70 -ms-transition-property: -ms-transform;
71 transition-property: transform;
72 -webkit-backface-visibility: hidden;
73 }
74 .tooltipster-grow.tooltipster-show {
75 -webkit-transform: scale(1,1);
76 -moz-transform: scale(1,1);
77 -o-transform: scale(1,1);
78 -ms-transform: scale(1,1);
79 transform: scale(1,1);
80 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
81 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
82 -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
83 -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
84 -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
85 transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
86 }
87
88 /* swing */
89
90 .tooltipster-swing {
91 opacity: 0;
92 -webkit-transform: rotateZ(4deg);
93 -moz-transform: rotateZ(4deg);
94 -o-transform: rotateZ(4deg);
95 -ms-transform: rotateZ(4deg);
96 transform: rotateZ(4deg);
97 -webkit-transition-property: -webkit-transform, opacity;
98 -moz-transition-property: -moz-transform;
99 -o-transition-property: -o-transform;
100 -ms-transition-property: -ms-transform;
101 transition-property: transform;
102 }
103 .tooltipster-swing.tooltipster-show {
104 opacity: 1;
105 -webkit-transform: rotateZ(0deg);
106 -moz-transform: rotateZ(0deg);
107 -o-transform: rotateZ(0deg);
108 -ms-transform: rotateZ(0deg);
109 transform: rotateZ(0deg);
110 -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 1);
111 -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
112 -moz-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
113 -ms-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
114 -o-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
115 transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
116 }
117
118 /* fall */
119
120 .tooltipster-fall {
121 -webkit-transition-property: top;
122 -moz-transition-property: top;
123 -o-transition-property: top;
124 -ms-transition-property: top;
125 transition-property: top;
126 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
127 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
128 -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
129 -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
130 -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
131 transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
132 }
133 .tooltipster-fall.tooltipster-initial {
134 top: 0 !important;
135 }
136 .tooltipster-fall.tooltipster-show {
137 }
138 .tooltipster-fall.tooltipster-dying {
139 -webkit-transition-property: all;
140 -moz-transition-property: all;
141 -o-transition-property: all;
142 -ms-transition-property: all;
143 transition-property: all;
144 top: 0 !important;
145 opacity: 0;
146 }
147
148 /* slide */
149
150 .tooltipster-slide {
151 -webkit-transition-property: left;
152 -moz-transition-property: left;
153 -o-transition-property: left;
154 -ms-transition-property: left;
155 transition-property: left;
156 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
157 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
158 -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
159 -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
160 -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
161 transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
162 }
163 .tooltipster-slide.tooltipster-initial {
164 left: -40px !important;
165 }
166 .tooltipster-slide.tooltipster-show {
167 }
168 .tooltipster-slide.tooltipster-dying {
169 -webkit-transition-property: all;
170 -moz-transition-property: all;
171 -o-transition-property: all;
172 -ms-transition-property: all;
173 transition-property: all;
174 left: 0 !important;
175 opacity: 0;
176 }
177
178 /* Update animations */
179
180 /* We use animations rather than transitions here because
181 transition durations may be specified in the style tag due to
182 animationDuration, and we try to avoid collisions and the use
183 of !important */
184
185 /* fade */
186
187 @keyframes tooltipster-fading {
188 0% {
189 opacity: 0;
190 }
191 100% {
192 opacity: 1;
193 }
194 }
195
196 .tooltipster-update-fade {
197 animation: tooltipster-fading 400ms;
198 }
199
200 /* rotate */
201
202 @keyframes tooltipster-rotating {
203 25% {
204 transform: rotate(-2deg);
205 }
206 75% {
207 transform: rotate(2deg);
208 }
209 100% {
210 transform: rotate(0);
211 }
212 }
213
214 .tooltipster-update-rotate {
215 animation: tooltipster-rotating 600ms;
216 }
217
218 /* scale */
219
220 @keyframes tooltipster-scaling {
221 50% {
222 transform: scale(1.1);
223 }
224 100% {
225 transform: scale(1);
226 }
227 }
228
229 .tooltipster-update-scale {
230 animation: tooltipster-scaling 600ms;
231 }
232
233 /**
234 * DEFAULT STYLE OF THE SIDETIP PLUGIN
235 *
236 * All styles are "namespaced" with .tooltipster-sidetip to prevent
237 * conflicts between plugins.
238 */
239
240 /* .tooltipster-box */
241
242 .tooltipster-sidetip .tooltipster-box {
243 background: #565656;
244 border: 2px solid black;
245 border-radius: 4px;
246 }
247
248 .tooltipster-sidetip.tooltipster-bottom .tooltipster-box {
249 margin-top: 8px;
250 }
251
252 .tooltipster-sidetip.tooltipster-left .tooltipster-box {
253 margin-right: 8px;
254 }
255
256 .tooltipster-sidetip.tooltipster-right .tooltipster-box {
257 margin-left: 8px;
258 }
259
260 .tooltipster-sidetip.tooltipster-top .tooltipster-box {
261 margin-bottom: 8px;
262 }
263
264 /* .tooltipster-content */
265
266 .tooltipster-sidetip .tooltipster-content {
267 color: white;
268 line-height: 18px;
269 padding: 6px 14px;
270 }
271
272 /* .tooltipster-arrow : will keep only the zone of .tooltipster-arrow-uncropped that
273 corresponds to the arrow we want to display */
274
275 .tooltipster-sidetip .tooltipster-arrow {
276 overflow: hidden;
277 position: absolute;
278 }
279
280 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow {
281 height: 10px;
282 /* half the width, for centering */
283 margin-left: -10px;
284 top: 0;
285 width: 20px;
286 }
287
288 .tooltipster-sidetip.tooltipster-left .tooltipster-arrow {
289 height: 20px;
290 margin-top: -10px;
291 right: 0;
292 /* top 0 to keep the arrow from overflowing .tooltipster-base when it has not
293 been positioned yet */
294 top: 0;
295 width: 10px;
296 }
297
298 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow {
299 height: 20px;
300 margin-top: -10px;
301 left: 0;
302 /* same as .tooltipster-left .tooltipster-arrow */
303 top: 0;
304 width: 10px;
305 }
306
307 .tooltipster-sidetip.tooltipster-top .tooltipster-arrow {
308 bottom: 0;
309 height: 10px;
310 margin-left: -10px;
311 width: 20px;
312 }
313
314 /* common rules between .tooltipster-arrow-background and .tooltipster-arrow-border */
315
316 .tooltipster-sidetip .tooltipster-arrow-background, .tooltipster-sidetip .tooltipster-arrow-border {
317 height: 0;
318 position: absolute;
319 width: 0;
320 }
321
322 /* .tooltipster-arrow-background */
323
324 .tooltipster-sidetip .tooltipster-arrow-background {
325 border: 10px solid transparent;
326 }
327
328 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background {
329 border-bottom-color: #565656;
330 left: 0;
331 top: 3px;
332 }
333
334 .tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background {
335 border-left-color: #565656;
336 left: -3px;
337 top: 0;
338 }
339
340 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background {
341 border-right-color: #565656;
342 left: 3px;
343 top: 0;
344 }
345
346 .tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background {
347 border-top-color: #565656;
348 left: 0;
349 top: -3px;
350 }
351
352 /* .tooltipster-arrow-border */
353
354 .tooltipster-sidetip .tooltipster-arrow-border {
355 border: 10px solid transparent;
356 left: 0;
357 top: 0;
358 }
359
360 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border {
361 border-bottom-color: black;
362 }
363
364 .tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border {
365 border-left-color: black;
366 }
367
368 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border {
369 border-right-color: black;
370 }
371
372 .tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border {
373 border-top-color: black;
374 }
375
376 /* tooltipster-arrow-uncropped */
377
378 .tooltipster-sidetip .tooltipster-arrow-uncropped {
379 position: relative;
380 }
381
382 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped {
383 top: -10px;
384 }
385
386 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped {
387 left: -10px;
388 }
389
390 .tooltipster-sidetip.tooltipster-shadow .tooltipster-box {
391 border: none;
392 border-radius: 5px;
393 background: #fff;
394 box-shadow: 0 0 5px 3px rgba(0, 0, 0, .1)
395 }
396
397 .tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-box {
398 margin-top: 6px
399 }
400
401 .tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-box {
402 margin-right: 6px
403 }
404
405 .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-box {
406 margin-left: 6px
407 }
408
409 .tooltipster-sidetip.tooltipster-shadow.tooltipster-top .tooltipster-box {
410 margin-bottom: 6px
411 }
412
413 .tooltipster-sidetip.tooltipster-shadow .tooltipster-content {
414 color: #8d8d8d
415 }
416
417 .tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow {
418 height: 6px;
419 margin-left: -6px;
420 width: 12px
421 }
422
423 .tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-arrow, .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow {
424 height: 12px;
425 margin-left: 0;
426 margin-top: -6px;
427 width: 6px
428 }
429
430 .tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow-background {
431 display: none
432 }
433
434 .tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow-border {
435 border: 6px solid transparent
436 }
437
438 .tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-arrow-border {
439 border-bottom-color: #fff
440 }
441
442 .tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-arrow-border {
443 border-left-color: #fff
444 }
445
446 .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow-border {
447 border-right-color: #fff
448 }
449
450 .tooltipster-sidetip.tooltipster-shadow.tooltipster-top .tooltipster-arrow-border {
451 border-top-color: #fff
452 }
453
454 .tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-arrow-uncropped {
455 top: -6px
456 }
457
458 .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow-uncropped {
459 left: -6px
460 } No newline at end of file
@@ -0,0 +1,24 b''
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3
4 <div class="user-hovercard">
5 <div class="user-hovercard-header">
6
7 </div>
8
9 <div class="user-hovercard-icon">
10 ${base.gravatar(c.user.email, 64)}
11 </div>
12
13 <div class="user-hovercard-name">
14 <strong>${c.user.full_name_or_username}</strong> <br/>
15 <code>@${h.link_to_user(c.user.username)}</code>
16
17 <div class="user-hovercard-bio">${dt.render_description(c.user.description, c.visual.stylify_metatags)}</div>
18 </div>
19
20 <div class="user-hovercard-footer">
21
22 </div>
23
24 </div>
@@ -0,0 +1,24 b''
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3
4 <div class="user-group-hovercard">
5 <div class="user-group-hovercard-header">
6
7 </div>
8
9 <div class="user-group-hovercard-icon">
10 ${base.user_group_icon(c.user_group, 64)}
11 </div>
12
13 <div class="user-group-hovercard-name">
14 <strong><a href="${h.route_path('user_group_profile', user_group_name=c.user_group.users_group_name)}">${c.user_group.users_group_name}</a></strong> <br/>
15 Members: ${len(c.user_group.members)}
16
17 <div class="user-group-hovercard-bio">${dt.render_description(c.user_group.user_group_description, c.visual.stylify_metatags)}</div>
18 </div>
19
20 <div class="user-group-hovercard-footer">
21
22 </div>
23
24 </div>
@@ -282,6 +282,7 b' def includeme(config):'
282 282 # apps
283 283 if load_all:
284 284 config.include('rhodecode.apps._base')
285 config.include('rhodecode.apps.hovercards')
285 286 config.include('rhodecode.apps.ops')
286 287 config.include('rhodecode.apps.admin')
287 288 config.include('rhodecode.apps.channelstream')
@@ -899,10 +899,9 b' def person_by_id(id_, show_attr="usernam'
899 899 return id_
900 900
901 901
902 def gravatar_with_user(request, author, show_disabled=False):
903 _render = request.get_partial_renderer(
904 'rhodecode:templates/base/base.mako')
905 return _render('gravatar_with_user', author, show_disabled=show_disabled)
902 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
903 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
904 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
906 905
907 906
908 907 tags_paterns = OrderedDict((
@@ -1625,7 +1624,7 b' def _process_url_func(match_obj, repo_na'
1625 1624
1626 1625 if link_format == 'html':
1627 1626 tmpl = (
1628 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1627 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1629 1628 '%(issue-prefix)s%(id-repr)s'
1630 1629 '</a>')
1631 1630 elif link_format == 'rst':
@@ -1643,7 +1642,7 b' def _process_url_func(match_obj, repo_na'
1643 1642 'id': issue_id,
1644 1643 'repo': repo_name,
1645 1644 'repo_name': repo_name_cleaned,
1646 'group_name': parent_group_name
1645 'group_name': parent_group_name,
1647 1646 }
1648 1647 # named regex variables
1649 1648 named_vars.update(match_obj.groupdict())
@@ -1660,6 +1659,7 b' def _process_url_func(match_obj, repo_na'
1660 1659 'id-repr': issue_id,
1661 1660 'issue-prefix': entry['pref'],
1662 1661 'serv': entry['url'],
1662 'title': entry['desc']
1663 1663 }
1664 1664 if return_raw_data:
1665 1665 return {
@@ -26,6 +26,7 b''
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 @import 'tooltips';
29 30
30 31 //--- BASE ------------------//
31 32 .noscript-error {
@@ -2819,3 +2820,78 b' form.markup-form {'
2819 2820 padding-top: 10px;
2820 2821 clear: both;
2821 2822 }
2823
2824
2825 .user-hovercard {
2826 padding: 5px;
2827 }
2828
2829 .user-hovercard-icon {
2830 display: inline;
2831 padding: 0;
2832 box-sizing: content-box;
2833 border-radius: 50%;
2834 float: left;
2835 }
2836
2837 .user-hovercard-name {
2838 float: right;
2839 vertical-align: top;
2840 padding-left: 10px;
2841 min-width: 150px;
2842 }
2843
2844 .user-hovercard-bio {
2845 clear: both;
2846 padding-top: 10px;
2847 }
2848
2849 .user-hovercard-header {
2850 clear: both;
2851 min-height: 10px;
2852 }
2853
2854 .user-hovercard-footer {
2855 clear: both;
2856 min-height: 10px;
2857 }
2858
2859 .user-group-hovercard {
2860 padding: 5px;
2861 }
2862
2863 .user-group-hovercard-icon {
2864 display: inline;
2865 padding: 0;
2866 box-sizing: content-box;
2867 border-radius: 50%;
2868 float: left;
2869 }
2870
2871 .user-group-hovercard-name {
2872 float: left;
2873 vertical-align: top;
2874 padding-left: 10px;
2875 min-width: 150px;
2876 }
2877
2878 .user-group-hovercard-icon i {
2879 border: 1px solid @grey4;
2880 border-radius: 4px;
2881 }
2882
2883 .user-group-hovercard-bio {
2884 clear: both;
2885 padding-top: 10px;
2886 line-height: 1.0em;
2887 }
2888
2889 .user-group-hovercard-header {
2890 clear: both;
2891 min-height: 10px;
2892 }
2893
2894 .user-group-hovercard-footer {
2895 clear: both;
2896 min-height: 10px;
2897 }
@@ -246,6 +246,7 b''
246 246 .icon-pr-merge-fail:before { &:extend(.icon-delete:before); }
247 247 .icon-wide-mode:before { &:extend(.icon-sort:before); }
248 248 .icon-flag-filled-red:before { &:extend(.icon-flag-filled:before); }
249 .icon-user-group-alt:before { &:extend(.icon-group:before); }
249 250
250 251 // TRANSFORM
251 252 .icon-merge:before {transform: rotate(180deg);}
@@ -30,6 +30,9 b' function registerRCRoutes() {'
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
35 pyroutes.register('hovercard_commit', '/_hovercard/commit/%(repo_name)s/%(user_id)s', ['repo_name', 'user_id']);
33 36 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
34 37 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
35 38 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
@@ -221,6 +221,90 b' var clipboardActivate = function() {'
221 221 });
222 222 };
223 223
224 var tooltipActivate = function () {
225 var delay = 50;
226 var animation = 'fade';
227 var theme = 'tooltipster-shadow';
228 var debug = false;
229
230 $('.tooltip').tooltipster({
231 debug: debug,
232 theme: theme,
233 animation: animation,
234 delay: delay,
235 contentCloning: true,
236 contentAsHTML: true,
237
238 functionBefore: function (instance, helper) {
239 var $origin = $(helper.origin);
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 instance.content(data);
242 }
243 });
244 var hovercardCache = {};
245
246 var loadHoverCard = function (url, callback) {
247 var id = url;
248
249 if (hovercardCache[id] !== undefined) {
250 callback(hovercardCache[id]);
251 return;
252 }
253
254 hovercardCache[id] = undefined;
255 $.get(url, function (data) {
256 hovercardCache[id] = data;
257 callback(hovercardCache[id]);
258 }).fail(function (data, textStatus, errorThrown) {
259 var msg = "Error while fetching hovercard.\nError code {0} ({1}).".format(data.status,data.statusText);
260 callback(msg);
261 });
262 };
263
264 $('.tooltip-hovercard').tooltipster({
265 debug: debug,
266 theme: theme,
267 animation: animation,
268 delay: delay,
269 interactive: true,
270 contentCloning: true,
271
272 trigger: 'custom',
273 triggerOpen: {
274 mouseenter: true,
275 },
276 triggerClose: {
277 mouseleave: true,
278 originClick: true,
279 touchleave: true
280 },
281 content: _gettext('Loading...'),
282 contentAsHTML: true,
283 updateAnimation: null,
284
285 functionBefore: function (instance, helper) {
286
287 var $origin = $(helper.origin);
288
289 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
290 if ($origin.data('loaded') !== true) {
291 var hovercardUrl = $origin.data('hovercardUrl');
292
293 if (hovercardUrl !== undefined && hovercardUrl !== "") {
294 loadHoverCard(hovercardUrl, function (data) {
295 instance.content(data);
296 })
297 } else {
298 var data = '<div style="white-space: pre-wrap">{0}</div>'.format($origin.data('hovercardAlt'))
299 instance.content(data);
300 }
301
302 // to remember that the data has been loaded
303 $origin.data('loaded', true);
304 }
305 }
306 })
307 };
224 308
225 309 // Formatting values in a Select2 dropdown of commit references
226 310 var formatSelect2SelectionRefs = function(commit_ref){
@@ -335,6 +335,7 b' var _submitAjaxPOST = function(url, post'
335 335 $('#injected_page_comments').append(o.rendered_text);
336 336 self.resetCommentFormState();
337 337 timeagoActivate();
338 tooltipActivate();
338 339
339 340 // mark visually which comment was resolved
340 341 if (resolvesCommentId) {
@@ -865,6 +866,7 b' var CommentsController = function() {'
865 866 // re trigger the linkification of next/prev navigation
866 867 linkifyComments($('.inline-comment-injected'));
867 868 timeagoActivate();
869 tooltipActivate();
868 870
869 871 if (window.updateSticky !== undefined) {
870 872 // potentially our comments change the active window size, so we
@@ -382,6 +382,7 b' var getFilesMetadata = function() {'
382 382 metadataRequest.done(function(data) {
383 383 $('#file-tree').html(data);
384 384 timeagoActivate();
385 tooltipActivate();
385 386 });
386 387 metadataRequest.fail(function (data, textStatus, errorThrown) {
387 388 if (data.status != 0) {
@@ -406,7 +407,8 b' var showAuthors = function(elem, annotat'
406 407 timeout: 5000
407 408 }).complete(function(){
408 409 $(elem).hide();
409 $('#file_authors_title').html(_gettext('All Authors'))
410 $('#file_authors_title').html(_gettext('All Authors'));
411 tooltipActivate();
410 412 })
411 413 };
412 414
@@ -320,6 +320,7 b' ReviewersController = function () {'
320 320 'reasons': reasons,
321 321 'create': true
322 322 });
323 tooltipActivate();
323 324 }
324 325 }
325 326
This diff has been collapsed as it changes many lines, (4347 lines changed) Show them Hide them
@@ -1,94 +1,4283 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 // #
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
6 // #
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
1 /**
2 * tooltipster http://iamceege.github.io/tooltipster/
3 * A rockin' custom tooltip jQuery plugin
4 * Developed by Caleb Jacob and Louis Ameline
5 * MIT license
6 */
7 (function (root, factory) {
8 if (typeof define === 'function' && define.amd) {
9 // AMD. Register as an anonymous module unless amdModuleId is set
10 define(["jquery"], function (a0) {
11 return (factory(a0));
12 });
13 } else if (typeof exports === 'object') {
14 // Node. Does not work with strict CommonJS, but
15 // only CommonJS-like environments that support module.exports,
16 // like Node.
17 module.exports = factory(require("jquery"));
18 } else {
19 factory(jQuery);
20 }
21 }(this, function ($) {
22
23 // This file will be UMDified by a build task.
24
25 var defaults = {
26 animation: 'fade',
27 animationDuration: 350,
28 content: null,
29 contentAsHTML: false,
30 contentCloning: false,
31 debug: true,
32 delay: 300,
33 delayTouch: [300, 500],
34 functionInit: null,
35 functionBefore: null,
36 functionReady: null,
37 functionAfter: null,
38 functionFormat: null,
39 IEmin: 6,
40 interactive: false,
41 multiple: false,
42 // will default to document.body, or must be an element positioned at (0, 0)
43 // in the document, typically like the very top views of an app.
44 parent: null,
45 plugins: ['sideTip'],
46 repositionOnScroll: false,
47 restoration: 'none',
48 selfDestruction: true,
49 theme: [],
50 timer: 0,
51 trackerInterval: 500,
52 trackOrigin: false,
53 trackTooltip: false,
54 trigger: 'hover',
55 triggerClose: {
56 click: false,
57 mouseleave: false,
58 originClick: false,
59 scroll: false,
60 tap: false,
61 touchleave: false
62 },
63 triggerOpen: {
64 click: false,
65 mouseenter: false,
66 tap: false,
67 touchstart: false
68 },
69 updateAnimation: 'rotate',
70 zIndex: 9999999
71 },
72 // we'll avoid using the 'window' global as a good practice but npm's
73 // jquery@<2.1.0 package actually requires a 'window' global, so not sure
74 // it's useful at all
75 win = (typeof window != 'undefined') ? window : null,
76 // env will be proxied by the core for plugins to have access its properties
77 env = {
78 // detect if this device can trigger touch events. Better have a false
79 // positive (unused listeners, that's ok) than a false negative.
80 // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
81 // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
82 hasTouchCapability: !!(
83 win
84 && ( 'ontouchstart' in win
85 || (win.DocumentTouch && win.document instanceof win.DocumentTouch)
86 || win.navigator.maxTouchPoints
87 )
88 ),
89 hasTransitions: transitionSupport(),
90 IE: false,
91 // don't set manually, it will be updated by a build task after the manifest
92 semVer: '4.2.7',
93 window: win
94 },
95 core = function() {
96
97 // core variables
98
99 // the core emitters
100 this.__$emitterPrivate = $({});
101 this.__$emitterPublic = $({});
102 this.__instancesLatestArr = [];
103 // collects plugin constructors
104 this.__plugins = {};
105 // proxy env variables for plugins who might use them
106 this._env = env;
107 };
108
109 // core methods
110 core.prototype = {
111
112 /**
113 * A function to proxy the public methods of an object onto another
114 *
115 * @param {object} constructor The constructor to bridge
116 * @param {object} obj The object that will get new methods (an instance or the core)
117 * @param {string} pluginName A plugin name for the console log message
118 * @return {core}
119 * @private
120 */
121 __bridge: function(constructor, obj, pluginName) {
122
123 // if it's not already bridged
124 if (!obj[pluginName]) {
125
126 var fn = function() {};
127 fn.prototype = constructor;
128
129 var pluginInstance = new fn();
130
131 // the _init method has to exist in instance constructors but might be missing
132 // in core constructors
133 if (pluginInstance.__init) {
134 pluginInstance.__init(obj);
135 }
136
137 $.each(constructor, function(methodName, fn) {
138
139 // don't proxy "private" methods, only "protected" and public ones
140 if (methodName.indexOf('__') != 0) {
141
142 // if the method does not exist yet
143 if (!obj[methodName]) {
144
145 obj[methodName] = function() {
146 return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments));
147 };
148
149 // remember to which plugin this method corresponds (several plugins may
150 // have methods of the same name, we need to be sure)
151 obj[methodName].bridged = pluginInstance;
152 }
153 else if (defaults.debug) {
154
155 console.log('The '+ methodName +' method of the '+ pluginName
156 +' plugin conflicts with another plugin or native methods');
157 }
158 }
159 });
160
161 obj[pluginName] = pluginInstance;
162 }
163
164 return this;
165 },
166
167 /**
168 * For mockup in Node env if need be, for testing purposes
169 *
170 * @return {core}
171 * @private
172 */
173 __setWindow: function(window) {
174 env.window = window;
175 return this;
176 },
177
178 /**
179 * Returns a ruler, a tool to help measure the size of a tooltip under
180 * various settings. Meant for plugins
181 *
182 * @see Ruler
183 * @return {object} A Ruler instance
184 * @protected
185 */
186 _getRuler: function($tooltip) {
187 return new Ruler($tooltip);
188 },
189
190 /**
191 * For internal use by plugins, if needed
192 *
193 * @return {core}
194 * @protected
195 */
196 _off: function() {
197 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
198 return this;
199 },
200
201 /**
202 * For internal use by plugins, if needed
203 *
204 * @return {core}
205 * @protected
206 */
207 _on: function() {
208 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
209 return this;
210 },
211
212 /**
213 * For internal use by plugins, if needed
214 *
215 * @return {core}
216 * @protected
217 */
218 _one: function() {
219 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
220 return this;
221 },
222
223 /**
224 * Returns (getter) or adds (setter) a plugin
225 *
226 * @param {string|object} plugin Provide a string (in the full form
227 * "namespace.name") to use as as getter, an object to use as a setter
228 * @return {object|core}
229 * @protected
230 */
231 _plugin: function(plugin) {
232
233 var self = this;
234
235 // getter
236 if (typeof plugin == 'string') {
237
238 var pluginName = plugin,
239 p = null;
240
241 // if the namespace is provided, it's easy to search
242 if (pluginName.indexOf('.') > 0) {
243 p = self.__plugins[pluginName];
244 }
245 // otherwise, return the first name that matches
246 else {
247 $.each(self.__plugins, function(i, plugin) {
248
249 if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) {
250 p = plugin;
251 return false;
252 }
253 });
254 }
255
256 return p;
257 }
258 // setter
259 else {
260
261 // force namespaces
262 if (plugin.name.indexOf('.') < 0) {
263 throw new Error('Plugins must be namespaced');
264 }
265
266 self.__plugins[plugin.name] = plugin;
267
268 // if the plugin has core features
269 if (plugin.core) {
270
271 // bridge non-private methods onto the core to allow new core methods
272 self.__bridge(plugin.core, self, plugin.name);
273 }
274
275 return this;
276 }
277 },
278
279 /**
280 * Trigger events on the core emitters
281 *
282 * @returns {core}
283 * @protected
284 */
285 _trigger: function() {
286
287 var args = Array.prototype.slice.apply(arguments);
288
289 if (typeof args[0] == 'string') {
290 args[0] = { type: args[0] };
291 }
292
293 // note: the order of emitters matters
294 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
295 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
296
297 return this;
298 },
299
300 /**
301 * Returns instances of all tooltips in the page or an a given element
302 *
303 * @param {string|HTML object collection} selector optional Use this
304 * parameter to restrict the set of objects that will be inspected
305 * for the retrieval of instances. By default, all instances in the
306 * page are returned.
307 * @return {array} An array of instance objects
308 * @public
309 */
310 instances: function(selector) {
311
312 var instances = [],
313 sel = selector || '.tooltipstered';
314
315 $(sel).each(function() {
316
317 var $this = $(this),
318 ns = $this.data('tooltipster-ns');
319
320 if (ns) {
321
322 $.each(ns, function(i, namespace) {
323 instances.push($this.data(namespace));
324 });
325 }
326 });
327
328 return instances;
329 },
330
331 /**
332 * Returns the Tooltipster objects generated by the last initializing call
333 *
334 * @return {array} An array of instance objects
335 * @public
336 */
337 instancesLatest: function() {
338 return this.__instancesLatestArr;
339 },
340
341 /**
342 * For public use only, not to be used by plugins (use ::_off() instead)
343 *
344 * @return {core}
345 * @public
346 */
347 off: function() {
348 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
349 return this;
350 },
351
352 /**
353 * For public use only, not to be used by plugins (use ::_on() instead)
354 *
355 * @return {core}
356 * @public
357 */
358 on: function() {
359 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
360 return this;
361 },
362
363 /**
364 * For public use only, not to be used by plugins (use ::_one() instead)
365 *
366 * @return {core}
367 * @public
368 */
369 one: function() {
370 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
371 return this;
372 },
373
374 /**
375 * Returns all HTML elements which have one or more tooltips
376 *
377 * @param {string} selector optional Use this to restrict the results
378 * to the descendants of an element
379 * @return {array} An array of HTML elements
380 * @public
381 */
382 origins: function(selector) {
383
384 var sel = selector ?
385 selector +' ' :
386 '';
387
388 return $(sel +'.tooltipstered').toArray();
389 },
390
391 /**
392 * Change default options for all future instances
393 *
394 * @param {object} d The options that should be made defaults
395 * @return {core}
396 * @public
397 */
398 setDefaults: function(d) {
399 $.extend(defaults, d);
400 return this;
401 },
402
403 /**
404 * For users to trigger their handlers on the public emitter
405 *
406 * @returns {core}
407 * @public
408 */
409 triggerHandler: function() {
410 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
411 return this;
412 }
413 };
414
415 // $.tooltipster will be used to call core methods
416 $.tooltipster = new core();
417
418 // the Tooltipster instance class (mind the capital T)
419 $.Tooltipster = function(element, options) {
420
421 // list of instance variables
422
423 // stack of custom callbacks provided as parameters to API methods
424 this.__callbacks = {
425 close: [],
426 open: []
427 };
428 // the schedule time of DOM removal
429 this.__closingTime;
430 // this will be the user content shown in the tooltip. A capital "C" is used
431 // because there is also a method called content()
432 this.__Content;
433 // for the size tracker
434 this.__contentBcr;
435 // to disable the tooltip after destruction
436 this.__destroyed = false;
437 // we can't emit directly on the instance because if a method with the same
438 // name as the event exists, it will be called by jQuery. Se we use a plain
439 // object as emitter. This emitter is for internal use by plugins,
440 // if needed.
441 this.__$emitterPrivate = $({});
442 // this emitter is for the user to listen to events without risking to mess
443 // with our internal listeners
444 this.__$emitterPublic = $({});
445 this.__enabled = true;
446 // the reference to the gc interval
447 this.__garbageCollector;
448 // various position and size data recomputed before each repositioning
449 this.__Geometry;
450 // the tooltip position, saved after each repositioning by a plugin
451 this.__lastPosition;
452 // a unique namespace per instance
453 this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000);
454 this.__options;
455 // will be used to support origins in scrollable areas
456 this.__$originParents;
457 this.__pointerIsOverOrigin = false;
458 // to remove themes if needed
459 this.__previousThemes = [];
460 // the state can be either: appearing, stable, disappearing, closed
461 this.__state = 'closed';
462 // timeout references
463 this.__timeouts = {
464 close: [],
465 open: null
466 };
467 // store touch events to be able to detect emulated mouse events
468 this.__touchEvents = [];
469 // the reference to the tracker interval
470 this.__tracker = null;
471 // the element to which this tooltip is associated
472 this._$origin;
473 // this will be the tooltip element (jQuery wrapped HTML element).
474 // It's the job of a plugin to create it and append it to the DOM
475 this._$tooltip;
476
477 // launch
478 this.__init(element, options);
479 };
480
481 $.Tooltipster.prototype = {
482
483 /**
484 * @param origin
485 * @param options
486 * @private
487 */
488 __init: function(origin, options) {
489
490 var self = this;
491
492 self._$origin = $(origin);
493 self.__options = $.extend(true, {}, defaults, options);
494
495 // some options may need to be reformatted
496 self.__optionsFormat();
497
498 // don't run on old IE if asked no to
499 if ( !env.IE
500 || env.IE >= self.__options.IEmin
501 ) {
502
503 // note: the content is null (empty) by default and can stay that
504 // way if the plugin remains initialized but not fed any content. The
505 // tooltip will just not appear.
506
507 // let's save the initial value of the title attribute for later
508 // restoration if need be.
509 var initialTitle = null;
510
511 // it will already have been saved in case of multiple tooltips
512 if (self._$origin.data('tooltipster-initialTitle') === undefined) {
513
514 initialTitle = self._$origin.attr('title');
515
516 // we do not want initialTitle to be "undefined" because
517 // of how jQuery's .data() method works
518 if (initialTitle === undefined) initialTitle = null;
519
520 self._$origin.data('tooltipster-initialTitle', initialTitle);
521 }
522
523 // If content is provided in the options, it has precedence over the
524 // title attribute.
525 // Note: an empty string is considered content, only 'null' represents
526 // the absence of content.
527 // Also, an existing title="" attribute will result in an empty string
528 // content
529 if (self.__options.content !== null) {
530 self.__contentSet(self.__options.content);
531 }
532 else {
533
534 var selector = self._$origin.attr('data-tooltip-content'),
535 $el;
536
537 if (selector){
538 $el = $(selector);
539 }
540
541 if ($el && $el[0]) {
542 self.__contentSet($el.first());
543 }
544 else {
545 self.__contentSet(initialTitle);
546 }
547 }
548
549 self._$origin
550 // strip the title off of the element to prevent the default tooltips
551 // from popping up
552 .removeAttr('title')
553 // to be able to find all instances on the page later (upon window
554 // events in particular)
555 .addClass('tooltipstered');
556
557 // set listeners on the origin
558 self.__prepareOrigin();
559
560 // set the garbage collector
561 self.__prepareGC();
562
563 // init plugins
564 $.each(self.__options.plugins, function(i, pluginName) {
565 self._plug(pluginName);
566 });
567
568 // to detect swiping
569 if (env.hasTouchCapability) {
570 $(env.window.document.body).on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) {
571 self._touchRecordEvent(event);
572 });
573 }
574
575 self
576 // prepare the tooltip when it gets created. This event must
577 // be fired by a plugin
578 ._on('created', function() {
579 self.__prepareTooltip();
580 })
581 // save position information when it's sent by a plugin
582 ._on('repositioned', function(e) {
583 self.__lastPosition = e.position;
584 });
585 }
586 else {
587 self.__options.disabled = true;
588 }
589 },
590
591 /**
592 * Insert the content into the appropriate HTML element of the tooltip
593 *
594 * @returns {self}
595 * @private
596 */
597 __contentInsert: function() {
598
599 var self = this,
600 $el = self._$tooltip.find('.tooltipster-content'),
601 formattedContent = self.__Content,
602 format = function(content) {
603 formattedContent = content;
604 };
605
606 self._trigger({
607 type: 'format',
608 content: self.__Content,
609 format: format
610 });
611
612 if (self.__options.functionFormat) {
613
614 formattedContent = self.__options.functionFormat.call(
615 self,
616 self,
617 { origin: self._$origin[0] },
618 self.__Content
619 );
620 }
621
622 if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) {
623 $el.text(formattedContent);
624 }
625 else {
626 $el
627 .empty()
628 .append(formattedContent);
629 }
630
631 return self;
632 },
633
634 /**
635 * Save the content, cloning it beforehand if need be
636 *
637 * @param content
638 * @returns {self}
639 * @private
640 */
641 __contentSet: function(content) {
642
643 // clone if asked. Cloning the object makes sure that each instance has its
644 // own version of the content (in case a same object were provided for several
645 // instances)
646 // reminder: typeof null === object
647 if (content instanceof $ && this.__options.contentCloning) {
648 content = content.clone(true);
649 }
650
651 this.__Content = content;
652
653 this._trigger({
654 type: 'updated',
655 content: content
656 });
657
658 return this;
659 },
660
661 /**
662 * Error message about a method call made after destruction
663 *
664 * @private
665 */
666 __destroyError: function() {
667 throw new Error('This tooltip has been destroyed and cannot execute your method call.');
668 },
669
670 /**
671 * Gather all information about dimensions and available space,
672 * called before every repositioning
673 *
674 * @private
675 * @returns {object}
676 */
677 __geometry: function() {
678
679 var self = this,
680 $target = self._$origin,
681 originIsArea = self._$origin.is('area');
682
683 // if this._$origin is a map area, the target we'll need
684 // the dimensions of is actually the image using the map,
685 // not the area itself
686 if (originIsArea) {
687
688 var mapName = self._$origin.parent().attr('name');
689
690 $target = $('img[usemap="#'+ mapName +'"]');
691 }
692
693 var bcr = $target[0].getBoundingClientRect(),
694 $document = $(env.window.document),
695 $window = $(env.window),
696 $parent = $target,
697 // some useful properties of important elements
698 geo = {
699 // available space for the tooltip, see down below
700 available: {
701 document: null,
702 window: null
703 },
704 document: {
705 size: {
706 height: $document.height(),
707 width: $document.width()
708 }
709 },
710 window: {
711 scroll: {
712 // the second ones are for IE compatibility
713 left: env.window.scrollX || env.window.document.documentElement.scrollLeft,
714 top: env.window.scrollY || env.window.document.documentElement.scrollTop
715 },
716 size: {
717 height: $window.height(),
718 width: $window.width()
719 }
720 },
721 origin: {
722 // the origin has a fixed lineage if itself or one of its
723 // ancestors has a fixed position
724 fixedLineage: false,
725 // relative to the document
726 offset: {},
727 size: {
728 height: bcr.bottom - bcr.top,
729 width: bcr.right - bcr.left
730 },
731 usemapImage: originIsArea ? $target[0] : null,
732 // relative to the window
733 windowOffset: {
734 bottom: bcr.bottom,
735 left: bcr.left,
736 right: bcr.right,
737 top: bcr.top
738 }
739 }
740 },
741 geoFixed = false;
742
743 // if the element is a map area, some properties may need
744 // to be recalculated
745 if (originIsArea) {
746
747 var shape = self._$origin.attr('shape'),
748 coords = self._$origin.attr('coords');
749
750 if (coords) {
751
752 coords = coords.split(',');
753
754 $.map(coords, function(val, i) {
755 coords[i] = parseInt(val);
756 });
757 }
758
759 // if the image itself is the area, nothing more to do
760 if (shape != 'default') {
761
762 switch(shape) {
763
764 case 'circle':
765
766 var circleCenterLeft = coords[0],
767 circleCenterTop = coords[1],
768 circleRadius = coords[2],
769 areaTopOffset = circleCenterTop - circleRadius,
770 areaLeftOffset = circleCenterLeft - circleRadius;
771
772 geo.origin.size.height = circleRadius * 2;
773 geo.origin.size.width = geo.origin.size.height;
774
775 geo.origin.windowOffset.left += areaLeftOffset;
776 geo.origin.windowOffset.top += areaTopOffset;
777
778 break;
779
780 case 'rect':
781
782 var areaLeft = coords[0],
783 areaTop = coords[1],
784 areaRight = coords[2],
785 areaBottom = coords[3];
786
787 geo.origin.size.height = areaBottom - areaTop;
788 geo.origin.size.width = areaRight - areaLeft;
789
790 geo.origin.windowOffset.left += areaLeft;
791 geo.origin.windowOffset.top += areaTop;
792
793 break;
794
795 case 'poly':
796
797 var areaSmallestX = 0,
798 areaSmallestY = 0,
799 areaGreatestX = 0,
800 areaGreatestY = 0,
801 arrayAlternate = 'even';
802
803 for (var i = 0; i < coords.length; i++) {
804
805 var areaNumber = coords[i];
806
807 if (arrayAlternate == 'even') {
808
809 if (areaNumber > areaGreatestX) {
810
811 areaGreatestX = areaNumber;
812
813 if (i === 0) {
814 areaSmallestX = areaGreatestX;
815 }
816 }
817
818 if (areaNumber < areaSmallestX) {
819 areaSmallestX = areaNumber;
820 }
821
822 arrayAlternate = 'odd';
823 }
824 else {
825 if (areaNumber > areaGreatestY) {
826
827 areaGreatestY = areaNumber;
828
829 if (i == 1) {
830 areaSmallestY = areaGreatestY;
831 }
832 }
833
834 if (areaNumber < areaSmallestY) {
835 areaSmallestY = areaNumber;
836 }
837
838 arrayAlternate = 'even';
839 }
840 }
841
842 geo.origin.size.height = areaGreatestY - areaSmallestY;
843 geo.origin.size.width = areaGreatestX - areaSmallestX;
844
845 geo.origin.windowOffset.left += areaSmallestX;
846 geo.origin.windowOffset.top += areaSmallestY;
847
848 break;
849 }
850 }
851 }
852
853 // user callback through an event
854 var edit = function(r) {
855 geo.origin.size.height = r.height,
856 geo.origin.windowOffset.left = r.left,
857 geo.origin.windowOffset.top = r.top,
858 geo.origin.size.width = r.width
859 };
860
861 self._trigger({
862 type: 'geometry',
863 edit: edit,
864 geometry: {
865 height: geo.origin.size.height,
866 left: geo.origin.windowOffset.left,
867 top: geo.origin.windowOffset.top,
868 width: geo.origin.size.width
869 }
870 });
871
872 // calculate the remaining properties with what we got
873
874 geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width;
875 geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height;
876
877 geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left;
878 geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top;
879 geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height;
880 geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width;
881
882 // the space that is available to display the tooltip relatively to the document
883 geo.available.document = {
884 bottom: {
885 height: geo.document.size.height - geo.origin.offset.bottom,
886 width: geo.document.size.width
887 },
888 left: {
889 height: geo.document.size.height,
890 width: geo.origin.offset.left
891 },
892 right: {
893 height: geo.document.size.height,
894 width: geo.document.size.width - geo.origin.offset.right
895 },
896 top: {
897 height: geo.origin.offset.top,
898 width: geo.document.size.width
899 }
900 };
901
902 // the space that is available to display the tooltip relatively to the viewport
903 // (the resulting values may be negative if the origin overflows the viewport)
904 geo.available.window = {
905 bottom: {
906 // the inner max is here to make sure the available height is no bigger
907 // than the viewport height (when the origin is off screen at the top).
908 // The outer max just makes sure that the height is not negative (when
909 // the origin overflows at the bottom).
910 height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0),
911 width: geo.window.size.width
912 },
913 left: {
914 height: geo.window.size.height,
915 width: Math.max(geo.origin.windowOffset.left, 0)
916 },
917 right: {
918 height: geo.window.size.height,
919 width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0)
920 },
921 top: {
922 height: Math.max(geo.origin.windowOffset.top, 0),
923 width: geo.window.size.width
924 }
925 };
926
927 while ($parent[0].tagName.toLowerCase() != 'html') {
928
929 if ($parent.css('position') == 'fixed') {
930 geo.origin.fixedLineage = true;
931 break;
932 }
933
934 $parent = $parent.parent();
935 }
936
937 return geo;
938 },
939
940 /**
941 * Some options may need to be formated before being used
942 *
943 * @returns {self}
944 * @private
945 */
946 __optionsFormat: function() {
947
948 if (typeof this.__options.animationDuration == 'number') {
949 this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration];
950 }
951
952 if (typeof this.__options.delay == 'number') {
953 this.__options.delay = [this.__options.delay, this.__options.delay];
954 }
955
956 if (typeof this.__options.delayTouch == 'number') {
957 this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch];
958 }
959
960 if (typeof this.__options.theme == 'string') {
961 this.__options.theme = [this.__options.theme];
962 }
963
964 // determine the future parent
965 if (this.__options.parent === null) {
966 this.__options.parent = $(env.window.document.body);
967 }
968 else if (typeof this.__options.parent == 'string') {
969 this.__options.parent = $(this.__options.parent);
970 }
971
972 if (this.__options.trigger == 'hover') {
973
974 this.__options.triggerOpen = {
975 mouseenter: true,
976 touchstart: true
977 };
978
979 this.__options.triggerClose = {
980 mouseleave: true,
981 originClick: true,
982 touchleave: true
983 };
984 }
985 else if (this.__options.trigger == 'click') {
986
987 this.__options.triggerOpen = {
988 click: true,
989 tap: true
990 };
991
992 this.__options.triggerClose = {
993 click: true,
994 tap: true
995 };
996 }
997
998 // for the plugins
999 this._trigger('options');
1000
1001 return this;
1002 },
1003
1004 /**
1005 * Schedules or cancels the garbage collector task
1006 *
1007 * @returns {self}
1008 * @private
1009 */
1010 __prepareGC: function() {
1011
1012 var self = this;
1013
1014 // in case the selfDestruction option has been changed by a method call
1015 if (self.__options.selfDestruction) {
1016
1017 // the GC task
1018 self.__garbageCollector = setInterval(function() {
1019
1020 var now = new Date().getTime();
1021
1022 // forget the old events
1023 self.__touchEvents = $.grep(self.__touchEvents, function(event, i) {
1024 // 1 minute
1025 return now - event.time > 60000;
1026 });
1027
1028 // auto-destruct if the origin is gone
1029 if (!bodyContains(self._$origin)) {
1030
1031 self.close(function(){
1032 self.destroy();
1033 });
1034 }
1035 }, 20000);
1036 }
1037 else {
1038 clearInterval(self.__garbageCollector);
1039 }
1040
1041 return self;
1042 },
1043
1044 /**
1045 * Sets listeners on the origin if the open triggers require them.
1046 * Unlike the listeners set at opening time, these ones
1047 * remain even when the tooltip is closed. It has been made a
1048 * separate method so it can be called when the triggers are
1049 * changed in the options. Closing is handled in _open()
1050 * because of the bindings that may be needed on the tooltip
1051 * itself
1052 *
1053 * @returns {self}
1054 * @private
1055 */
1056 __prepareOrigin: function() {
1057
1058 var self = this;
1059
1060 // in case we're resetting the triggers
1061 self._$origin.off('.'+ self.__namespace +'-triggerOpen');
1062
1063 // if the device is touch capable, even if only mouse triggers
1064 // are asked, we need to listen to touch events to know if the mouse
1065 // events are actually emulated (so we can ignore them)
1066 if (env.hasTouchCapability) {
1067
1068 self._$origin.on(
1069 'touchstart.'+ self.__namespace +'-triggerOpen ' +
1070 'touchend.'+ self.__namespace +'-triggerOpen ' +
1071 'touchcancel.'+ self.__namespace +'-triggerOpen',
1072 function(event){
1073 self._touchRecordEvent(event);
1074 }
1075 );
1076 }
1077
1078 // mouse click and touch tap work the same way
1079 if ( self.__options.triggerOpen.click
1080 || (self.__options.triggerOpen.tap && env.hasTouchCapability)
1081 ) {
1082
1083 var eventNames = '';
1084 if (self.__options.triggerOpen.click) {
1085 eventNames += 'click.'+ self.__namespace +'-triggerOpen ';
1086 }
1087 if (self.__options.triggerOpen.tap && env.hasTouchCapability) {
1088 eventNames += 'touchend.'+ self.__namespace +'-triggerOpen';
1089 }
1090
1091 self._$origin.on(eventNames, function(event) {
1092 if (self._touchIsMeaningfulEvent(event)) {
1093 self._open(event);
1094 }
1095 });
1096 }
1097
1098 // mouseenter and touch start work the same way
1099 if ( self.__options.triggerOpen.mouseenter
1100 || (self.__options.triggerOpen.touchstart && env.hasTouchCapability)
1101 ) {
1102
1103 var eventNames = '';
1104 if (self.__options.triggerOpen.mouseenter) {
1105 eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen ';
1106 }
1107 if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) {
1108 eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen';
1109 }
1110
1111 self._$origin.on(eventNames, function(event) {
1112 if ( self._touchIsTouchEvent(event)
1113 || !self._touchIsEmulatedEvent(event)
1114 ) {
1115 self.__pointerIsOverOrigin = true;
1116 self._openShortly(event);
1117 }
1118 });
1119 }
1120
1121 // info for the mouseleave/touchleave close triggers when they use a delay
1122 if ( self.__options.triggerClose.mouseleave
1123 || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
1124 ) {
1125
1126 var eventNames = '';
1127 if (self.__options.triggerClose.mouseleave) {
1128 eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen ';
1129 }
1130 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
1131 eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen';
1132 }
1133
1134 self._$origin.on(eventNames, function(event) {
1135
1136 if (self._touchIsMeaningfulEvent(event)) {
1137 self.__pointerIsOverOrigin = false;
1138 }
1139 });
1140 }
1141
1142 return self;
1143 },
1144
1145 /**
1146 * Do the things that need to be done only once after the tooltip
1147 * HTML element it has been created. It has been made a separate
1148 * method so it can be called when options are changed. Remember
1149 * that the tooltip may actually exist in the DOM before it is
1150 * opened, and present after it has been closed: it's the display
1151 * plugin that takes care of handling it.
1152 *
1153 * @returns {self}
1154 * @private
1155 */
1156 __prepareTooltip: function() {
1157
1158 var self = this,
1159 p = self.__options.interactive ? 'auto' : '';
1160
1161 // this will be useful to know quickly if the tooltip is in
1162 // the DOM or not
1163 self._$tooltip
1164 .attr('id', self.__namespace)
1165 .css({
1166 // pointer events
1167 'pointer-events': p,
1168 zIndex: self.__options.zIndex
1169 });
1170
1171 // themes
1172 // remove the old ones and add the new ones
1173 $.each(self.__previousThemes, function(i, theme) {
1174 self._$tooltip.removeClass(theme);
1175 });
1176 $.each(self.__options.theme, function(i, theme) {
1177 self._$tooltip.addClass(theme);
1178 });
1179
1180 self.__previousThemes = $.merge([], self.__options.theme);
1181
1182 return self;
1183 },
1184
1185 /**
1186 * Handles the scroll on any of the parents of the origin (when the
1187 * tooltip is open)
1188 *
1189 * @param {object} event
1190 * @returns {self}
1191 * @private
1192 */
1193 __scrollHandler: function(event) {
1194
1195 var self = this;
1196
1197 if (self.__options.triggerClose.scroll) {
1198 self._close(event);
1199 }
1200 else {
1201
1202 // if the origin or tooltip have been removed: do nothing, the tracker will
1203 // take care of it later
1204 if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) {
1205
1206 var geo = null;
1207
1208 // if the scroll happened on the window
1209 if (event.target === env.window.document) {
1210
1211 // if the origin has a fixed lineage, window scroll will have no
1212 // effect on its position nor on the position of the tooltip
1213 if (!self.__Geometry.origin.fixedLineage) {
1214
1215 // we don't need to do anything unless repositionOnScroll is true
1216 // because the tooltip will already have moved with the window
1217 // (and of course with the origin)
1218 if (self.__options.repositionOnScroll) {
1219 self.reposition(event);
1220 }
1221 }
1222 }
1223 // if the scroll happened on another parent of the tooltip, it means
1224 // that it's in a scrollable area and now needs to have its position
1225 // adjusted or recomputed, depending ont the repositionOnScroll
1226 // option. Also, if the origin is partly hidden due to a parent that
1227 // hides its overflow, we'll just hide (not close) the tooltip.
1228 else {
1229
1230 geo = self.__geometry();
1231
1232 var overflows = false;
1233
1234 // a fixed position origin is not affected by the overflow hiding
1235 // of a parent
1236 if (self._$origin.css('position') != 'fixed') {
1237
1238 self.__$originParents.each(function(i, el) {
1239
1240 var $el = $(el),
1241 overflowX = $el.css('overflow-x'),
1242 overflowY = $el.css('overflow-y');
1243
1244 if (overflowX != 'visible' || overflowY != 'visible') {
1245
1246 var bcr = el.getBoundingClientRect();
1247
1248 if (overflowX != 'visible') {
1249
1250 if ( geo.origin.windowOffset.left < bcr.left
1251 || geo.origin.windowOffset.right > bcr.right
1252 ) {
1253 overflows = true;
1254 return false;
1255 }
1256 }
1257
1258 if (overflowY != 'visible') {
1259
1260 if ( geo.origin.windowOffset.top < bcr.top
1261 || geo.origin.windowOffset.bottom > bcr.bottom
1262 ) {
1263 overflows = true;
1264 return false;
1265 }
1266 }
1267 }
1268
1269 // no need to go further if fixed, for the same reason as above
1270 if ($el.css('position') == 'fixed') {
1271 return false;
1272 }
1273 });
1274 }
1275
1276 if (overflows) {
1277 self._$tooltip.css('visibility', 'hidden');
1278 }
1279 else {
1280
1281 self._$tooltip.css('visibility', 'visible');
1282
1283 // reposition
1284 if (self.__options.repositionOnScroll) {
1285 self.reposition(event);
1286 }
1287 // or just adjust offset
1288 else {
1289
1290 // we have to use offset and not windowOffset because this way,
1291 // only the scroll distance of the scrollable areas are taken into
1292 // account (the scrolltop value of the main window must be
1293 // ignored since the tooltip already moves with it)
1294 var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left,
1295 offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top;
1296
1297 // add the offset to the position initially computed by the display plugin
1298 self._$tooltip.css({
1299 left: self.__lastPosition.coord.left + offsetLeft,
1300 top: self.__lastPosition.coord.top + offsetTop
1301 });
1302 }
1303 }
1304 }
1305
1306 self._trigger({
1307 type: 'scroll',
1308 event: event,
1309 geo: geo
1310 });
1311 }
1312 }
1313
1314 return self;
1315 },
1316
1317 /**
1318 * Changes the state of the tooltip
1319 *
1320 * @param {string} state
1321 * @returns {self}
1322 * @private
1323 */
1324 __stateSet: function(state) {
1325
1326 this.__state = state;
1327
1328 this._trigger({
1329 type: 'state',
1330 state: state
1331 });
1332
1333 return this;
1334 },
1335
1336 /**
1337 * Clear appearance timeouts
1338 *
1339 * @returns {self}
1340 * @private
1341 */
1342 __timeoutsClear: function() {
1343
1344 // there is only one possible open timeout: the delayed opening
1345 // when the mouseenter/touchstart open triggers are used
1346 clearTimeout(this.__timeouts.open);
1347 this.__timeouts.open = null;
1348
1349 // ... but several close timeouts: the delayed closing when the
1350 // mouseleave close trigger is used and the timer option
1351 $.each(this.__timeouts.close, function(i, timeout) {
1352 clearTimeout(timeout);
1353 });
1354 this.__timeouts.close = [];
1355
1356 return this;
1357 },
1358
1359 /**
1360 * Start the tracker that will make checks at regular intervals
1361 *
1362 * @returns {self}
1363 * @private
1364 */
1365 __trackerStart: function() {
1366
1367 var self = this,
1368 $content = self._$tooltip.find('.tooltipster-content');
1369
1370 // get the initial content size
1371 if (self.__options.trackTooltip) {
1372 self.__contentBcr = $content[0].getBoundingClientRect();
1373 }
1374
1375 self.__tracker = setInterval(function() {
1376
1377 // if the origin or tooltip elements have been removed.
1378 // Note: we could destroy the instance now if the origin has
1379 // been removed but we'll leave that task to our garbage collector
1380 if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) {
1381 self._close();
1382 }
1383 // if everything is alright
1384 else {
1385
1386 // compare the former and current positions of the origin to reposition
1387 // the tooltip if need be
1388 if (self.__options.trackOrigin) {
1389
1390 var g = self.__geometry(),
1391 identical = false;
1392
1393 // compare size first (a change requires repositioning too)
1394 if (areEqual(g.origin.size, self.__Geometry.origin.size)) {
1395
1396 // for elements that have a fixed lineage (see __geometry()), we track the
1397 // top and left properties (relative to window)
1398 if (self.__Geometry.origin.fixedLineage) {
1399 if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) {
1400 identical = true;
1401 }
1402 }
1403 // otherwise, track total offset (relative to document)
1404 else {
1405 if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) {
1406 identical = true;
1407 }
1408 }
1409 }
1410
1411 if (!identical) {
1412
1413 // close the tooltip when using the mouseleave close trigger
1414 // (see https://github.com/iamceege/tooltipster/pull/253)
1415 if (self.__options.triggerClose.mouseleave) {
1416 self._close();
1417 }
1418 else {
1419 self.reposition();
1420 }
1421 }
1422 }
1423
1424 if (self.__options.trackTooltip) {
1425
1426 var currentBcr = $content[0].getBoundingClientRect();
1427
1428 if ( currentBcr.height !== self.__contentBcr.height
1429 || currentBcr.width !== self.__contentBcr.width
1430 ) {
1431 self.reposition();
1432 self.__contentBcr = currentBcr;
1433 }
1434 }
1435 }
1436 }, self.__options.trackerInterval);
1437
1438 return self;
1439 },
1440
1441 /**
1442 * Closes the tooltip (after the closing delay)
1443 *
1444 * @param event
1445 * @param callback
1446 * @param force Set to true to override a potential refusal of the user's function
1447 * @returns {self}
1448 * @protected
1449 */
1450 _close: function(event, callback, force) {
1451
1452 var self = this,
1453 ok = true;
1454
1455 self._trigger({
1456 type: 'close',
1457 event: event,
1458 stop: function() {
1459 ok = false;
1460 }
1461 });
1462
1463 // a destroying tooltip (force == true) may not refuse to close
1464 if (ok || force) {
1465
1466 // save the method custom callback and cancel any open method custom callbacks
1467 if (callback) self.__callbacks.close.push(callback);
1468 self.__callbacks.open = [];
1469
1470 // clear open/close timeouts
1471 self.__timeoutsClear();
1472
1473 var finishCallbacks = function() {
1474
1475 // trigger any close method custom callbacks and reset them
1476 $.each(self.__callbacks.close, function(i,c) {
1477 c.call(self, self, {
1478 event: event,
1479 origin: self._$origin[0]
1480 });
1481 });
1482
1483 self.__callbacks.close = [];
1484 };
1485
1486 if (self.__state != 'closed') {
1487
1488 var necessary = true,
1489 d = new Date(),
1490 now = d.getTime(),
1491 newClosingTime = now + self.__options.animationDuration[1];
1492
1493 // the tooltip may already already be disappearing, but if a new
1494 // call to close() is made after the animationDuration was changed
1495 // to 0 (for example), we ought to actually close it sooner than
1496 // previously scheduled. In that case it should be noted that the
1497 // browser will not adapt the animation duration to the new
1498 // animationDuration that was set after the start of the closing
1499 // animation.
1500 // Note: the same thing could be considered at opening, but is not
1501 // really useful since the tooltip is actually opened immediately
1502 // upon a call to _open(). Since it would not make the opening
1503 // animation finish sooner, its sole impact would be to trigger the
1504 // state event and the open callbacks sooner than the actual end of
1505 // the opening animation, which is not great.
1506 if (self.__state == 'disappearing') {
1507
1508 if ( newClosingTime > self.__closingTime
1509 // in case closing is actually overdue because the script
1510 // execution was suspended. See #679
1511 && self.__options.animationDuration[1] > 0
1512 ) {
1513 necessary = false;
1514 }
1515 }
1516
1517 if (necessary) {
1518
1519 self.__closingTime = newClosingTime;
1520
1521 if (self.__state != 'disappearing') {
1522 self.__stateSet('disappearing');
1523 }
1524
1525 var finish = function() {
1526
1527 // stop the tracker
1528 clearInterval(self.__tracker);
1529
1530 // a "beforeClose" option has been asked several times but would
1531 // probably useless since the content element is still accessible
1532 // via ::content(), and because people can always use listeners
1533 // inside their content to track what's going on. For the sake of
1534 // simplicity, this has been denied. Bur for the rare people who
1535 // really need the option (for old browsers or for the case where
1536 // detaching the content is actually destructive, for file or
1537 // password inputs for example), this event will do the work.
1538 self._trigger({
1539 type: 'closing',
1540 event: event
1541 });
1542
1543 // unbind listeners which are no longer needed
1544
1545 self._$tooltip
1546 .off('.'+ self.__namespace +'-triggerClose')
1547 .removeClass('tooltipster-dying');
1548
1549 // orientationchange, scroll and resize listeners
1550 $(env.window).off('.'+ self.__namespace +'-triggerClose');
1551
1552 // scroll listeners
1553 self.__$originParents.each(function(i, el) {
1554 $(el).off('scroll.'+ self.__namespace +'-triggerClose');
1555 });
1556 // clear the array to prevent memory leaks
1557 self.__$originParents = null;
1558
1559 $(env.window.document.body).off('.'+ self.__namespace +'-triggerClose');
1560
1561 self._$origin.off('.'+ self.__namespace +'-triggerClose');
1562
1563 self._off('dismissable');
1564
1565 // a plugin that would like to remove the tooltip from the
1566 // DOM when closed should bind on this
1567 self.__stateSet('closed');
1568
1569 // trigger event
1570 self._trigger({
1571 type: 'after',
1572 event: event
1573 });
1574
1575 // call our constructor custom callback function
1576 if (self.__options.functionAfter) {
1577 self.__options.functionAfter.call(self, self, {
1578 event: event,
1579 origin: self._$origin[0]
1580 });
1581 }
1582
1583 // call our method custom callbacks functions
1584 finishCallbacks();
1585 };
1586
1587 if (env.hasTransitions) {
1588
1589 self._$tooltip.css({
1590 '-moz-animation-duration': self.__options.animationDuration[1] + 'ms',
1591 '-ms-animation-duration': self.__options.animationDuration[1] + 'ms',
1592 '-o-animation-duration': self.__options.animationDuration[1] + 'ms',
1593 '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms',
1594 'animation-duration': self.__options.animationDuration[1] + 'ms',
1595 'transition-duration': self.__options.animationDuration[1] + 'ms'
1596 });
1597
1598 self._$tooltip
1599 // clear both potential open and close tasks
1600 .clearQueue()
1601 .removeClass('tooltipster-show')
1602 // for transitions only
1603 .addClass('tooltipster-dying');
1604
1605 if (self.__options.animationDuration[1] > 0) {
1606 self._$tooltip.delay(self.__options.animationDuration[1]);
1607 }
1608
1609 self._$tooltip.queue(finish);
1610 }
1611 else {
1612
1613 self._$tooltip
1614 .stop()
1615 .fadeOut(self.__options.animationDuration[1], finish);
1616 }
1617 }
1618 }
1619 // if the tooltip is already closed, we still need to trigger
1620 // the method custom callbacks
1621 else {
1622 finishCallbacks();
1623 }
1624 }
1625
1626 return self;
1627 },
1628
1629 /**
1630 * For internal use by plugins, if needed
1631 *
1632 * @returns {self}
1633 * @protected
1634 */
1635 _off: function() {
1636 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
1637 return this;
1638 },
1639
1640 /**
1641 * For internal use by plugins, if needed
1642 *
1643 * @returns {self}
1644 * @protected
1645 */
1646 _on: function() {
1647 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
1648 return this;
1649 },
1650
1651 /**
1652 * For internal use by plugins, if needed
1653 *
1654 * @returns {self}
1655 * @protected
1656 */
1657 _one: function() {
1658 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
1659 return this;
1660 },
1661
1662 /**
1663 * Opens the tooltip right away.
1664 *
1665 * @param event
1666 * @param callback Will be called when the opening animation is over
1667 * @returns {self}
1668 * @protected
1669 */
1670 _open: function(event, callback) {
1671
1672 var self = this;
1673
1674 // if the destruction process has not begun and if this was not
1675 // triggered by an unwanted emulated click event
1676 if (!self.__destroying) {
1677
1678 // check that the origin is still in the DOM
1679 if ( bodyContains(self._$origin)
1680 // if the tooltip is enabled
1681 && self.__enabled
1682 ) {
1683
1684 var ok = true;
1685
1686 // if the tooltip is not open yet, we need to call functionBefore.
1687 // otherwise we can jst go on
1688 if (self.__state == 'closed') {
1689
1690 // trigger an event. The event.stop function allows the callback
1691 // to prevent the opening of the tooltip
1692 self._trigger({
1693 type: 'before',
1694 event: event,
1695 stop: function() {
1696 ok = false;
1697 }
1698 });
1699
1700 if (ok && self.__options.functionBefore) {
1701
1702 // call our custom function before continuing
1703 ok = self.__options.functionBefore.call(self, self, {
1704 event: event,
1705 origin: self._$origin[0]
1706 });
1707 }
1708 }
1709
1710 if (ok !== false) {
1711
1712 // if there is some content
1713 if (self.__Content !== null) {
1714
1715 // save the method callback and cancel close method callbacks
1716 if (callback) {
1717 self.__callbacks.open.push(callback);
1718 }
1719 self.__callbacks.close = [];
1720
1721 // get rid of any appearance timeouts
1722 self.__timeoutsClear();
1723
1724 var extraTime,
1725 finish = function() {
1726
1727 if (self.__state != 'stable') {
1728 self.__stateSet('stable');
1729 }
1730
1731 // trigger any open method custom callbacks and reset them
1732 $.each(self.__callbacks.open, function(i,c) {
1733 c.call(self, self, {
1734 origin: self._$origin[0],
1735 tooltip: self._$tooltip[0]
1736 });
1737 });
1738
1739 self.__callbacks.open = [];
1740 };
1741
1742 // if the tooltip is already open
1743 if (self.__state !== 'closed') {
1744
1745 // the timer (if any) will start (or restart) right now
1746 extraTime = 0;
1747
1748 // if it was disappearing, cancel that
1749 if (self.__state === 'disappearing') {
1750
1751 self.__stateSet('appearing');
1752
1753 if (env.hasTransitions) {
1754
1755 self._$tooltip
1756 .clearQueue()
1757 .removeClass('tooltipster-dying')
1758 .addClass('tooltipster-show');
1759
1760 if (self.__options.animationDuration[0] > 0) {
1761 self._$tooltip.delay(self.__options.animationDuration[0]);
1762 }
1763
1764 self._$tooltip.queue(finish);
1765 }
1766 else {
1767 // in case the tooltip was currently fading out, bring it back
1768 // to life
1769 self._$tooltip
1770 .stop()
1771 .fadeIn(finish);
1772 }
1773 }
1774 // if the tooltip is already open, we still need to trigger the method
1775 // custom callback
1776 else if (self.__state == 'stable') {
1777 finish();
1778 }
1779 }
1780 // if the tooltip isn't already open, open it
1781 else {
1782
1783 // a plugin must bind on this and store the tooltip in this._$tooltip
1784 self.__stateSet('appearing');
1785
1786 // the timer (if any) will start when the tooltip has fully appeared
1787 // after its transition
1788 extraTime = self.__options.animationDuration[0];
1789
1790 // insert the content inside the tooltip
1791 self.__contentInsert();
1792
1793 // reposition the tooltip and attach to the DOM
1794 self.reposition(event, true);
1795
1796 // animate in the tooltip. If the display plugin wants no css
1797 // animations, it may override the animation option with a
1798 // dummy value that will produce no effect
1799 if (env.hasTransitions) {
1800
1801 // note: there seems to be an issue with start animations which
1802 // are randomly not played on fast devices in both Chrome and FF,
1803 // couldn't find a way to solve it yet. It seems that applying
1804 // the classes before appending to the DOM helps a little, but
1805 // it messes up some CSS transitions. The issue almost never
1806 // happens when delay[0]==0 though
1807 self._$tooltip
1808 .addClass('tooltipster-'+ self.__options.animation)
1809 .addClass('tooltipster-initial')
1810 .css({
1811 '-moz-animation-duration': self.__options.animationDuration[0] + 'ms',
1812 '-ms-animation-duration': self.__options.animationDuration[0] + 'ms',
1813 '-o-animation-duration': self.__options.animationDuration[0] + 'ms',
1814 '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms',
1815 'animation-duration': self.__options.animationDuration[0] + 'ms',
1816 'transition-duration': self.__options.animationDuration[0] + 'ms'
1817 });
1818
1819 setTimeout(
1820 function() {
1821
1822 // a quick hover may have already triggered a mouseleave
1823 if (self.__state != 'closed') {
1824
1825 self._$tooltip
1826 .addClass('tooltipster-show')
1827 .removeClass('tooltipster-initial');
1828
1829 if (self.__options.animationDuration[0] > 0) {
1830 self._$tooltip.delay(self.__options.animationDuration[0]);
1831 }
1832
1833 self._$tooltip.queue(finish);
1834 }
1835 },
1836 0
1837 );
1838 }
1839 else {
1840
1841 // old browsers will have to live with this
1842 self._$tooltip
1843 .css('display', 'none')
1844 .fadeIn(self.__options.animationDuration[0], finish);
1845 }
1846
1847 // checks if the origin is removed while the tooltip is open
1848 self.__trackerStart();
1849
1850 // NOTE: the listeners below have a '-triggerClose' namespace
1851 // because we'll remove them when the tooltip closes (unlike
1852 // the '-triggerOpen' listeners). So some of them are actually
1853 // not about close triggers, rather about positioning.
1854
1855 $(env.window)
1856 // reposition on resize
1857 .on('resize.'+ self.__namespace +'-triggerClose', function(e) {
1858
1859 var $ae = $(document.activeElement);
1860
1861 // reposition only if the resize event was not triggered upon the opening
1862 // of a virtual keyboard due to an input field being focused within the tooltip
1863 // (otherwise the repositioning would lose the focus)
1864 if ( (!$ae.is('input') && !$ae.is('textarea'))
1865 || !$.contains(self._$tooltip[0], $ae[0])
1866 ) {
1867 self.reposition(e);
1868 }
1869 })
1870 // same as below for parents
1871 .on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
1872 self.__scrollHandler(e);
1873 });
1874
1875 self.__$originParents = self._$origin.parents();
1876
1877 // scrolling may require the tooltip to be moved or even
1878 // repositioned in some cases
1879 self.__$originParents.each(function(i, parent) {
1880
1881 $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
1882 self.__scrollHandler(e);
1883 });
1884 });
1885
1886 if ( self.__options.triggerClose.mouseleave
1887 || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
1888 ) {
1889
1890 // we use an event to allow users/plugins to control when the mouseleave/touchleave
1891 // close triggers will come to action. It allows to have more triggering elements
1892 // than just the origin and the tooltip for example, or to cancel/delay the closing,
1893 // or to make the tooltip interactive even if it wasn't when it was open, etc.
1894 self._on('dismissable', function(event) {
1895
1896 if (event.dismissable) {
1897
1898 if (event.delay) {
1899
1900 timeout = setTimeout(function() {
1901 // event.event may be undefined
1902 self._close(event.event);
1903 }, event.delay);
1904
1905 self.__timeouts.close.push(timeout);
1906 }
1907 else {
1908 self._close(event);
1909 }
1910 }
1911 else {
1912 clearTimeout(timeout);
1913 }
1914 });
1915
1916 // now set the listeners that will trigger 'dismissable' events
1917 var $elements = self._$origin,
1918 eventNamesIn = '',
1919 eventNamesOut = '',
1920 timeout = null;
1921
1922 // if we have to allow interaction, bind on the tooltip too
1923 if (self.__options.interactive) {
1924 $elements = $elements.add(self._$tooltip);
1925 }
1926
1927 if (self.__options.triggerClose.mouseleave) {
1928 eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose ';
1929 eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose ';
1930 }
1931 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
1932 eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose';
1933 eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose';
1934 }
1935
1936 $elements
1937 // close after some time spent outside of the elements
1938 .on(eventNamesOut, function(event) {
1939
1940 // it's ok if the touch gesture ended up to be a swipe,
1941 // it's still a "touch leave" situation
1942 if ( self._touchIsTouchEvent(event)
1943 || !self._touchIsEmulatedEvent(event)
1944 ) {
1945
1946 var delay = (event.type == 'mouseleave') ?
1947 self.__options.delay :
1948 self.__options.delayTouch;
1949
1950 self._trigger({
1951 delay: delay[1],
1952 dismissable: true,
1953 event: event,
1954 type: 'dismissable'
1955 });
1956 }
1957 })
1958 // suspend the mouseleave timeout when the pointer comes back
1959 // over the elements
1960 .on(eventNamesIn, function(event) {
1961
1962 // it's also ok if the touch event is a swipe gesture
1963 if ( self._touchIsTouchEvent(event)
1964 || !self._touchIsEmulatedEvent(event)
1965 ) {
1966 self._trigger({
1967 dismissable: false,
1968 event: event,
1969 type: 'dismissable'
1970 });
1971 }
1972 });
1973 }
1974
1975 // close the tooltip when the origin gets a mouse click (common behavior of
1976 // native tooltips)
1977 if (self.__options.triggerClose.originClick) {
1978
1979 self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) {
1980
1981 // we could actually let a tap trigger this but this feature just
1982 // does not make sense on touch devices
1983 if ( !self._touchIsTouchEvent(event)
1984 && !self._touchIsEmulatedEvent(event)
1985 ) {
1986 self._close(event);
1987 }
1988 });
1989 }
1990
1991 // set the same bindings for click and touch on the body to close the tooltip
1992 if ( self.__options.triggerClose.click
1993 || (self.__options.triggerClose.tap && env.hasTouchCapability)
1994 ) {
1995
1996 // don't set right away since the click/tap event which triggered this method
1997 // (if it was a click/tap) is going to bubble up to the body, we don't want it
1998 // to close the tooltip immediately after it opened
1999 setTimeout(function() {
2000
2001 if (self.__state != 'closed') {
2002
2003 var eventNames = '',
2004 $body = $(env.window.document.body);
2005
2006 if (self.__options.triggerClose.click) {
2007 eventNames += 'click.'+ self.__namespace +'-triggerClose ';
2008 }
2009 if (self.__options.triggerClose.tap && env.hasTouchCapability) {
2010 eventNames += 'touchend.'+ self.__namespace +'-triggerClose';
2011 }
2012
2013 $body.on(eventNames, function(event) {
2014
2015 if (self._touchIsMeaningfulEvent(event)) {
2016
2017 self._touchRecordEvent(event);
2018
2019 if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) {
2020 self._close(event);
2021 }
2022 }
2023 });
2024
2025 // needed to detect and ignore swiping
2026 if (self.__options.triggerClose.tap && env.hasTouchCapability) {
2027
2028 $body.on('touchstart.'+ self.__namespace +'-triggerClose', function(event) {
2029 self._touchRecordEvent(event);
2030 });
2031 }
2032 }
2033 }, 0);
2034 }
2035
2036 self._trigger('ready');
2037
2038 // call our custom callback
2039 if (self.__options.functionReady) {
2040 self.__options.functionReady.call(self, self, {
2041 origin: self._$origin[0],
2042 tooltip: self._$tooltip[0]
2043 });
2044 }
2045 }
2046
2047 // if we have a timer set, let the countdown begin
2048 if (self.__options.timer > 0) {
2049
2050 var timeout = setTimeout(function() {
2051 self._close();
2052 }, self.__options.timer + extraTime);
2053
2054 self.__timeouts.close.push(timeout);
2055 }
2056 }
2057 }
2058 }
2059 }
2060
2061 return self;
2062 },
2063
2064 /**
2065 * When using the mouseenter/touchstart open triggers, this function will
2066 * schedule the opening of the tooltip after the delay, if there is one
2067 *
2068 * @param event
2069 * @returns {self}
2070 * @protected
2071 */
2072 _openShortly: function(event) {
2073
2074 var self = this,
2075 ok = true;
2076
2077 if (self.__state != 'stable' && self.__state != 'appearing') {
2078
2079 // if a timeout is not already running
2080 if (!self.__timeouts.open) {
2081
2082 self._trigger({
2083 type: 'start',
2084 event: event,
2085 stop: function() {
2086 ok = false;
2087 }
2088 });
2089
2090 if (ok) {
2091
2092 var delay = (event.type.indexOf('touch') == 0) ?
2093 self.__options.delayTouch :
2094 self.__options.delay;
2095
2096 if (delay[0]) {
2097
2098 self.__timeouts.open = setTimeout(function() {
2099
2100 self.__timeouts.open = null;
2101
2102 // open only if the pointer (mouse or touch) is still over the origin.
2103 // The check on the "meaningful event" can only be made here, after some
2104 // time has passed (to know if the touch was a swipe or not)
2105 if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) {
2106
2107 // signal that we go on
2108 self._trigger('startend');
2109
2110 self._open(event);
2111 }
2112 else {
2113 // signal that we cancel
2114 self._trigger('startcancel');
2115 }
2116 }, delay[0]);
2117 }
2118 else {
2119 // signal that we go on
2120 self._trigger('startend');
2121
2122 self._open(event);
2123 }
2124 }
2125 }
2126 }
2127
2128 return self;
2129 },
18 2130
19 2131 /**
20 * TOOLTIP IMPL.
2132 * Meant for plugins to get their options
2133 *
2134 * @param {string} pluginName The name of the plugin that asks for its options
2135 * @param {object} defaultOptions The default options of the plugin
2136 * @returns {object} The options
2137 * @protected
2138 */
2139 _optionsExtract: function(pluginName, defaultOptions) {
2140
2141 var self = this,
2142 options = $.extend(true, {}, defaultOptions);
2143
2144 // if the plugin options were isolated in a property named after the
2145 // plugin, use them (prevents conflicts with other plugins)
2146 var pluginOptions = self.__options[pluginName];
2147
2148 // if not, try to get them as regular options
2149 if (!pluginOptions){
2150
2151 pluginOptions = {};
2152
2153 $.each(defaultOptions, function(optionName, value) {
2154
2155 var o = self.__options[optionName];
2156
2157 if (o !== undefined) {
2158 pluginOptions[optionName] = o;
2159 }
2160 });
2161 }
2162
2163 // let's merge the default options and the ones that were provided. We'd want
2164 // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow
2165 // extend on two levels, that will be enough if options are not more than 1
2166 // level deep
2167 $.each(options, function(optionName, value) {
2168
2169 if (pluginOptions[optionName] !== undefined) {
2170
2171 if (( typeof value == 'object'
2172 && !(value instanceof Array)
2173 && value != null
2174 )
2175 &&
2176 ( typeof pluginOptions[optionName] == 'object'
2177 && !(pluginOptions[optionName] instanceof Array)
2178 && pluginOptions[optionName] != null
2179 )
2180 ) {
2181 $.extend(options[optionName], pluginOptions[optionName]);
2182 }
2183 else {
2184 options[optionName] = pluginOptions[optionName];
2185 }
2186 }
2187 });
2188
2189 return options;
2190 },
2191
2192 /**
2193 * Used at instantiation of the plugin, or afterwards by plugins that activate themselves
2194 * on existing instances
2195 *
2196 * @param {object} pluginName
2197 * @returns {self}
2198 * @protected
2199 */
2200 _plug: function(pluginName) {
2201
2202 var plugin = $.tooltipster._plugin(pluginName);
2203
2204 if (plugin) {
2205
2206 // if there is a constructor for instances
2207 if (plugin.instance) {
2208
2209 // proxy non-private methods on the instance to allow new instance methods
2210 $.tooltipster.__bridge(plugin.instance, this, plugin.name);
2211 }
2212 }
2213 else {
2214 throw new Error('The "'+ pluginName +'" plugin is not defined');
2215 }
2216
2217 return this;
2218 },
2219
2220 /**
2221 * This will return true if the event is a mouse event which was
2222 * emulated by the browser after a touch event. This allows us to
2223 * really dissociate mouse and touch triggers.
2224 *
2225 * There is a margin of error if a real mouse event is fired right
2226 * after (within the delay shown below) a touch event on the same
2227 * element, but hopefully it should not happen often.
2228 *
2229 * @returns {boolean}
2230 * @protected
2231 */
2232 _touchIsEmulatedEvent: function(event) {
2233
2234 var isEmulated = false,
2235 now = new Date().getTime();
2236
2237 for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
2238
2239 var e = this.__touchEvents[i];
2240
2241 // delay, in milliseconds. It's supposed to be 300ms in
2242 // most browsers (350ms on iOS) to allow a double tap but
2243 // can be less (check out FastClick for more info)
2244 if (now - e.time < 500) {
2245
2246 if (e.target === event.target) {
2247 isEmulated = true;
2248 }
2249 }
2250 else {
2251 break;
2252 }
2253 }
2254
2255 return isEmulated;
2256 },
2257
2258 /**
2259 * Returns false if the event was an emulated mouse event or
2260 * a touch event involved in a swipe gesture.
2261 *
2262 * @param {object} event
2263 * @returns {boolean}
2264 * @protected
21 2265 */
22
23 var TTIP = {};
24
25 TTIP.main = {
26 offset: [15,15],
27 maxWidth: 600,
28
29 setDeferredListeners: function(){
30 $('body').on('mouseover', '.tooltip', yt.show_tip);
31 $('body').on('mousemove', '.tooltip', yt.move_tip);
32 $('body').on('mouseout', '.tooltip', yt.close_tip);
2266 _touchIsMeaningfulEvent: function(event) {
2267 return (
2268 (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target))
2269 || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event))
2270 );
2271 },
2272
2273 /**
2274 * Checks if an event is a touch event
2275 *
2276 * @param {object} event
2277 * @returns {boolean}
2278 * @protected
2279 */
2280 _touchIsTouchEvent: function(event){
2281 return event.type.indexOf('touch') == 0;
2282 },
2283
2284 /**
2285 * Store touch events for a while to detect swiping and emulated mouse events
2286 *
2287 * @param {object} event
2288 * @returns {self}
2289 * @protected
2290 */
2291 _touchRecordEvent: function(event) {
2292
2293 if (this._touchIsTouchEvent(event)) {
2294 event.time = new Date().getTime();
2295 this.__touchEvents.push(event);
2296 }
2297
2298 return this;
2299 },
2300
2301 /**
2302 * Returns true if a swipe happened after the last touchstart event fired on
2303 * event.target.
2304 *
2305 * We need to differentiate a swipe from a tap before we let the event open
2306 * or close the tooltip. A swipe is when a touchmove (scroll) event happens
2307 * on the body between the touchstart and the touchend events of an element.
2308 *
2309 * @param {object} target The HTML element that may have triggered the swipe
2310 * @returns {boolean}
2311 * @protected
2312 */
2313 _touchSwiped: function(target) {
2314
2315 var swiped = false;
2316
2317 for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
2318
2319 var e = this.__touchEvents[i];
2320
2321 if (e.type == 'touchmove') {
2322 swiped = true;
2323 break;
2324 }
2325 else if (
2326 e.type == 'touchstart'
2327 && target === e.target
2328 ) {
2329 break;
2330 }
2331 }
2332
2333 return swiped;
2334 },
2335
2336 /**
2337 * Triggers an event on the instance emitters
2338 *
2339 * @returns {self}
2340 * @protected
2341 */
2342 _trigger: function() {
2343
2344 var args = Array.prototype.slice.apply(arguments);
2345
2346 if (typeof args[0] == 'string') {
2347 args[0] = { type: args[0] };
2348 }
2349
2350 // add properties to the event
2351 args[0].instance = this;
2352 args[0].origin = this._$origin ? this._$origin[0] : null;
2353 args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null;
2354
2355 // note: the order of emitters matters
2356 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
2357 $.tooltipster._trigger.apply($.tooltipster, args);
2358 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
2359
2360 return this;
2361 },
2362
2363 /**
2364 * Deactivate a plugin on this instance
2365 *
2366 * @returns {self}
2367 * @protected
2368 */
2369 _unplug: function(pluginName) {
2370
2371 var self = this;
2372
2373 // if the plugin has been activated on this instance
2374 if (self[pluginName]) {
2375
2376 var plugin = $.tooltipster._plugin(pluginName);
2377
2378 // if there is a constructor for instances
2379 if (plugin.instance) {
2380
2381 // unbridge
2382 $.each(plugin.instance, function(methodName, fn) {
2383
2384 // if the method exists (privates methods do not) and comes indeed from
2385 // this plugin (may be missing or come from a conflicting plugin).
2386 if ( self[methodName]
2387 && self[methodName].bridged === self[pluginName]
2388 ) {
2389 delete self[methodName];
2390 }
2391 });
2392 }
2393
2394 // destroy the plugin
2395 if (self[pluginName].__destroy) {
2396 self[pluginName].__destroy();
2397 }
2398
2399 // remove the reference to the plugin instance
2400 delete self[pluginName];
2401 }
2402
2403 return self;
2404 },
2405
2406 /**
2407 * @see self::_close
2408 * @returns {self}
2409 * @public
2410 */
2411 close: function(callback) {
2412
2413 if (!this.__destroyed) {
2414 this._close(null, callback);
2415 }
2416 else {
2417 this.__destroyError();
2418 }
2419
2420 return this;
33 2421 },
34 2422
35 init: function(){
36 $('#tip-box').remove();
37 yt.tipBox = document.createElement('div');
38 document.body.appendChild(yt.tipBox);
39 yt.tipBox.id = 'tip-box';
40
41 $(yt.tipBox).hide();
42 $(yt.tipBox).css('position', 'absolute');
43 if(yt.maxWidth !== null){
44 $(yt.tipBox).css('max-width', yt.maxWidth+'px');
45 }
46 yt.setDeferredListeners();
2423 /**
2424 * Sets or gets the content of the tooltip
2425 *
2426 * @returns {mixed|self}
2427 * @public
2428 */
2429 content: function(content) {
2430
2431 var self = this;
2432
2433 // getter method
2434 if (content === undefined) {
2435 return self.__Content;
2436 }
2437 // setter method
2438 else {
2439
2440 if (!self.__destroyed) {
2441
2442 // change the content
2443 self.__contentSet(content);
2444
2445 if (self.__Content !== null) {
2446
2447 // update the tooltip if it is open
2448 if (self.__state !== 'closed') {
2449
2450 // reset the content in the tooltip
2451 self.__contentInsert();
2452
2453 // reposition and resize the tooltip
2454 self.reposition();
2455
2456 // if we want to play a little animation showing the content changed
2457 if (self.__options.updateAnimation) {
2458
2459 if (env.hasTransitions) {
2460
2461 // keep the reference in the local scope
2462 var animation = self.__options.updateAnimation;
2463
2464 self._$tooltip.addClass('tooltipster-update-'+ animation);
2465
2466 // remove the class after a while. The actual duration of the
2467 // update animation may be shorter, it's set in the CSS rules
2468 setTimeout(function() {
2469
2470 if (self.__state != 'closed') {
2471
2472 self._$tooltip.removeClass('tooltipster-update-'+ animation);
2473 }
2474 }, 1000);
2475 }
2476 else {
2477 self._$tooltip.fadeTo(200, 0.5, function() {
2478 if (self.__state != 'closed') {
2479 self._$tooltip.fadeTo(200, 1);
2480 }
2481 });
2482 }
2483 }
2484 }
2485 }
2486 else {
2487 self._close();
2488 }
2489 }
2490 else {
2491 self.__destroyError();
2492 }
2493
2494 return self;
2495 }
2496 },
2497
2498 /**
2499 * Destroys the tooltip
2500 *
2501 * @returns {self}
2502 * @public
2503 */
2504 destroy: function() {
2505
2506 var self = this;
2507
2508 if (!self.__destroyed) {
2509
2510 if(self.__state != 'closed'){
2511
2512 // no closing delay
2513 self.option('animationDuration', 0)
2514 // force closing
2515 ._close(null, null, true);
2516 }
2517 else {
2518 // there might be an open timeout still running
2519 self.__timeoutsClear();
2520 }
2521
2522 // send event
2523 self._trigger('destroy');
2524
2525 self.__destroyed = true;
2526
2527 self._$origin
2528 .removeData(self.__namespace)
2529 // remove the open trigger listeners
2530 .off('.'+ self.__namespace +'-triggerOpen');
2531
2532 // remove the touch listener
2533 $(env.window.document.body).off('.' + self.__namespace +'-triggerOpen');
2534
2535 var ns = self._$origin.data('tooltipster-ns');
2536
2537 // if the origin has been removed from DOM, its data may
2538 // well have been destroyed in the process and there would
2539 // be nothing to clean up or restore
2540 if (ns) {
2541
2542 // if there are no more tooltips on this element
2543 if (ns.length === 1) {
2544
2545 // optional restoration of a title attribute
2546 var title = null;
2547 if (self.__options.restoration == 'previous') {
2548 title = self._$origin.data('tooltipster-initialTitle');
2549 }
2550 else if (self.__options.restoration == 'current') {
2551
2552 // old school technique to stringify when outerHTML is not supported
2553 title = (typeof self.__Content == 'string') ?
2554 self.__Content :
2555 $('<div></div>').append(self.__Content).html();
2556 }
2557
2558 if (title) {
2559 self._$origin.attr('title', title);
2560 }
2561
2562 // final cleaning
2563
2564 self._$origin.removeClass('tooltipstered');
2565
2566 self._$origin
2567 .removeData('tooltipster-ns')
2568 .removeData('tooltipster-initialTitle');
2569 }
2570 else {
2571 // remove the instance namespace from the list of namespaces of
2572 // tooltips present on the element
2573 ns = $.grep(ns, function(el, i) {
2574 return el !== self.__namespace;
2575 });
2576 self._$origin.data('tooltipster-ns', ns);
2577 }
2578 }
2579
2580 // last event
2581 self._trigger('destroyed');
2582
2583 // unbind private and public event listeners
2584 self._off();
2585 self.off();
2586
2587 // remove external references, just in case
2588 self.__Content = null;
2589 self.__$emitterPrivate = null;
2590 self.__$emitterPublic = null;
2591 self.__options.parent = null;
2592 self._$origin = null;
2593 self._$tooltip = null;
2594
2595 // make sure the object is no longer referenced in there to prevent
2596 // memory leaks
2597 $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) {
2598 return self !== el;
2599 });
2600
2601 clearInterval(self.__garbageCollector);
2602 }
2603 else {
2604 self.__destroyError();
2605 }
2606
2607 // we return the scope rather than true so that the call to
2608 // .tooltipster('destroy') actually returns the matched elements
2609 // and applies to all of them
2610 return self;
47 2611 },
48 2612
49 show_tip: function(e, el){
50 e.stopPropagation();
51 e.preventDefault();
52 var el = e.data || e.currentTarget || el;
53 if(el.tagName.toLowerCase() === 'img'){
54 yt.tipText = el.alt ? el.alt : '';
55 } else {
56 yt.tipText = el.title ? el.title : '';
57 }
58
59 if(yt.tipText !== ''){
60 // save org title
61 $(el).attr('tt_title', yt.tipText);
62 // reset title to not show org tooltips
63 $(el).attr('title', '');
64
65 yt.tipBox.innerHTML = yt.tipText;
66 $(yt.tipBox).show();
2613 /**
2614 * Disables the tooltip
2615 *
2616 * @returns {self}
2617 * @public
2618 */
2619 disable: function() {
2620
2621 if (!this.__destroyed) {
2622
2623 // close first, in case the tooltip would not disappear on
2624 // its own (no close trigger)
2625 this._close();
2626 this.__enabled = false;
2627
2628 return this;
2629 }
2630 else {
2631 this.__destroyError();
2632 }
2633
2634 return this;
2635 },
2636
2637 /**
2638 * Returns the HTML element of the origin
2639 *
2640 * @returns {self}
2641 * @public
2642 */
2643 elementOrigin: function() {
2644
2645 if (!this.__destroyed) {
2646 return this._$origin[0];
2647 }
2648 else {
2649 this.__destroyError();
2650 }
2651 },
2652
2653 /**
2654 * Returns the HTML element of the tooltip
2655 *
2656 * @returns {self}
2657 * @public
2658 */
2659 elementTooltip: function() {
2660 return this._$tooltip ? this._$tooltip[0] : null;
2661 },
2662
2663 /**
2664 * Enables the tooltip
2665 *
2666 * @returns {self}
2667 * @public
2668 */
2669 enable: function() {
2670 this.__enabled = true;
2671 return this;
2672 },
2673
2674 /**
2675 * Alias, deprecated in 4.0.0
2676 *
2677 * @param {function} callback
2678 * @returns {self}
2679 * @public
2680 */
2681 hide: function(callback) {
2682 return this.close(callback);
2683 },
2684
2685 /**
2686 * Returns the instance
2687 *
2688 * @returns {self}
2689 * @public
2690 */
2691 instance: function() {
2692 return this;
2693 },
2694
2695 /**
2696 * For public use only, not to be used by plugins (use ::_off() instead)
2697 *
2698 * @returns {self}
2699 * @public
2700 */
2701 off: function() {
2702
2703 if (!this.__destroyed) {
2704 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2705 }
2706
2707 return this;
2708 },
2709
2710 /**
2711 * For public use only, not to be used by plugins (use ::_on() instead)
2712 *
2713 * @returns {self}
2714 * @public
2715 */
2716 on: function() {
2717
2718 if (!this.__destroyed) {
2719 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2720 }
2721 else {
2722 this.__destroyError();
2723 }
2724
2725 return this;
2726 },
2727
2728 /**
2729 * For public use only, not to be used by plugins
2730 *
2731 * @returns {self}
2732 * @public
2733 */
2734 one: function() {
2735
2736 if (!this.__destroyed) {
2737 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2738 }
2739 else {
2740 this.__destroyError();
2741 }
2742
2743 return this;
2744 },
2745
2746 /**
2747 * @see self::_open
2748 * @returns {self}
2749 * @public
2750 */
2751 open: function(callback) {
2752
2753 if (!this.__destroyed) {
2754 this._open(null, callback);
2755 }
2756 else {
2757 this.__destroyError();
2758 }
2759
2760 return this;
2761 },
2762
2763 /**
2764 * Get or set options. For internal use and advanced users only.
2765 *
2766 * @param {string} o Option name
2767 * @param {mixed} val optional A new value for the option
2768 * @return {mixed|self} If val is omitted, the value of the option
2769 * is returned, otherwise the instance itself is returned
2770 * @public
2771 */
2772 option: function(o, val) {
2773
2774 // getter
2775 if (val === undefined) {
2776 return this.__options[o];
2777 }
2778 // setter
2779 else {
2780
2781 if (!this.__destroyed) {
2782
2783 // change value
2784 this.__options[o] = val;
2785
2786 // format
2787 this.__optionsFormat();
2788
2789 // re-prepare the triggers if needed
2790 if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) {
2791 this.__prepareOrigin();
2792 }
2793
2794 if (o === 'selfDestruction') {
2795 this.__prepareGC();
2796 }
2797 }
2798 else {
2799 this.__destroyError();
2800 }
2801
2802 return this;
67 2803 }
68 2804 },
69 2805
70 move_tip: function(e, el){
71 e.stopPropagation();
72 e.preventDefault();
73 var el = e.data || e.currentTarget || el;
74 var movePos = [e.pageX, e.pageY];
75 $(yt.tipBox).css('top', (movePos[1] + yt.offset[1]) + 'px')
76 $(yt.tipBox).css('left', (movePos[0] + yt.offset[0]) + 'px')
2806 /**
2807 * This method is in charge of setting the position and size properties of the tooltip.
2808 * All the hard work is delegated to the display plugin.
2809 * Note: The tooltip may be detached from the DOM at the moment the method is called
2810 * but must be attached by the end of the method call.
2811 *
2812 * @param {object} event For internal use only. Defined if an event such as
2813 * window resizing triggered the repositioning
2814 * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you
2815 * know that the tooltip not being in the DOM is not an issue (typically when the
2816 * tooltip element has just been created but has not been added to the DOM yet).
2817 * @returns {self}
2818 * @public
2819 */
2820 reposition: function(event, tooltipIsDetached) {
2821
2822 var self = this;
2823
2824 if (!self.__destroyed) {
2825
2826 // if the tooltip is still open and the origin is still in the DOM
2827 if (self.__state != 'closed' && bodyContains(self._$origin)) {
2828
2829 // if the tooltip has not been removed from DOM manually (or if it
2830 // has been detached on purpose)
2831 if (tooltipIsDetached || bodyContains(self._$tooltip)) {
2832
2833 if (!tooltipIsDetached) {
2834 // detach in case the tooltip overflows the window and adds
2835 // scrollbars to it, so __geometry can be accurate
2836 self._$tooltip.detach();
2837 }
2838
2839 // refresh the geometry object before passing it as a helper
2840 self.__Geometry = self.__geometry();
2841
2842 // let a plugin fo the rest
2843 self._trigger({
2844 type: 'reposition',
2845 event: event,
2846 helper: {
2847 geo: self.__Geometry
2848 }
2849 });
2850 }
2851 }
2852 }
2853 else {
2854 self.__destroyError();
2855 }
2856
2857 return self;
2858 },
2859
2860 /**
2861 * Alias, deprecated in 4.0.0
2862 *
2863 * @param callback
2864 * @returns {self}
2865 * @public
2866 */
2867 show: function(callback) {
2868 return this.open(callback);
2869 },
2870
2871 /**
2872 * Returns some properties about the instance
2873 *
2874 * @returns {object}
2875 * @public
2876 */
2877 status: function() {
2878
2879 return {
2880 destroyed: this.__destroyed,
2881 enabled: this.__enabled,
2882 open: this.__state !== 'closed',
2883 state: this.__state
2884 };
77 2885 },
78 2886
79 close_tip: function(e, el){
80 e.stopPropagation();
81 e.preventDefault();
82 var el = e.data || e.currentTarget || el;
83 $(yt.tipBox).hide();
84 $(el).attr('title', $(el).attr('tt_title'));
85 $('#tip-box').hide();
2887 /**
2888 * For public use only, not to be used by plugins
2889 *
2890 * @returns {self}
2891 * @public
2892 */
2893 triggerHandler: function() {
2894
2895 if (!this.__destroyed) {
2896 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2897 }
2898 else {
2899 this.__destroyError();
2900 }
2901
2902 return this;
2903 }
2904 };
2905
2906 $.fn.tooltipster = function() {
2907
2908 // for using in closures
2909 var args = Array.prototype.slice.apply(arguments),
2910 // common mistake: an HTML element can't be in several tooltips at the same time
2911 contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.';
2912
2913 // this happens with $(sel).tooltipster(...) when $(sel) does not match anything
2914 if (this.length === 0) {
2915
2916 // still chainable
2917 return this;
2918 }
2919 // this happens when calling $(sel).tooltipster('methodName or options')
2920 // where $(sel) matches one or more elements
2921 else {
2922
2923 // method calls
2924 if (typeof args[0] === 'string') {
2925
2926 var v = '#*$~&';
2927
2928 this.each(function() {
2929
2930 // retrieve the namepaces of the tooltip(s) that exist on that element.
2931 // We will interact with the first tooltip only.
2932 var ns = $(this).data('tooltipster-ns'),
2933 // self represents the instance of the first tooltipster plugin
2934 // associated to the current HTML object of the loop
2935 self = ns ? $(this).data(ns[0]) : null;
2936
2937 // if the current element holds a tooltipster instance
2938 if (self) {
2939
2940 if (typeof self[args[0]] === 'function') {
2941
2942 if ( this.length > 1
2943 && args[0] == 'content'
2944 && ( args[1] instanceof $
2945 || (typeof args[1] == 'object' && args[1] != null && args[1].tagName)
2946 )
2947 && !self.__options.contentCloning
2948 && self.__options.debug
2949 ) {
2950 console.log(contentCloningWarning);
2951 }
2952
2953 // note : args[1] and args[2] may not be defined
2954 var resp = self[args[0]](args[1], args[2]);
2955 }
2956 else {
2957 throw new Error('Unknown method "'+ args[0] +'"');
2958 }
2959
2960 // if the function returned anything other than the instance
2961 // itself (which implies chaining, except for the `instance` method)
2962 if (resp !== self || args[0] === 'instance') {
2963
2964 v = resp;
2965
2966 // return false to stop .each iteration on the first element
2967 // matched by the selector
2968 return false;
2969 }
2970 }
2971 else {
2972 throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element');
2973 }
2974 });
2975
2976 return (v !== '#*$~&') ? v : this;
2977 }
2978 // first argument is undefined or an object: the tooltip is initializing
2979 else {
2980
2981 // reset the array of last initialized objects
2982 $.tooltipster.__instancesLatestArr = [];
2983
2984 // is there a defined value for the multiple option in the options object ?
2985 var multipleIsSet = args[0] && args[0].multiple !== undefined,
2986 // if the multiple option is set to true, or if it's not defined but
2987 // set to true in the defaults
2988 multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple),
2989 // same for content
2990 contentIsSet = args[0] && args[0].content !== undefined,
2991 content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content),
2992 // same for contentCloning
2993 contentCloningIsSet = args[0] && args[0].contentCloning !== undefined,
2994 contentCloning =
2995 (contentCloningIsSet && args[0].contentCloning)
2996 || (!contentCloningIsSet && defaults.contentCloning),
2997 // same for debug
2998 debugIsSet = args[0] && args[0].debug !== undefined,
2999 debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug);
3000
3001 if ( this.length > 1
3002 && ( content instanceof $
3003 || (typeof content == 'object' && content != null && content.tagName)
3004 )
3005 && !contentCloning
3006 && debug
3007 ) {
3008 console.log(contentCloningWarning);
3009 }
3010
3011 // create a tooltipster instance for each element if it doesn't
3012 // already have one or if the multiple option is set, and attach the
3013 // object to it
3014 this.each(function() {
3015
3016 var go = false,
3017 $this = $(this),
3018 ns = $this.data('tooltipster-ns'),
3019 obj = null;
3020
3021 if (!ns) {
3022 go = true;
3023 }
3024 else if (multiple) {
3025 go = true;
3026 }
3027 else if (debug) {
3028 console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.');
3029 console.log(this);
3030 }
3031
3032 if (go) {
3033 obj = new $.Tooltipster(this, args[0]);
3034
3035 // save the reference of the new instance
3036 if (!ns) ns = [];
3037 ns.push(obj.__namespace);
3038 $this.data('tooltipster-ns', ns);
3039
3040 // save the instance itself
3041 $this.data(obj.__namespace, obj);
3042
3043 // call our constructor custom function.
3044 // we do this here and not in ::init() because we wanted
3045 // the object to be saved in $this.data before triggering
3046 // it
3047 if (obj.__options.functionInit) {
3048 obj.__options.functionInit.call(obj, obj, {
3049 origin: this
3050 });
3051 }
3052
3053 // and now the event, for the plugins and core emitter
3054 obj._trigger('init');
3055 }
3056
3057 $.tooltipster.__instancesLatestArr.push(obj);
3058 });
3059
3060 return this;
3061 }
3062 }
3063 };
3064
3065 // Utilities
3066
3067 /**
3068 * A class to check if a tooltip can fit in given dimensions
3069 *
3070 * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it
3071 */
3072 function Ruler($tooltip) {
3073
3074 // list of instance variables
3075
3076 this.$container;
3077 this.constraints = null;
3078 this.__$tooltip;
3079
3080 this.__init($tooltip);
3081 }
3082
3083 Ruler.prototype = {
3084
3085 /**
3086 * Move the tooltip into an invisible div that does not allow overflow to make
3087 * size tests. Note: the tooltip may or may not be attached to the DOM at the
3088 * moment this method is called, it does not matter.
3089 *
3090 * @param {object} $tooltip The object to test. May be just a clone of the
3091 * actual tooltip.
3092 * @private
3093 */
3094 __init: function($tooltip) {
3095
3096 this.__$tooltip = $tooltip;
3097
3098 this.__$tooltip
3099 .css({
3100 // for some reason we have to specify top and left 0
3101 left: 0,
3102 // any overflow will be ignored while measuring
3103 overflow: 'hidden',
3104 // positions at (0,0) without the div using 100% of the available width
3105 position: 'absolute',
3106 top: 0
3107 })
3108 // overflow must be auto during the test. We re-set this in case
3109 // it were modified by the user
3110 .find('.tooltipster-content')
3111 .css('overflow', 'auto');
3112
3113 this.$container = $('<div class="tooltipster-ruler"></div>')
3114 .append(this.__$tooltip)
3115 .appendTo(env.window.document.body);
3116 },
3117
3118 /**
3119 * Force the browser to redraw (re-render) the tooltip immediately. This is required
3120 * when you changed some CSS properties and need to make something with it
3121 * immediately, without waiting for the browser to redraw at the end of instructions.
3122 *
3123 * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
3124 * @private
3125 */
3126 __forceRedraw: function() {
3127
3128 // note: this would work but for Webkit only
3129 //this.__$tooltip.close();
3130 //this.__$tooltip[0].offsetHeight;
3131 //this.__$tooltip.open();
3132
3133 // works in FF too
3134 var $p = this.__$tooltip.parent();
3135 this.__$tooltip.detach();
3136 this.__$tooltip.appendTo($p);
3137 },
3138
3139 /**
3140 * Set maximum dimensions for the tooltip. A call to ::measure afterwards
3141 * will tell us if the content overflows or if it's ok
3142 *
3143 * @param {int} width
3144 * @param {int} height
3145 * @return {Ruler}
3146 * @public
3147 */
3148 constrain: function(width, height) {
3149
3150 this.constraints = {
3151 width: width,
3152 height: height
3153 };
3154
3155 this.__$tooltip.css({
3156 // we disable display:flex, otherwise the content would overflow without
3157 // creating horizontal scrolling (which we need to detect).
3158 display: 'block',
3159 // reset any previous height
3160 height: '',
3161 // we'll check if horizontal scrolling occurs
3162 overflow: 'auto',
3163 // we'll set the width and see what height is generated and if there
3164 // is horizontal overflow
3165 width: width
3166 });
3167
3168 return this;
3169 },
3170
3171 /**
3172 * Reset the tooltip content overflow and remove the test container
3173 *
3174 * @returns {Ruler}
3175 * @public
3176 */
3177 destroy: function() {
3178
3179 // in case the element was not a clone
3180 this.__$tooltip
3181 .detach()
3182 .find('.tooltipster-content')
3183 .css({
3184 // reset to CSS value
3185 display: '',
3186 overflow: ''
3187 });
3188
3189 this.$container.remove();
3190 },
3191
3192 /**
3193 * Removes any constraints
3194 *
3195 * @returns {Ruler}
3196 * @public
3197 */
3198 free: function() {
3199
3200 this.constraints = null;
3201
3202 // reset to natural size
3203 this.__$tooltip.css({
3204 display: '',
3205 height: '',
3206 overflow: 'visible',
3207 width: ''
3208 });
3209
3210 return this;
3211 },
3212
3213 /**
3214 * Returns the size of the tooltip. When constraints are applied, also returns
3215 * whether the tooltip fits in the provided dimensions.
3216 * The idea is to see if the new height is small enough and if the content does
3217 * not overflow horizontally.
3218 *
3219 * @param {int} width
3220 * @param {int} height
3221 * @returns {object} An object with a bool `fits` property and a `size` property
3222 * @public
3223 */
3224 measure: function() {
3225
3226 this.__forceRedraw();
3227
3228 var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(),
3229 result = { size: {
3230 // bcr.width/height are not defined in IE8- but in this
3231 // case, bcr.right/bottom will have the same value
3232 // except in iOS 8+ where tooltipBcr.bottom/right are wrong
3233 // after scrolling for reasons yet to be determined.
3234 // tooltipBcr.top/left might not be 0, see issue #514
3235 height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top),
3236 width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left)
3237 }};
3238
3239 if (this.constraints) {
3240
3241 // note: we used to use offsetWidth instead of boundingRectClient but
3242 // it returned rounded values, causing issues with sub-pixel layouts.
3243
3244 // note2: noticed that the bcrWidth of text content of a div was once
3245 // greater than the bcrWidth of its container by 1px, causing the final
3246 // tooltip box to be too small for its content. However, evaluating
3247 // their widths one against the other (below) surprisingly returned
3248 // equality. Happened only once in Chrome 48, was not able to reproduce
3249 // => just having fun with float position values...
3250
3251 var $content = this.__$tooltip.find('.tooltipster-content'),
3252 height = this.__$tooltip.outerHeight(),
3253 contentBcr = $content[0].getBoundingClientRect(),
3254 fits = {
3255 height: height <= this.constraints.height,
3256 width: (
3257 // this condition accounts for min-width property that
3258 // may apply
3259 tooltipBcr.width <= this.constraints.width
3260 // the -1 is here because scrollWidth actually returns
3261 // a rounded value, and may be greater than bcr.width if
3262 // it was rounded up. This may cause an issue for contents
3263 // which actually really overflow by 1px or so, but that
3264 // should be rare. Not sure how to solve this efficiently.
3265 // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx
3266 && contentBcr.width >= $content[0].scrollWidth - 1
3267 )
3268 };
3269
3270 result.fits = fits.height && fits.width;
3271 }
3272
3273 // old versions of IE get the width wrong for some reason and it causes
3274 // the text to be broken to a new line, so we round it up. If the width
3275 // is the width of the screen though, we can assume it is accurate.
3276 if ( env.IE
3277 && env.IE <= 11
3278 && result.size.width !== env.window.document.documentElement.clientWidth
3279 ) {
3280 result.size.width = Math.ceil(result.size.width) + 1;
3281 }
3282
3283 return result;
86 3284 }
87 3285 };
88 3286
89 // activate tooltips
90 yt = TTIP.main;
91 if ($(document).data('activated-tooltips') !== '1'){
92 $(document).ready(yt.init);
93 $(document).data('activated-tooltips', '1');
94 }
3287 // quick & dirty compare function, not bijective nor multidimensional
3288 function areEqual(a,b) {
3289 var same = true;
3290 $.each(a, function(i, _) {
3291 if (b[i] === undefined || a[i] !== b[i]) {
3292 same = false;
3293 return false;
3294 }
3295 });
3296 return same;
3297 }
3298
3299 /**
3300 * A fast function to check if an element is still in the DOM. It
3301 * tries to use an id as ids are indexed by the browser, or falls
3302 * back to jQuery's `contains` method. May fail if two elements
3303 * have the same id, but so be it
3304 *
3305 * @param {object} $obj A jQuery-wrapped HTML element
3306 * @return {boolean}
3307 */
3308 function bodyContains($obj) {
3309 var id = $obj.attr('id'),
3310 el = id ? env.window.document.getElementById(id) : null;
3311 // must also check that the element with the id is the one we want
3312 return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]);
3313 }
3314
3315 // detect IE versions for dirty fixes
3316 var uA = navigator.userAgent.toLowerCase();
3317 if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]);
3318 else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11;
3319 else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]);
3320
3321 // detecting support for CSS transitions
3322 function transitionSupport() {
3323
3324 // env.window is not defined yet when this is called
3325 if (!win) return false;
3326
3327 var b = win.document.body || win.document.documentElement,
3328 s = b.style,
3329 p = 'transition',
3330 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
3331
3332 if (typeof s[p] == 'string') { return true; }
3333
3334 p = p.charAt(0).toUpperCase() + p.substr(1);
3335 for (var i=0; i<v.length; i++) {
3336 if (typeof s[v[i] + p] == 'string') { return true; }
3337 }
3338 return false;
3339 }
3340
3341 // we'll return jQuery for plugins not to have to declare it as a dependency,
3342 // but it's done by a build task since it should be included only once at the
3343 // end when we concatenate the main file with a plugin
3344 // sideTip is Tooltipster's default plugin.
3345 // This file will be UMDified by a build task.
3346
3347 var pluginName = 'tooltipster.sideTip';
3348
3349 $.tooltipster._plugin({
3350 name: pluginName,
3351 instance: {
3352 /**
3353 * Defaults are provided as a function for an easy override by inheritance
3354 *
3355 * @return {object} An object with the defaults options
3356 * @private
3357 */
3358 __defaults: function() {
3359
3360 return {
3361 // if the tooltip should display an arrow that points to the origin
3362 arrow: true,
3363 // the distance in pixels between the tooltip and the origin
3364 distance: 6,
3365 // allows to easily change the position of the tooltip
3366 functionPosition: null,
3367 maxWidth: null,
3368 // used to accomodate the arrow of tooltip if there is one.
3369 // First to make sure that the arrow target is not too close
3370 // to the edge of the tooltip, so the arrow does not overflow
3371 // the tooltip. Secondly when we reposition the tooltip to
3372 // make sure that it's positioned in such a way that the arrow is
3373 // still pointing at the target (and not a few pixels beyond it).
3374 // It should be equal to or greater than half the width of
3375 // the arrow (by width we mean the size of the side which touches
3376 // the side of the tooltip).
3377 minIntersection: 16,
3378 minWidth: 0,
3379 // deprecated in 4.0.0. Listed for _optionsExtract to pick it up
3380 position: null,
3381 side: 'top',
3382 // set to false to position the tooltip relatively to the document rather
3383 // than the window when we open it
3384 viewportAware: true
3385 };
3386 },
3387
3388 /**
3389 * Run once: at instantiation of the plugin
3390 *
3391 * @param {object} instance The tooltipster object that instantiated this plugin
3392 * @private
3393 */
3394 __init: function(instance) {
3395
3396 var self = this;
3397
3398 // list of instance variables
3399
3400 self.__instance = instance;
3401 self.__namespace = 'tooltipster-sideTip-'+ Math.round(Math.random()*1000000);
3402 self.__previousState = 'closed';
3403 self.__options;
3404
3405 // initial formatting
3406 self.__optionsFormat();
3407
3408 self.__instance._on('state.'+ self.__namespace, function(event) {
3409
3410 if (event.state == 'closed') {
3411 self.__close();
3412 }
3413 else if (event.state == 'appearing' && self.__previousState == 'closed') {
3414 self.__create();
3415 }
3416
3417 self.__previousState = event.state;
3418 });
3419
3420 // reformat every time the options are changed
3421 self.__instance._on('options.'+ self.__namespace, function() {
3422 self.__optionsFormat();
3423 });
3424
3425 self.__instance._on('reposition.'+ self.__namespace, function(e) {
3426 self.__reposition(e.event, e.helper);
3427 });
3428 },
3429
3430 /**
3431 * Called when the tooltip has closed
3432 *
3433 * @private
3434 */
3435 __close: function() {
3436
3437 // detach our content object first, so the next jQuery's remove()
3438 // call does not unbind its event handlers
3439 if (this.__instance.content() instanceof $) {
3440 this.__instance.content().detach();
3441 }
3442
3443 // remove the tooltip from the DOM
3444 this.__instance._$tooltip.remove();
3445 this.__instance._$tooltip = null;
3446 },
3447
3448 /**
3449 * Creates the HTML element of the tooltip.
3450 *
3451 * @private
3452 */
3453 __create: function() {
3454
3455 // note: we wrap with a .tooltipster-box div to be able to set a margin on it
3456 // (.tooltipster-base must not have one)
3457 var $html = $(
3458 '<div class="tooltipster-base tooltipster-sidetip">' +
3459 '<div class="tooltipster-box">' +
3460 '<div class="tooltipster-content"></div>' +
3461 '</div>' +
3462 '<div class="tooltipster-arrow">' +
3463 '<div class="tooltipster-arrow-uncropped">' +
3464 '<div class="tooltipster-arrow-border"></div>' +
3465 '<div class="tooltipster-arrow-background"></div>' +
3466 '</div>' +
3467 '</div>' +
3468 '</div>'
3469 );
3470
3471 // hide arrow if asked
3472 if (!this.__options.arrow) {
3473 $html
3474 .find('.tooltipster-box')
3475 .css('margin', 0)
3476 .end()
3477 .find('.tooltipster-arrow')
3478 .hide();
3479 }
3480
3481 // apply min/max width if asked
3482 if (this.__options.minWidth) {
3483 $html.css('min-width', this.__options.minWidth + 'px');
3484 }
3485 if (this.__options.maxWidth) {
3486 $html.css('max-width', this.__options.maxWidth + 'px');
3487 }
3488
3489 this.__instance._$tooltip = $html;
3490
3491 // tell the instance that the tooltip element has been created
3492 this.__instance._trigger('created');
3493 },
3494
3495 /**
3496 * Used when the plugin is to be unplugged
3497 *
3498 * @private
3499 */
3500 __destroy: function() {
3501 this.__instance._off('.'+ self.__namespace);
3502 },
3503
3504 /**
3505 * (Re)compute this.__options from the options declared to the instance
3506 *
3507 * @private
3508 */
3509 __optionsFormat: function() {
3510
3511 var self = this;
3512
3513 // get the options
3514 self.__options = self.__instance._optionsExtract(pluginName, self.__defaults());
3515
3516 // for backward compatibility, deprecated in v4.0.0
3517 if (self.__options.position) {
3518 self.__options.side = self.__options.position;
3519 }
3520
3521 // options formatting
3522
3523 // format distance as a four-cell array if it ain't one yet and then make
3524 // it an object with top/bottom/left/right properties
3525 if (typeof self.__options.distance != 'object') {
3526 self.__options.distance = [self.__options.distance];
3527 }
3528 if (self.__options.distance.length < 4) {
3529
3530 if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0];
3531 if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0];
3532 if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1];
3533
3534 self.__options.distance = {
3535 top: self.__options.distance[0],
3536 right: self.__options.distance[1],
3537 bottom: self.__options.distance[2],
3538 left: self.__options.distance[3]
3539 };
3540 }
3541
3542 // let's transform:
3543 // 'top' into ['top', 'bottom', 'right', 'left']
3544 // 'right' into ['right', 'left', 'top', 'bottom']
3545 // 'bottom' into ['bottom', 'top', 'right', 'left']
3546 // 'left' into ['left', 'right', 'top', 'bottom']
3547 if (typeof self.__options.side == 'string') {
3548
3549 var opposites = {
3550 'top': 'bottom',
3551 'right': 'left',
3552 'bottom': 'top',
3553 'left': 'right'
3554 };
3555
3556 self.__options.side = [self.__options.side, opposites[self.__options.side]];
3557
3558 if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') {
3559 self.__options.side.push('top', 'bottom');
3560 }
3561 else {
3562 self.__options.side.push('right', 'left');
3563 }
3564 }
3565
3566 // misc
3567 // disable the arrow in IE6 unless the arrow option was explicitly set to true
3568 if ( $.tooltipster._env.IE === 6
3569 && self.__options.arrow !== true
3570 ) {
3571 self.__options.arrow = false;
3572 }
3573 },
3574
3575 /**
3576 * This method must compute and set the positioning properties of the
3577 * tooltip (left, top, width, height, etc.). It must also make sure the
3578 * tooltip is eventually appended to its parent (since the element may be
3579 * detached from the DOM at the moment the method is called).
3580 *
3581 * We'll evaluate positioning scenarios to find which side can contain the
3582 * tooltip in the best way. We'll consider things relatively to the window
3583 * (unless the user asks not to), then to the document (if need be, or if the
3584 * user explicitly requires the tests to run on the document). For each
3585 * scenario, measures are taken, allowing us to know how well the tooltip
3586 * is going to fit. After that, a sorting function will let us know what
3587 * the best scenario is (we also allow the user to choose his favorite
3588 * scenario by using an event).
3589 *
3590 * @param {object} helper An object that contains variables that plugin
3591 * creators may find useful (see below)
3592 * @param {object} helper.geo An object with many layout properties
3593 * about objects of interest (window, document, origin). This should help
3594 * plugin users compute the optimal position of the tooltip
3595 * @private
3596 */
3597 __reposition: function(event, helper) {
3598
3599 var self = this,
3600 finalResult,
3601 // to know where to put the tooltip, we need to know on which point
3602 // of the x or y axis we should center it. That coordinate is the target
3603 targets = self.__targetFind(helper),
3604 testResults = [];
3605
3606 // make sure the tooltip is detached while we make tests on a clone
3607 self.__instance._$tooltip.detach();
3608
3609 // we could actually provide the original element to the Ruler and
3610 // not a clone, but it just feels right to keep it out of the
3611 // machinery.
3612 var $clone = self.__instance._$tooltip.clone(),
3613 // start position tests session
3614 ruler = $.tooltipster._getRuler($clone),
3615 satisfied = false,
3616 animation = self.__instance.option('animation');
3617
3618 // an animation class could contain properties that distort the size
3619 if (animation) {
3620 $clone.removeClass('tooltipster-'+ animation);
3621 }
3622
3623 // start evaluating scenarios
3624 $.each(['window', 'document'], function(i, container) {
3625
3626 var takeTest = null;
3627
3628 // let the user decide to keep on testing or not
3629 self.__instance._trigger({
3630 container: container,
3631 helper: helper,
3632 satisfied: satisfied,
3633 takeTest: function(bool) {
3634 takeTest = bool;
3635 },
3636 results: testResults,
3637 type: 'positionTest'
3638 });
3639
3640 if ( takeTest == true
3641 || ( takeTest != false
3642 && satisfied == false
3643 // skip the window scenarios if asked. If they are reintegrated by
3644 // the callback of the positionTest event, they will have to be
3645 // excluded using the callback of positionTested
3646 && (container != 'window' || self.__options.viewportAware)
3647 )
3648 ) {
3649
3650 // for each allowed side
3651 for (var i=0; i < self.__options.side.length; i++) {
3652
3653 var distance = {
3654 horizontal: 0,
3655 vertical: 0
3656 },
3657 side = self.__options.side[i];
3658
3659 if (side == 'top' || side == 'bottom') {
3660 distance.vertical = self.__options.distance[side];
3661 }
3662 else {
3663 distance.horizontal = self.__options.distance[side];
3664 }
3665
3666 // this may have an effect on the size of the tooltip if there are css
3667 // rules for the arrow or something else
3668 self.__sideChange($clone, side);
3669
3670 $.each(['natural', 'constrained'], function(i, mode) {
3671
3672 takeTest = null;
3673
3674 // emit an event on the instance
3675 self.__instance._trigger({
3676 container: container,
3677 event: event,
3678 helper: helper,
3679 mode: mode,
3680 results: testResults,
3681 satisfied: satisfied,
3682 side: side,
3683 takeTest: function(bool) {
3684 takeTest = bool;
3685 },
3686 type: 'positionTest'
3687 });
3688
3689 if ( takeTest == true
3690 || ( takeTest != false
3691 && satisfied == false
3692 )
3693 ) {
3694
3695 var testResult = {
3696 container: container,
3697 // we let the distance as an object here, it can make things a little easier
3698 // during the user's calculations at positionTest/positionTested
3699 distance: distance,
3700 // whether the tooltip can fit in the size of the viewport (does not mean
3701 // that we'll be able to make it initially entirely visible, see 'whole')
3702 fits: null,
3703 mode: mode,
3704 outerSize: null,
3705 side: side,
3706 size: null,
3707 target: targets[side],
3708 // check if the origin has enough surface on screen for the tooltip to
3709 // aim at it without overflowing the viewport (this is due to the thickness
3710 // of the arrow represented by the minIntersection length).
3711 // If not, the tooltip will have to be partly or entirely off screen in
3712 // order to stay docked to the origin. This value will stay null when the
3713 // container is the document, as it is not relevant
3714 whole: null
3715 };
3716
3717 // get the size of the tooltip with or without size constraints
3718 var rulerConfigured = (mode == 'natural') ?
3719 ruler.free() :
3720 ruler.constrain(
3721 helper.geo.available[container][side].width - distance.horizontal,
3722 helper.geo.available[container][side].height - distance.vertical
3723 ),
3724 rulerResults = rulerConfigured.measure();
3725
3726 testResult.size = rulerResults.size;
3727 testResult.outerSize = {
3728 height: rulerResults.size.height + distance.vertical,
3729 width: rulerResults.size.width + distance.horizontal
3730 };
3731
3732 if (mode == 'natural') {
3733
3734 if( helper.geo.available[container][side].width >= testResult.outerSize.width
3735 && helper.geo.available[container][side].height >= testResult.outerSize.height
3736 ) {
3737 testResult.fits = true;
3738 }
3739 else {
3740 testResult.fits = false;
3741 }
3742 }
3743 else {
3744 testResult.fits = rulerResults.fits;
3745 }
3746
3747 if (container == 'window') {
3748
3749 if (!testResult.fits) {
3750 testResult.whole = false;
3751 }
3752 else {
3753 if (side == 'top' || side == 'bottom') {
3754
3755 testResult.whole = (
3756 helper.geo.origin.windowOffset.right >= self.__options.minIntersection
3757 && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection
3758 );
3759 }
3760 else {
3761 testResult.whole = (
3762 helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection
3763 && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection
3764 );
3765 }
3766 }
3767 }
3768
3769 testResults.push(testResult);
3770
3771 // we don't need to compute more positions if we have one fully on screen
3772 if (testResult.whole) {
3773 satisfied = true;
3774 }
3775 else {
3776 // don't run the constrained test unless the natural width was greater
3777 // than the available width, otherwise it's pointless as we know it
3778 // wouldn't fit either
3779 if ( testResult.mode == 'natural'
3780 && ( testResult.fits
3781 || testResult.size.width <= helper.geo.available[container][side].width
3782 )
3783 ) {
3784 return false;
3785 }
3786 }
3787 }
3788 });
3789 }
3790 }
3791 });
3792
3793 // the user may eliminate the unwanted scenarios from testResults, but he's
3794 // not supposed to alter them at this point. functionPosition and the
3795 // position event serve that purpose.
3796 self.__instance._trigger({
3797 edit: function(r) {
3798 testResults = r;
3799 },
3800 event: event,
3801 helper: helper,
3802 results: testResults,
3803 type: 'positionTested'
3804 });
3805
3806 /**
3807 * Sort the scenarios to find the favorite one.
3808 *
3809 * The favorite scenario is when we can fully display the tooltip on screen,
3810 * even if it means that the middle of the tooltip is no longer centered on
3811 * the middle of the origin (when the origin is near the edge of the screen
3812 * or even partly off screen). We want the tooltip on the preferred side,
3813 * even if it means that we have to use a constrained size rather than a
3814 * natural one (as long as it fits). When the origin is off screen at the top
3815 * the tooltip will be positioned at the bottom (if allowed), if the origin
3816 * is off screen on the right, it will be positioned on the left, etc.
3817 * If there are no scenarios where the tooltip can fit on screen, or if the
3818 * user does not want the tooltip to fit on screen (viewportAware == false),
3819 * we fall back to the scenarios relative to the document.
3820 *
3821 * When the tooltip is bigger than the viewport in either dimension, we stop
3822 * looking at the window scenarios and consider the document scenarios only,
3823 * with the same logic to find on which side it would fit best.
3824 *
3825 * If the tooltip cannot fit the document on any side, we force it at the
3826 * bottom, so at least the user can scroll to see it.
3827 */
3828 testResults.sort(function(a, b) {
3829
3830 // best if it's whole (the tooltip fits and adapts to the viewport)
3831 if (a.whole && !b.whole) {
3832 return -1;
3833 }
3834 else if (!a.whole && b.whole) {
3835 return 1;
3836 }
3837 else if (a.whole && b.whole) {
3838
3839 var ai = self.__options.side.indexOf(a.side),
3840 bi = self.__options.side.indexOf(b.side);
3841
3842 // use the user's sides fallback array
3843 if (ai < bi) {
3844 return -1;
3845 }
3846 else if (ai > bi) {
3847 return 1;
3848 }
3849 else {
3850 // will be used if the user forced the tests to continue
3851 return a.mode == 'natural' ? -1 : 1;
3852 }
3853 }
3854 else {
3855
3856 // better if it fits
3857 if (a.fits && !b.fits) {
3858 return -1;
3859 }
3860 else if (!a.fits && b.fits) {
3861 return 1;
3862 }
3863 else if (a.fits && b.fits) {
3864
3865 var ai = self.__options.side.indexOf(a.side),
3866 bi = self.__options.side.indexOf(b.side);
3867
3868 // use the user's sides fallback array
3869 if (ai < bi) {
3870 return -1;
3871 }
3872 else if (ai > bi) {
3873 return 1;
3874 }
3875 else {
3876 // will be used if the user forced the tests to continue
3877 return a.mode == 'natural' ? -1 : 1;
3878 }
3879 }
3880 else {
3881
3882 // if everything failed, this will give a preference to the case where
3883 // the tooltip overflows the document at the bottom
3884 if ( a.container == 'document'
3885 && a.side == 'bottom'
3886 && a.mode == 'natural'
3887 ) {
3888 return -1;
3889 }
3890 else {
3891 return 1;
3892 }
3893 }
3894 }
3895 });
3896
3897 finalResult = testResults[0];
3898
3899
3900 // now let's find the coordinates of the tooltip relatively to the window
3901 finalResult.coord = {};
3902
3903 switch (finalResult.side) {
3904
3905 case 'left':
3906 case 'right':
3907 finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2);
3908 break;
3909
3910 case 'bottom':
3911 case 'top':
3912 finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2);
3913 break;
3914 }
3915
3916 switch (finalResult.side) {
3917
3918 case 'left':
3919 finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width;
3920 break;
3921
3922 case 'right':
3923 finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal;
3924 break;
3925
3926 case 'top':
3927 finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height;
3928 break;
3929
3930 case 'bottom':
3931 finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical;
3932 break;
3933 }
3934
3935 // if the tooltip can potentially be contained within the viewport dimensions
3936 // and that we are asked to make it fit on screen
3937 if (finalResult.container == 'window') {
3938
3939 // if the tooltip overflows the viewport, we'll move it accordingly (then it will
3940 // not be centered on the middle of the origin anymore). We only move horizontally
3941 // for top and bottom tooltips and vice versa.
3942 if (finalResult.side == 'top' || finalResult.side == 'bottom') {
3943
3944 // if there is an overflow on the left
3945 if (finalResult.coord.left < 0) {
3946
3947 // prevent the overflow unless the origin itself gets off screen (minus the
3948 // margin needed to keep the arrow pointing at the target)
3949 if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) {
3950 finalResult.coord.left = 0;
3951 }
3952 else {
3953 finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1;
3954 }
3955 }
3956 // or an overflow on the right
3957 else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
3958
3959 if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) {
3960 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
3961 }
3962 else {
3963 finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width;
3964 }
3965 }
3966 }
3967 else {
3968
3969 // overflow at the top
3970 if (finalResult.coord.top < 0) {
3971
3972 if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) {
3973 finalResult.coord.top = 0;
3974 }
3975 else {
3976 finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1;
3977 }
3978 }
3979 // or at the bottom
3980 else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) {
3981
3982 if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) {
3983 finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height;
3984 }
3985 else {
3986 finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height;
3987 }
3988 }
3989 }
3990 }
3991 else {
3992
3993 // there might be overflow here too but it's easier to handle. If there has
3994 // to be an overflow, we'll make sure it's on the right side of the screen
3995 // (because the browser will extend the document size if there is an overflow
3996 // on the right, but not on the left). The sort function above has already
3997 // made sure that a bottom document overflow is preferred to a top overflow,
3998 // so we don't have to care about it.
3999
4000 // if there is an overflow on the right
4001 if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
4002
4003 // this may actually create on overflow on the left but we'll fix it in a sec
4004 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
4005 }
4006
4007 // if there is an overflow on the left
4008 if (finalResult.coord.left < 0) {
4009
4010 // don't care if it overflows the right after that, we made our best
4011 finalResult.coord.left = 0;
4012 }
4013 }
4014
4015
4016 // submit the positioning proposal to the user function which may choose to change
4017 // the side, size and/or the coordinates
4018
4019 // first, set the rules that corresponds to the proposed side: it may change
4020 // the size of the tooltip, and the custom functionPosition may want to detect the
4021 // size of something before making a decision. So let's make things easier for the
4022 // implementor
4023 self.__sideChange($clone, finalResult.side);
4024
4025 // add some variables to the helper
4026 helper.tooltipClone = $clone[0];
4027 helper.tooltipParent = self.__instance.option('parent').parent[0];
4028 // move informative values to the helper
4029 helper.mode = finalResult.mode;
4030 helper.whole = finalResult.whole;
4031 // add some variables to the helper for the functionPosition callback (these
4032 // will also be added to the event fired by self.__instance._trigger but that's
4033 // ok, we're just being consistent)
4034 helper.origin = self.__instance._$origin[0];
4035 helper.tooltip = self.__instance._$tooltip[0];
4036
4037 // leave only the actionable values in there for functionPosition
4038 delete finalResult.container;
4039 delete finalResult.fits;
4040 delete finalResult.mode;
4041 delete finalResult.outerSize;
4042 delete finalResult.whole;
4043
4044 // keep only the distance on the relevant side, for clarity
4045 finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical;
4046
4047 // beginners may not be comfortable with the concept of editing the object
4048 // passed by reference, so we provide an edit function and pass a clone
4049 var finalResultClone = $.extend(true, {}, finalResult);
4050
4051 // emit an event on the instance
4052 self.__instance._trigger({
4053 edit: function(result) {
4054 finalResult = result;
4055 },
4056 event: event,
4057 helper: helper,
4058 position: finalResultClone,
4059 type: 'position'
4060 });
4061
4062 if (self.__options.functionPosition) {
4063
4064 var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone);
4065
4066 if (result) finalResult = result;
4067 }
4068
4069 // end the positioning tests session (the user might have had a
4070 // use for it during the position event, now it's over)
4071 ruler.destroy();
4072
4073 // compute the position of the target relatively to the tooltip root
4074 // element so we can place the arrow and make the needed adjustments
4075 var arrowCoord,
4076 maxVal;
4077
4078 if (finalResult.side == 'top' || finalResult.side == 'bottom') {
4079
4080 arrowCoord = {
4081 prop: 'left',
4082 val: finalResult.target - finalResult.coord.left
4083 };
4084 maxVal = finalResult.size.width - this.__options.minIntersection;
4085 }
4086 else {
4087
4088 arrowCoord = {
4089 prop: 'top',
4090 val: finalResult.target - finalResult.coord.top
4091 };
4092 maxVal = finalResult.size.height - this.__options.minIntersection;
4093 }
4094
4095 // cannot lie beyond the boundaries of the tooltip, minus the
4096 // arrow margin
4097 if (arrowCoord.val < this.__options.minIntersection) {
4098 arrowCoord.val = this.__options.minIntersection;
4099 }
4100 else if (arrowCoord.val > maxVal) {
4101 arrowCoord.val = maxVal;
4102 }
4103
4104 var originParentOffset;
4105
4106 // let's convert the window-relative coordinates into coordinates relative to the
4107 // future positioned parent that the tooltip will be appended to
4108 if (helper.geo.origin.fixedLineage) {
4109
4110 // same as windowOffset when the position is fixed
4111 originParentOffset = helper.geo.origin.windowOffset;
4112 }
4113 else {
4114
4115 // this assumes that the parent of the tooltip is located at
4116 // (0, 0) in the document, typically like when the parent is
4117 // <body>.
4118 // If we ever allow other types of parent, .tooltipster-ruler
4119 // will have to be appended to the parent to inherit css style
4120 // values that affect the display of the text and such.
4121 originParentOffset = {
4122 left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left,
4123 top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top
4124 };
4125 }
4126
4127 finalResult.coord = {
4128 left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left),
4129 top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top)
4130 };
4131
4132 // set position values on the original tooltip element
4133
4134 self.__sideChange(self.__instance._$tooltip, finalResult.side);
4135
4136 if (helper.geo.origin.fixedLineage) {
4137 self.__instance._$tooltip
4138 .css('position', 'fixed');
4139 }
4140 else {
4141 // CSS default
4142 self.__instance._$tooltip
4143 .css('position', '');
4144 }
4145
4146 self.__instance._$tooltip
4147 .css({
4148 left: finalResult.coord.left,
4149 top: finalResult.coord.top,
4150 // we need to set a size even if the tooltip is in its natural size
4151 // because when the tooltip is positioned beyond the width of the body
4152 // (which is by default the width of the window; it will happen when
4153 // you scroll the window horizontally to get to the origin), its text
4154 // content will otherwise break lines at each word to keep up with the
4155 // body overflow strategy.
4156 height: finalResult.size.height,
4157 width: finalResult.size.width
4158 })
4159 .find('.tooltipster-arrow')
4160 .css({
4161 'left': '',
4162 'top': ''
4163 })
4164 .css(arrowCoord.prop, arrowCoord.val);
4165
4166 // append the tooltip HTML element to its parent
4167 self.__instance._$tooltip.appendTo(self.__instance.option('parent'));
4168
4169 self.__instance._trigger({
4170 type: 'repositioned',
4171 event: event,
4172 position: finalResult
4173 });
4174 },
4175
4176 /**
4177 * Make whatever modifications are needed when the side is changed. This has
4178 * been made an independant method for easy inheritance in custom plugins based
4179 * on this default plugin.
4180 *
4181 * @param {object} $obj
4182 * @param {string} side
4183 * @private
4184 */
4185 __sideChange: function($obj, side) {
4186
4187 $obj
4188 .removeClass('tooltipster-bottom')
4189 .removeClass('tooltipster-left')
4190 .removeClass('tooltipster-right')
4191 .removeClass('tooltipster-top')
4192 .addClass('tooltipster-'+ side);
4193 },
4194
4195 /**
4196 * Returns the target that the tooltip should aim at for a given side.
4197 * The calculated value is a distance from the edge of the window
4198 * (left edge for top/bottom sides, top edge for left/right side). The
4199 * tooltip will be centered on that position and the arrow will be
4200 * positioned there (as much as possible).
4201 *
4202 * @param {object} helper
4203 * @return {integer}
4204 * @private
4205 */
4206 __targetFind: function(helper) {
4207
4208 var target = {},
4209 rects = this.__instance._$origin[0].getClientRects();
4210
4211 // these lines fix a Chrome bug (issue #491)
4212 if (rects.length > 1) {
4213 var opacity = this.__instance._$origin.css('opacity');
4214 if(opacity == 1) {
4215 this.__instance._$origin.css('opacity', 0.99);
4216 rects = this.__instance._$origin[0].getClientRects();
4217 this.__instance._$origin.css('opacity', 1);
4218 }
4219 }
4220
4221 // by default, the target will be the middle of the origin
4222 if (rects.length < 2) {
4223
4224 target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2));
4225 target.bottom = target.top;
4226
4227 target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2));
4228 target.right = target.left;
4229 }
4230 // if multiple client rects exist, the element may be text split
4231 // up into multiple lines and the middle of the origin may not be
4232 // best option anymore. We need to choose the best target client rect
4233 else {
4234
4235 // top: the first
4236 var targetRect = rects[0];
4237 target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
4238
4239 // right: the middle line, rounded down in case there is an even
4240 // number of lines (looks more centered => check out the
4241 // demo with 4 split lines)
4242 if (rects.length > 2) {
4243 targetRect = rects[Math.ceil(rects.length / 2) - 1];
4244 }
4245 else {
4246 targetRect = rects[0];
4247 }
4248 target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
4249
4250 // bottom: the last
4251 targetRect = rects[rects.length - 1];
4252 target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
4253
4254 // left: the middle line, rounded up
4255 if (rects.length > 2) {
4256 targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1];
4257 }
4258 else {
4259 targetRect = rects[rects.length - 1];
4260 }
4261
4262 target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
4263 }
4264
4265 return target;
4266 }
4267 }
4268 });
4269
4270 /* a build task will add "return $;" here */
4271 return $;
4272
4273 }));
4274
4275 //
4276 // $(document).ready(function() {
4277 // $('.tooltip-hovercard').tooltipster({
4278 // theme: 'tooltipster-shadow',
4279 // animation: 'fade',
4280 // delay: 100,
4281 // contentCloning: true,
4282 //
4283 // }); No newline at end of file
@@ -110,6 +110,7 b''
110 110 },
111 111 "initComplete": function( settings, json ) {
112 112 timeagoActivate();
113 tooltipActivate();
113 114 get_datatable_count();
114 115 }
115 116 });
@@ -117,6 +118,7 b''
117 118 // update the counter when things change
118 119 $('#gist_list_table').on('draw.dt', function() {
119 120 timeagoActivate();
121 tooltipActivate();
120 122 get_datatable_count();
121 123 });
122 124
@@ -75,7 +75,7 b''
75 75
76 76 <div class="author">
77 77 <div title="${h.tooltip(c.file_last_commit.author)}">
78 ${self.gravatar_with_user(c.file_last_commit.author, 16)} - ${_('created')} ${h.age_component(c.file_last_commit.date)}
78 ${self.gravatar_with_user(c.file_last_commit.author, 16, tooltip=True)} - ${_('created')} ${h.age_component(c.file_last_commit.date)}
79 79 </div>
80 80
81 81 </div>
@@ -69,6 +69,7 b''
69 69 },
70 70 "drawCallback": function( settings, json ) {
71 71 timeagoActivate();
72 tooltipActivate();
72 73 },
73 74 "createdRow": function ( row, data, index ) {
74 75 if (data['closed']) {
@@ -5,7 +5,7 b''
5 5
6 6 elems = [
7 7 (_('Repository Group ID'), c.repo_group.group_id, '', ''),
8 (_('Owner'), lambda:base.gravatar_with_user(c.repo_group.user.email), '', ''),
8 (_('Owner'), lambda:base.gravatar_with_user(c.repo_group.user.email, tooltip=True), '', ''),
9 9 (_('Created on'), h.format_date(c.repo_group.created_on), '', ''),
10 10 (_('Updated on'), h.format_date(c.repo_group.updated_on), '', ''),
11 11 (_('Cached Commit date'), (c.repo_group.changeset_cache.get('date')), '', ''),
@@ -26,7 +26,7 b''
26 26 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
27 27 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
28 28 <td class="td-user">
29 ${base.gravatar(_user.email, 16)}
29 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
30 30 ${h.link_to_user(_user.username)}
31 31 %if getattr(_user, 'admin_row', None):
32 32 (${_('super admin')})
@@ -58,7 +58,7 b''
58 58 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', checked=_user.permission=='group.write')}</td>
59 59 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', checked=_user.permission=='group.admin')}</td>
60 60 <td class="td-user">
61 ${base.gravatar(_user.email, 16)}
61 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
62 62 <span class="user">
63 63 % if _user.username == h.DEFAULT_USER:
64 64 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
@@ -101,7 +101,7 b''
101 101 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', disabled="disabled")}</td>
102 102 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', disabled="disabled")}</td>
103 103 <td class="td-user">
104 ${base.gravatar(_user.email, 16)}
104 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
105 105 <span class="user">
106 106 % if _user.username == h.DEFAULT_USER:
107 107 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
@@ -140,7 +140,8 b''
140 140 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.write', checked=_user_group.permission=='group.write')}</td>
141 141 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.admin', checked=_user_group.permission=='group.admin')}</td>
142 142 <td class="td-componentname">
143 <i class="icon-user-group"></i>
143 ${base.user_group_icon(_user_group, tooltip=True)}
144
144 145 %if c.is_super_admin:
145 146 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
146 147 ${_user_group.users_group_name}
@@ -3,7 +3,7 b''
3 3 <%
4 4 elems = [
5 5 (_('Repository ID'), c.rhodecode_db_repo.repo_id, '', ''),
6 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email), '', ''),
6 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email, tooltip=True), '', ''),
7 7 (_('Created on'), h.format_date(c.rhodecode_db_repo.created_on), '', ''),
8 8 (_('Updated on'), h.format_date(c.rhodecode_db_repo.updated_on), '', ''),
9 9 (_('Cached Commit id'), lambda: h.link_to(c.rhodecode_db_repo.changeset_cache.get('short_id'), h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.changeset_cache.get('raw_id'))), '', ''),
@@ -25,7 +25,7 b''
25 25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
26 26 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
27 27 <td class="td-user">
28 ${base.gravatar(_user.email, 16)}
28 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
29 29 ${h.link_to_user(_user.username)}
30 30 %if getattr(_user, 'admin_row', None):
31 31 (${_('super admin')})
@@ -80,7 +80,7 b''
80 80 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.write', checked=_user.permission=='repository.write')}</td>
81 81 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.admin', checked=_user.permission=='repository.admin')}</td>
82 82 <td class="td-user">
83 ${base.gravatar(_user.email, 16)}
83 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
84 84 <span class="user">
85 85 % if _user.username == h.DEFAULT_USER:
86 86 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
@@ -139,7 +139,7 b''
139 139 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.write', checked=_user_group.permission=='repository.write')}</td>
140 140 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.admin', checked=_user_group.permission=='repository.admin')}</td>
141 141 <td class="td-componentname">
142 <i class="icon-user-group"></i>
142 ${base.user_group_icon(_user_group, tooltip=True)}
143 143 %if c.is_super_admin:
144 144 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
145 145 ${_user_group.users_group_name}
@@ -26,7 +26,7 b''
26 26 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
27 27 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
28 28 <td class="td-user">
29 ${base.gravatar(_user.email, 16)}
29 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
30 30 <span class="user">
31 31 ${h.link_to_user(_user.username)}
32 32 %if getattr(_user, 'admin_row', None):
@@ -60,7 +60,7 b''
60 60 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write')}</td>
61 61 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin')}</td>
62 62 <td class="td-user">
63 ${base.gravatar(_user.email, 16)}
63 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
64 64 <span class="user">
65 65 % if _user.username == h.DEFAULT_USER:
66 66 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
@@ -103,7 +103,7 b''
103 103 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write', disabled="disabled")}</td>
104 104 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin', disabled="disabled")}</td>
105 105 <td class="td-user">
106 ${base.gravatar(_user.email, 16)}
106 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
107 107 <span class="user">
108 108 % if _user.username == h.DEFAULT_USER:
109 109 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
@@ -76,7 +76,7 b''
76 76 <tr>
77 77 <td id="member_user_${user.user_id}" class="td-author">
78 78 <div class="group_member">
79 ${base.gravatar(user.email, 16)}
79 ${base.gravatar(user.email, 16, user=user, tooltip=True)}
80 80 <span class="username user">${h.link_to(h.person(user), h.route_path('user_edit',user_id=user.user_id))}</span>
81 81 <input type="hidden" name="__start__" value="member:mapping">
82 82 <input type="hidden" name="member_user_id" value="${user.user_id}">
@@ -194,31 +194,71 b''
194 194
195 195 </%def>
196 196
197 <%def name="gravatar(email, size=16)">
197 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None)">
198 198 <%
199 if (size > 16):
200 gravatar_class = 'gravatar gravatar-large'
199 if size > 16:
200 gravatar_class = ['gravatar','gravatar-large']
201 201 else:
202 gravatar_class = 'gravatar'
202 gravatar_class = ['gravatar']
203
204 data_hovercard_url = ''
205 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
206
207 if tooltip:
208 gravatar_class += ['tooltip-hovercard']
209
210 if tooltip and user:
211 if user.username == h.DEFAULT_USER:
212 gravatar_class.pop(-1)
213 else:
214 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
215 gravatar_class = ' '.join(gravatar_class)
216
203 217 %>
204 218 <%doc>
205 219 TODO: johbo: For now we serve double size images to make it smooth
206 220 for retina. This is how it worked until now. Should be replaced
207 221 with a better solution at some point.
208 222 </%doc>
209 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
223
224 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
225 </%def>
226
227
228 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False)">
229 <%
230 email = h.email_or_none(contact)
231 rc_user = h.discover_user(contact)
232 %>
233
234 <div class="rc-user">
235 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
236 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
237 </div>
210 238 </%def>
211 239
212 240
213 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
214 <% email = h.email_or_none(contact) %>
215 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
216 ${self.gravatar(email, size)}
217 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
218 </div>
241 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
242 <%
243 if (size > 16):
244 gravatar_class = 'icon-user-group-alt'
245 else:
246 gravatar_class = 'icon-user-group-alt'
247
248 if tooltip:
249 gravatar_class += ' tooltip-hovercard'
250
251 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
252 %>
253 <%doc>
254 TODO: johbo: For now we serve double size images to make it smooth
255 for retina. This is how it worked until now. Should be replaced
256 with a better solution at some point.
257 </%doc>
258
259 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
219 260 </%def>
220 261
221
222 262 <%def name="repo_page_title(repo_instance)">
223 263 <div class="title-content repo-title">
224 264
@@ -1090,4 +1130,3 b''
1090 1130 </div><!-- /.modal-content -->
1091 1131 </div><!-- /.modal-dialog -->
1092 1132 </div><!-- /.modal -->
1093
@@ -205,6 +205,7 b' hit preview to see the link'
205 205 },
206 206 success: function(data){
207 207 $('#test_pattern_result').html(data);
208 tooltipActivate();
208 209 },
209 210 error: function(jqXHR, textStatus, errorThrown){
210 211 $('#test_pattern_result').html('Error: ' + errorThrown);
@@ -131,6 +131,7 b" c.template_context['attachment_store'] ="
131 131 $(document).ready(function(){
132 132 show_more_event();
133 133 timeagoActivate();
134 tooltipActivate();
134 135 clipboardActivate();
135 136 })
136 137 </script>
@@ -72,6 +72,7 b''
72 72 "initComplete": function(settings, json) {
73 73 get_datatable_count();
74 74 timeagoActivate();
75 tooltipActivate();
75 76 compare_radio_buttons("${c.repo_name}", 'book');
76 77 }
77 78 });
@@ -80,6 +81,7 b''
80 81 $('#obj_list_table').on('draw.dt', function() {
81 82 get_datatable_count();
82 83 timeagoActivate();
84 tooltipActivate();
83 85 });
84 86
85 87 // filter, filter both grids
@@ -2,6 +2,7 b''
2 2 ## usage:
3 3 ## <%namespace name="bookmarks" file="/bookmarks/bookmarks_data.mako"/>
4 4 ## bookmarks.<func_name>(arg,arg2)
5 <%namespace name="base" file="/base/base.mako"/>
5 6
6 7 <%def name="compare(commit_id)">
7 8 <input class="compare-radio-button" type="radio" name="compare_source" value="${commit_id}"/>
@@ -10,7 +11,7 b''
10 11
11 12
12 13 <%def name="name(name, files_url, closed)">
13 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % (name,))}">
14 <span class="tag booktag">
14 15 <a href="${files_url}">
15 16 <i class="icon-bookmark"></i>
16 17 ${name}
@@ -23,7 +24,7 b''
23 24 </%def>
24 25
25 26 <%def name="author(author)">
26 <span class="tooltip" title="${h.tooltip(author)}">${h.link_to_user(author)}</span>
27 ${base.gravatar_with_user(author, tooltip=True)}
27 28 </%def>
28 29
29 30 <%def name="commit(message, commit_id, commit_idx)">
@@ -71,6 +71,7 b''
71 71 "initComplete": function( settings, json ) {
72 72 get_datatable_count();
73 73 timeagoActivate();
74 tooltipActivate();
74 75 compare_radio_buttons("${c.repo_name}", 'branch');
75 76 }
76 77 });
@@ -79,6 +80,7 b''
79 80 $('#obj_list_table').on('draw.dt', function() {
80 81 get_datatable_count();
81 82 timeagoActivate();
83 tooltipActivate();
82 84 });
83 85
84 86 // filter, filter both grids
@@ -2,6 +2,7 b''
2 2 ## usage:
3 3 ## <%namespace name="branch" file="/branches/branches_data.mako"/>
4 4 ## branch.<func_name>(arg,arg2)
5 <%namespace name="base" file="/base/base.mako"/>
5 6
6 7 <%def name="compare(commit_id)">
7 8 <input class="compare-radio-button" type="radio" name="compare_source" value="${commit_id}"/>
@@ -9,7 +10,7 b''
9 10 </%def>
10 11
11 12 <%def name="name(name, files_url, closed)">
12 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % (name,))}">
13 <span class="tag branchtag">
13 14 <a href="${files_url}"><i class="icon-code-fork"></i>${name}
14 15 %if closed:
15 16 [closed]
@@ -23,7 +24,7 b''
23 24 </%def>
24 25
25 26 <%def name="author(author)">
26 <span class="tooltip" title="${h.tooltip(author)}">${h.link_to_user(author)}</span>
27 ${base.gravatar_with_user(author, tooltip=True)}
27 28 </%def>
28 29
29 30 <%def name="commit(message, commit_id, commit_idx)">
@@ -33,9 +33,11 b''
33 33
34 34 <div class="fieldset">
35 35 <div class="left-content">
36
36 <%
37 rc_user = h.discover_user(c.commit.author_email)
38 %>
37 39 <div class="left-content-avatar">
38 ${base.gravatar(c.commit.author_email, 30)}
40 ${base.gravatar(c.commit.author_email, 30, tooltip=True, user=rc_user)}
39 41 </div>
40 42
41 43 <div class="left-content-message">
@@ -50,10 +52,10 b''
50 52 <div class="fieldset" data-toggle="summary-details">
51 53 <div class="">
52 54 <table>
53 <tr class="file_author tooltip" title="${h.tooltip(h.author_string(c.commit.author_email))}">
55 <tr class="file_author">
54 56
55 57 <td>
56 <span class="user commit-author">${h.link_to_user(c.commit.author)}</span>
58 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
57 59 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
58 60 </td>
59 61
@@ -130,15 +132,15 b''
130 132 <p>${_('Diff options')}:</p>
131 133 <div class="right-label-summary">
132 134 <div class="diff-actions">
133 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
135 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
134 136 ${_('Raw Diff')}
135 137 </a>
136 138 |
137 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
139 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
138 140 ${_('Patch Diff')}
139 141 </a>
140 142 |
141 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
143 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
142 144 ${_('Download Diff')}
143 145 </a>
144 146 </div>
@@ -56,7 +56,7 b''
56 56 </div>
57 57
58 58 <div class="author ${'author-inline' if inline else 'author-general'}">
59 ${base.gravatar_with_user(comment.author.email, 16)}
59 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
60 60 </div>
61 61 <div class="date">
62 62 ${h.age_component(comment.modified_at, time_is_local=True)}
@@ -4,7 +4,6 b''
4 4 <%
5 5 from rhodecode.lib.codeblocks import render_tokenstream
6 6 # avoid module lookup for performance
7 html_escape = h.html_escape
8 7 tooltip = h.tooltip
9 8 %>
10 9 <tr class="cb-line cb-line-fresh ${'cb-annotate' if show_annotation else ''}"
@@ -15,11 +14,9 b''
15 14
16 15 % if annotation:
17 16 % if show_annotation:
18 <td class="cb-annotate-info tooltip"
19 title="Author: ${tooltip(annotation.author) | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}"
20 >
21 ${h.gravatar_with_user(request, annotation.author, 16) | n}
22 <div class="cb-annotate-message truncate-wrap">${h.chop_at_smart(annotation.message, '\n', suffix_if_chopped='...')}</div>
17 <td class="cb-annotate-info">
18 ${h.gravatar_with_user(request, annotation.author, 16, tooltip=True) | n}
19 <div class="tooltip cb-annotate-message truncate-wrap" title="SHA: ${h.show_id(annotation)}<br/>Date: ${annotation.date}<br/>${annotation.message | entity}">${h.chop_at_smart(annotation.message, '\n', suffix_if_chopped='...')}</div>
23 20 </td>
24 21 <td class="cb-annotate-message-spacer">
25 22 <a class="tooltip" href="#show-previous-annotation" onclick="return annotationController.previousAnnotation('${annotation.raw_id}', '${c.f_path}', ${line_num})" title="${tooltip(_('view annotation from before this change'))}">
@@ -90,7 +90,7 b''
90 90 ${h.age_component(commit.date)}
91 91 </td>
92 92 <td class="td-user">
93 ${base.gravatar_with_user(commit.author)}
93 ${base.gravatar_with_user(commit.author, tooltip=True)}
94 94 </td>
95 95
96 96 <td class="td-tags tags-col">
@@ -5,7 +5,7 b''
5 5 %for cnt,cs in enumerate(c.pagination):
6 6 <tr id="chg_${cnt+1}" class="${('tablerow%s' % (cnt%2))}">
7 7 <td class="td-user">
8 ${base.gravatar_with_user(cs.author, 16)}
8 ${base.gravatar_with_user(cs.author, 16, tooltip=True)}
9 9 </td>
10 10 <td class="td-time">
11 11 <div class="date">
@@ -35,7 +35,7 b''
35 35 ${h.age_component(commit.date)}
36 36 </td>
37 37 <td class="td-user">
38 ${base.gravatar_with_user(commit.author, 16)}
38 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
39 39 </td>
40 40 <td class="td-hash">
41 41 <code>
@@ -167,12 +167,6 b''
167 167 %endif
168 168 </%def>
169 169
170 <%def name="user_gravatar(email, size=16)">
171 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
172 ${base.gravatar(email, 16)}
173 </div>
174 </%def>
175
176 170 <%def name="repo_actions(repo_name, super_user=True)">
177 171 <div>
178 172 <div class="grid_edit">
@@ -295,7 +289,7 b''
295 289 </%def>
296 290
297 291 <%def name="user_profile(username)">
298 ${base.gravatar_with_user(username, 16)}
292 ${base.gravatar_with_user(username, 16, tooltip=True)}
299 293 </%def>
300 294
301 295 <%def name="user_group_name(user_group_name)">
@@ -323,7 +317,7 b''
323 317 </%def>
324 318
325 319 <%def name="gist_author(full_contact, created_on, expires)">
326 ${base.gravatar_with_user(full_contact, 16)}
320 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
327 321 </%def>
328 322
329 323
@@ -389,7 +383,7 b''
389 383 </%def>
390 384
391 385 <%def name="pullrequest_author(full_contact)">
392 ${base.gravatar_with_user(full_contact, 16)}
386 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
393 387 </%def>
394 388
395 389
@@ -9,6 +9,13 b' if (size > 16) {'
9 9 } else {
10 10 var gravatar_class = 'gravatar';
11 11 }
12
13 if (tooltip) {
14 var gravatar_class = gravatar_class + ' tooltip-hovercard';
15 }
16
17 var data_hovercard_alt = username;
18
12 19 %>
13 20
14 21 <%
@@ -17,10 +24,11 b' if (show_disabled) {'
17 24 } else {
18 25 var user_cls = 'user';
19 26 }
27 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
20 28 %>
21 29
22 30 <div class="rc-user">
23 <img class="<%= gravatar_class %>" src="<%- gravatar_url -%>" height="<%= size %>" width="<%= size %>">
31 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
24 32 <span class="<%= user_cls %>"> <%- user_link -%> </span>
25 33 </div>
26 34
@@ -62,6 +70,9 b' var CG = new ColorGenerator();'
62 70 renderTemplate('gravatarWithUser', {
63 71 'size': 16,
64 72 'show_disabled': false,
73 'tooltip': true,
74 'username': member.username,
75 'user_id': member.user_id,
65 76 'user_link': member.user_link,
66 77 'gravatar_url': member.gravatar_link
67 78 })
@@ -4,13 +4,15 b''
4 4
5 5 <table class="sidebar-right-content">
6 6 % for email, user, commits in sorted(c.authors, key=lambda e: c.file_last_commit.author_email!=e[0]):
7 <tr class="file_author tooltip" title="${h.tooltip(h.author_string(email))}">
8
7 <tr class="file_author">
9 8 <td>
9 <%
10 rc_user = h.discover_user(user)
11 %>
10 12 % if not c.file_author:
11 ${base.gravatar(email, 16)}
13 ${base.gravatar(email, 16, user=rc_user, tooltip=True)}
12 14 % endif
13 <span class="user commit-author">${h.link_to_user(user)}</span>
15 <span class="user commit-author">${h.link_to_user(rc_user or user)}</span>
14 16 % if c.file_author:
15 17 <span class="commit-date">- ${h.age_component(c.file_last_commit.date)}</span>
16 18 <a href="#ShowAuthors" onclick="showAuthors(this, ${("1" if c.annotate else "0")}); return false" class="action_link"> - ${_('Load All Authors')}</a>
@@ -193,6 +193,8 b''
193 193 container: '#file_history_container',
194 194 push: false,
195 195 timeout: 5000
196 }).complete(function () {
197 tooltipActivate();
196 198 });
197 199 });
198 200
@@ -341,6 +343,7 b''
341 343
342 344 $(document).ready(function() {
343 345 timeagoActivate();
346 tooltipActivate();
344 347
345 348 if ($('#trimmed_message_box').height() < 50) {
346 349 $('#message_expand').hide();
@@ -67,7 +67,7 b''
67 67 </td>
68 68 <td class="td-user" data-attr-name="author">
69 69 % if c.full_load:
70 <span data-author="${node.last_commit.author}" title="${h.tooltip(node.last_commit.author)}">${h.gravatar_with_user(request, node.last_commit.author)|n}</span>
70 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
71 71 % endif
72 72 </td>
73 73 %else:
@@ -68,6 +68,7 b''
68 68 },
69 69 "drawCallback": function( settings, json ) {
70 70 timeagoActivate();
71 tooltipActivate();
71 72 quick_repo_menu();
72 73 // hide pagination for single page
73 74 if (settings._iDisplayLength >= settings.fnRecordsDisplay()) {
@@ -105,6 +106,7 b''
105 106 },
106 107 "drawCallback": function( settings, json ) {
107 108 timeagoActivate();
109 tooltipActivate();
108 110 quick_repo_menu();
109 111 // hide pagination for single page
110 112 if (settings._iDisplayLength >= settings.fnRecordsDisplay()) {
@@ -44,6 +44,7 b''
44 44 $(document).on('pjax:success',function(){
45 45 show_more_event();
46 46 timeagoActivate();
47 tooltipActivate();
47 48 });
48 49 </script>
49 50 %else:
@@ -111,7 +111,7 b''
111 111 <div class="block-right pr-details-content reviewers">
112 112 <ul class="group_members">
113 113 <li>
114 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
114 ${self.gravatar_with_user(c.rhodecode_user.email, 16, tooltip=True)}
115 115 </li>
116 116 </ul>
117 117 </div>
@@ -271,7 +271,7 b''
271 271 <div class="block-right pr-details-content reviewers">
272 272 <ul class="group_members">
273 273 <li>
274 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
274 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
275 275 </li>
276 276 </ul>
277 277 </div>
@@ -328,6 +328,7 b''
328 328 % endfor
329 329
330 330 </ul>
331
331 332 <input type="hidden" name="__end__" value="review_members:sequence">
332 333 ## end members redering block
333 334
@@ -457,7 +458,7 b''
457 458 ${h.age_component(commit.date)}
458 459 </td>
459 460 <td class="td-user">
460 ${base.gravatar_with_user(commit.author, 16)}
461 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
461 462 </td>
462 463 <td class="td-hash">
463 464 <code>
@@ -95,6 +95,7 b''
95 95 },
96 96 "drawCallback": function( settings, json ) {
97 97 timeagoActivate();
98 tooltipActivate();
98 99 },
99 100 "createdRow": function ( row, data, index ) {
100 101 if (data['closed']) {
@@ -54,7 +54,7 b''
54 54 if isinstance(author, dict):
55 55 author = author['email']
56 56 %>
57 ${base.gravatar_with_user(author)}
57 ${base.gravatar_with_user(author, tooltip=True)}
58 58 </td>
59 59 </tr>
60 60 % endif
@@ -147,7 +147,7 b''
147 147 <div class="left-label-summary">
148 148 <p>${_('Owner')}</p>
149 149 <div class="right-label-summary">
150 ${base.gravatar_with_user(c.rhodecode_db_repo.user.email, 16)}
150 ${base.gravatar_with_user(c.rhodecode_db_repo.user.email, 16, tooltip=True)}
151 151 </div>
152 152
153 153 </div>
@@ -54,7 +54,7 b''
54 54 ${h.age_component(cs.date)}
55 55 </td>
56 56 <td class="td-user author">
57 ${base.gravatar_with_user(cs.author)}
57 ${base.gravatar_with_user(cs.author, tooltip=True)}
58 58 </td>
59 59
60 60 <td class="td-tags">
@@ -98,7 +98,7 b''
98 98
99 99 <script type="text/javascript">
100 100 $(document).pjax('#shortlog_data .pager_link','#shortlog_data', {timeout: 5000, scrollTo: false, push: false});
101 $(document).on('pjax:success', function(){ timeagoActivate(); });
101 $(document).on('pjax:success', function(){ timeagoActivate(); tooltipActivate();});
102 102 $(document).on('pjax:timeout', function(event) {
103 103 // Prevent default timeout redirection behavior
104 104 event.preventDefault()
@@ -72,6 +72,7 b''
72 72 "initComplete": function(settings, json) {
73 73 get_datatable_count();
74 74 timeagoActivate();
75 tooltipActivate();
75 76 compare_radio_buttons("${c.repo_name}", 'tag');
76 77 }
77 78 });
@@ -80,6 +81,7 b''
80 81 $('#obj_list_table').on('draw.dt', function() {
81 82 get_datatable_count();
82 83 timeagoActivate();
84 tooltipActivate();
83 85 });
84 86
85 87 // filter, filter both grids
@@ -2,6 +2,7 b''
2 2 ## usage:
3 3 ## <%namespace name="tags" file="/tags/tags_data.mako"/>
4 4 ## tags.<func_name>(arg,arg2)
5 <%namespace name="base" file="/base/base.mako"/>
5 6
6 7 <%def name="compare(commit_id)">
7 8 <input class="compare-radio-button" type="radio" name="compare_source" value="${commit_id}"/>
@@ -9,7 +10,7 b''
9 10 </%def>
10 11
11 12 <%def name="name(name, files_url, closed)">
12 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % (name,))}">
13 <span class="tagtag tag">
13 14 <a href="${files_url}"><i class="icon-tag"></i>${name}</a>
14 15 </span>
15 16 </%def>
@@ -19,11 +20,11 b''
19 20 </%def>
20 21
21 22 <%def name="author(author)">
22 <span class="tooltip" title="${h.tooltip(author)}">${h.link_to_user(author)}</span>
23 ${base.gravatar_with_user(author, tooltip=True)}
23 24 </%def>
24 25
25 26 <%def name="commit(message, commit_id, commit_idx)">
26 27 <div>
27 <pre><a title="${h.tooltip(message)}" href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit_id)}">r${commit_idx}:${h.short_id(commit_id)}</a></pre>
28 <pre><a class="tooltip" title="${h.tooltip(message)}" href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit_id)}">r${commit_idx}:${h.short_id(commit_id)}</a></pre>
28 29 </div>
29 30 </%def>
@@ -35,9 +35,8 b''
35 35 ${_('Owner')}:
36 36 </div>
37 37 <div class="group_member">
38 ${base.gravatar(c.user_group.user.email, 16)}
38 ${base.gravatar(c.user_group.user.email, 16, user=c.user_group.user, tooltip=True)}
39 39 <span class="username user">${h.link_to_user(c.user_group.user)}</span>
40
41 40 </div>
42 41 </div>
43 42 <div class="field">
@@ -65,7 +64,7 b''
65 64 <tr>
66 65 <td id="member_user_${user.user_id}" class="td-author">
67 66 <div class="group_member">
68 ${base.gravatar(user.email, 16)}
67 ${base.gravatar(user.email, 16, user=user, tooltip=True)}
69 68 <span class="username user">${h.link_to(h.person(user), h.route_path('user_edit',user_id=user.user_id))}</span>
70 69 <input type="hidden" name="__start__" value="member:mapping">
71 70 <input type="hidden" name="member_user_id" value="${user.user_id}">
General Comments 0
You need to be logged in to leave comments. Login now