MediaWiki:Gadget-ReferenceTooltips.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
1 // See [[mw:Reference Tooltips]]
2 // Source: https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js
3
4 $( function () {
5
6 // wiki settings
7 var REF_LINK_SELECTOR = '.reference, a[href^="#CITEREF"]',
8 COMMENTED_TEXT_CLASS = 'rt-commentedText',
9 COMMENTED_TEXT_SELECTOR = ( COMMENTED_TEXT_CLASS ? '.' + COMMENTED_TEXT_CLASS + ', ' : '') +
10 'abbr[title]';
11
12 if ( !['', 'RuneScape'].includes(mw.config.get('wgCanonicalNamespace')) ||
13 $( REF_LINK_SELECTOR + ', ' + COMMENTED_TEXT_SELECTOR ).length === 0
14 ) {
15 return;
16 }
17
18 mw.messages.set( {
19 'rt-settings': 'Reference Tooltips settings',
20 'rt-enable-footer': 'Enable Reference Tooltips',
21 'rt-settings-title': 'Reference Tooltips',
22 'rt-save': 'Save',
23 'rt-cancel': 'Cancel',
24 'rt-enable': 'Enable',
25 'rt-disable': 'Disable',
26 'rt-activationMethod': 'Tooltip appears when',
27 'rt-hovering': 'hovering',
28 'rt-clicking': 'clicking',
29 'rt-delay': 'Delay before the tooltip appears (in milliseconds)',
30 'rt-tooltipsForComments': 'Show tooltips over <span title="Tooltip example" class="' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' ) + '" style="border-bottom: 1px dotted; cursor: help;">text with a dotted underline</span> in Reference Tooltips style (allows to see such tooltips on devices with no mouse support)',
31 'rt-disabledNote': 'You can re-enable Reference Tooltips using a link in the footer of the page.',
32 'rt-done': 'Done',
33 'rt-enabled': 'Reference Tooltips are enabled'
34 } );
35
36 // "Global" variables
37 var SECONDS_IN_A_DAY = 60 * 60 * 24,
38 CLASSES = {
39 FADE_IN_DOWN: 'rt-fade-in-down',
40 FADE_IN_UP: 'rt-fade-in-up',
41 FADE_OUT_DOWN: 'rt-fade-out-down',
42 FADE_OUT_UP: 'rt-fade-out-up'
43 },
44 IS_TOUCHSCREEN = 'ontouchstart' in document.documentElement,
45 // Quite a rough check for mobile browsers, a mix of what is advised at
46 // https://stackoverflow.com/a/24600597 (sends to
47 // https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent)
48 // and https://stackoverflow.com/a/14301832
49 IS_MOBILE = /Mobi|Android/i.test( navigator.userAgent ) ||
50 typeof window.orientation !== 'undefined',
51 CLIENT_NAME = $.client.profile().name,
52 settingsString, settings, enabled, delay, activatedByClick, tooltipsForComments, cursorWaitCss,
53 windowManager,
54 $body = $( document.body ),
55 $window = $( window );
56
57 function rt( $content ) {
58 // Popups gadget
59 // we don't have it here - comment out
60 // if ( window.pg ) {
61 // return;
62 // }
63
64 var teSelector,
65 settingsDialogOpening = false;
66
67 function setSettingsCookie() {
68 mw.cookie.set(
69 'RTsettings',
70 Number( enabled ) + '|' + delay + '|' + Number( activatedByClick ) + '|' +
71 Number( tooltipsForComments ),
72 { path: '/', expires: 90 * SECONDS_IN_A_DAY, prefix: '' }
73 );
74 }
75
76 function enableRt() {
77 enabled = true;
78 setSettingsCookie();
79 $( '.rt-enableItem' ).remove();
80 rt( $content );
81 mw.notify( mw.msg( 'rt-enabled' ) );
82 }
83
84 function disableRt() {
85 $content.find( teSelector ).removeClass( 'rt-commentedText' ).off( '.rt' );
86 $body.off( '.rt' );
87 $window.off( '.rt' );
88 }
89
90 function addEnableLink() {
91 // #footer-places – Vector
92 // #f-list – Timeless, Monobook, Modern
93 // parent of #footer li – Cologne Blue
94 var $footer = $( '#footer-places' );
95 // if ( !$footer.length ) {
96 // $footer = $( '#footer li' ).parent();
97 // }
98 $footer.append(
99 $( '<li>' )
100 .addClass( 'rt-enableItem' )
101 .append(
102 $( '<a>' )
103 .text( mw.msg( 'rt-enable-footer' ) )
104 .attr( 'href', 'javascript:' )
105 .click( function ( e ) {
106 e.preventDefault();
107 enableRt();
108 } )
109 )
110 );
111 }
112
113 function TooltippedElement( $element ) {
114 var tooltip,
115 events,
116 te = this;
117
118 function onStartEvent( e ) {
119 var showRefArgs;
120
121 if ( activatedByClick && te.type !== 'commentedText' && e.type !== 'contextmenu' ) {
122 e.preventDefault();
123 }
124 if ( !te.noRef ) {
125 showRefArgs = [ $( this ) ];
126 if ( te.type !== 'supRef' ) {
127 showRefArgs.push( e.pageX, e.pageY );
128 }
129 te.showRef.apply( te, showRefArgs );
130 }
131 }
132
133 function onEndEvent() {
134 if ( !te.noRef ) {
135 te.hideRef();
136 }
137 }
138
139 if ( !$element ) {
140 return;
141 }
142
143 // TooltippedElement.$element and TooltippedElement.$originalElement will be different when
144 // the first is changed after its cloned version is hovered in a tooltip
145 this.$element = $element;
146 this.$originalElement = $element;
147 if ( this.$element.is( REF_LINK_SELECTOR ) ) {
148 if ( this.$element.prop( 'tagName' ) === 'SUP' ) {
149 this.type = 'supRef';
150 } else {
151 this.type = 'harvardRef';
152 }
153 } else {
154 this.type = 'commentedText';
155 this.comment = this.$element.attr( 'title' );
156 if ( !this.comment ) {
157 return;
158 }
159 this.$element.addClass('rt-commentedText');
160 }
161
162 if ( activatedByClick ) {
163 events = {
164 'click.rt': onStartEvent
165 };
166 // Adds an ability to see tooltips for links
167 if ( this.type === 'commentedText' &&
168 ( this.$element.closest( 'a' ).length ||
169 this.$element.has( 'a' ).length
170 )
171 ) {
172 events[ 'contextmenu.rt' ] = onStartEvent;
173 }
174 } else {
175 events = {
176 'mouseenter.rt': onStartEvent,
177 'mouseleave.rt': onEndEvent
178 };
179 }
180
181 this.$element.on( events );
182
183 this.hideRef = function ( immediately ) {
184 clearTimeout( te.showTimer );
185
186 if ( this.type === 'commentedText' ) {
187 this.$element.attr( 'title', this.comment );
188 }
189
190 if ( this.tooltip && this.tooltip.isPresent ) {
191 if ( activatedByClick || immediately ) {
192 this.tooltip.hide();
193 } else {
194 this.hideTimer = setTimeout( function () {
195 te.tooltip.hide();
196 }, 200 );
197 }
198 } else if ( this.$ref && this.$ref.hasClass( 'rt-target' ) ) {
199 this.$ref.removeClass( 'rt-target' );
200 if ( activatedByClick ) {
201 $body.off( 'click.rt touchstart.rt', this.onBodyClick );
202 }
203 }
204 };
205
206 this.showRef = function ( $element, ePageX, ePageY ) {
207 // Popups gadget
208 // comment out as with above
209 // if ( window.pg ) {
210 // disableRt();
211 // return;
212 // }
213
214 if ( this.tooltip && !this.tooltip.$content.length ) {
215 return;
216 }
217
218 var tooltipInitiallyPresent = this.tooltip && this.tooltip.isPresent;
219
220 function reallyShow() {
221 var viewportTop, refOffsetTop, teHref;
222
223 if ( !te.$ref && !te.comment ) {
224 teHref = te.type === 'supRef' ?
225 te.$element.find( 'a' ).attr( 'href' ) :
226 te.$element.attr( 'href' ); // harvardRef
227 te.$ref = teHref &&
228 $( '#' + $.escapeSelector( teHref.slice( 1 ) ) );
229 if ( !te.$ref || !te.$ref.length || !te.$ref.text() ) {
230 te.noRef = true;
231 return;
232 }
233 }
234
235 if ( !tooltipInitiallyPresent && !te.comment ) {
236 viewportTop = $window.scrollTop();
237 refOffsetTop = te.$ref.offset().top;
238 if ( !activatedByClick &&
239 viewportTop < refOffsetTop &&
240 viewportTop + $window.height() > refOffsetTop + te.$ref.height() &&
241 // There can be gadgets/scripts that make references horizontally scrollable.
242 $window.width() > te.$ref.offset().left + te.$ref.width()
243 ) {
244 // Highlight the reference itself
245 te.$ref.addClass( 'rt-target' );
246 return;
247 }
248 }
249
250 if ( !te.tooltip ) {
251 te.tooltip = new Tooltip( te );
252 if ( !te.tooltip.$content.length ) {
253 return;
254 }
255 }
256
257 // If this tooltip is called from inside another tooltip. We can't define it
258 // in the constructor since a ref can be cloned but have the same Tooltip object;
259 // so, Tooltip.parent is a floating value.
260 te.tooltip.parent = te.$element.closest( '.rt-tooltip' ).data( 'tooltip' );
261 if ( te.tooltip.parent && te.tooltip.parent.disappearing ) {
262 return;
263 }
264
265 te.tooltip.show();
266
267 if ( tooltipInitiallyPresent ) {
268 if ( te.tooltip.$element.hasClass( 'rt-tooltip-above' ) ) {
269 te.tooltip.$element.addClass( CLASSES.FADE_IN_DOWN );
270 } else {
271 te.tooltip.$element.addClass( CLASSES.FADE_IN_UP );
272 }
273 return;
274 }
275
276 te.tooltip.calculatePosition( ePageX, ePageY );
277
278 $window.on( 'resize.rt', te.onWindowResize );
279 }
280
281 // We redefine this.$element here because e.target can be a reference link inside
282 // a reference tooltip, not a link that was initially assigned to this.$element
283 this.$element = $element;
284
285 if ( this.type === 'commentedText' ) {
286 this.$element.attr( 'title', '' );
287 }
288
289 if ( activatedByClick ) {
290 if ( tooltipInitiallyPresent ||
291 ( this.$ref && this.$ref.hasClass( 'rt-target' ) )
292 ) {
293 return;
294 } else {
295 setTimeout( function () {
296 $body.on( 'click.rt touchstart.rt', te.onBodyClick );
297 }, 0 );
298 }
299 }
300
301 if ( activatedByClick || tooltipInitiallyPresent ) {
302 reallyShow();
303 } else {
304 this.showTimer = setTimeout( reallyShow, delay );
305 }
306 };
307
308 this.onBodyClick = function ( e ) {
309 if ( !te.tooltip && !(te.$ref && te.$ref.hasClass( 'rt-target' )) ) {
310 return;
311 }
312
313 var $current = $( e.target );
314
315 function contextMatchesParameter( parameter ) {
316 return this === parameter;
317 }
318
319 // The last condition is used to determine cases when a clicked tooltip is the current
320 // element's tooltip or one of its descendants
321 while ( $current.length &&
322 ( !$current.hasClass( 'rt-tooltip' ) ||
323 !$current.data( 'tooltip' ) ||
324 !$current.data( 'tooltip' ).upToTopParent(
325 contextMatchesParameter, [ te.tooltip ],
326 true
327 )
328 )
329 ) {
330 $current = $current.parent();
331 }
332 if ( !$current.length ) {
333 te.hideRef();
334 }
335 };
336
337 this.onWindowResize = function () {
338 te.tooltip.calculatePosition();
339 };
340 }
341
342 function Tooltip( te ) {
343 function openSettingsDialog() {
344 var settingsDialog, settingsWindow;
345
346 if ( cursorWaitCss ) {
347 cursorWaitCss.disabled = true;
348 }
349
350 function SettingsDialog() {
351 SettingsDialog.parent.call( this );
352 }
353 OO.inheritClass( SettingsDialog, OO.ui.ProcessDialog );
354
355 SettingsDialog.static.name = 'settingsDialog';
356 SettingsDialog.static.title = mw.msg( 'rt-settings-title' );
357 SettingsDialog.static.actions = [
358 {
359 modes: 'basic',
360 action: 'save',
361 label: mw.msg( 'rt-save' ),
362 flags: [ 'primary', 'progressive' ]
363 },
364 {
365 modes: 'basic',
366 label: mw.msg( 'rt-cancel' ),
367 flags: 'safe'
368 },
369 {
370 modes: 'disabled',
371 action: 'deactivated',
372 label: mw.msg( 'rt-done' ),
373 flags: [ 'primary', 'progressive' ]
374 }
375 ];
376
377 SettingsDialog.prototype.initialize = function () {
378 var dialog = this;
379
380 SettingsDialog.parent.prototype.initialize.apply( this, arguments );
381
382 this.enableOption = new OO.ui.RadioOptionWidget( {
383 label: mw.msg( 'rt-enable' )
384 } );
385 this.disableOption = new OO.ui.RadioOptionWidget( {
386 label: mw.msg( 'rt-disable' )
387 } );
388 this.enableSelect = new OO.ui.RadioSelectWidget( {
389 items: [ this.enableOption, this.disableOption ],
390 classes: [ 'rt-enableSelect' ]
391 } );
392 this.enableSelect.selectItem( this.enableOption );
393 this.enableSelect.on( 'choose', function ( item ) {
394 if ( item === dialog.disableOption ) {
395 dialog.activationMethodSelect.setDisabled( true );
396 dialog.delayInput.setDisabled( true );
397 dialog.tooltipsForCommentsCheckbox.setDisabled( true );
398 } else {
399 dialog.activationMethodSelect.setDisabled( false );
400 dialog.delayInput.setDisabled( dialog.clickOption.isSelected() );
401 dialog.tooltipsForCommentsCheckbox.setDisabled( false );
402 }
403 } );
404
405 this.hoverOption = new OO.ui.RadioOptionWidget( {
406 label: mw.msg( 'rt-hovering' )
407 } );
408 this.clickOption = new OO.ui.RadioOptionWidget( {
409 label: mw.msg( 'rt-clicking' )
410 } );
411 this.activationMethodSelect = new OO.ui.RadioSelectWidget( {
412 items: [ this.hoverOption, this.clickOption ]
413 } );
414 this.activationMethodSelect.selectItem( activatedByClick ?
415 this.clickOption :
416 this.hoverOption
417 );
418 this.activationMethodSelect.on( 'choose', function ( item ) {
419 if ( item === dialog.clickOption ) {
420 dialog.delayInput.setDisabled( true );
421 } else {
422 dialog.delayInput.setDisabled( dialog.clickOption.isSelected() );
423 }
424 } );
425 this.activationMethodField = new OO.ui.FieldLayout( this.activationMethodSelect, {
426 label: mw.msg( 'rt-activationMethod' ),
427 align: 'top'
428 } );
429
430 this.delayInput = new OO.ui.NumberInputWidget( {
431 input: { value: delay },
432 step: 50,
433 min: 0,
434 max: 5000,
435 disabled: activatedByClick,
436 classes: [ 'rt-numberInput' ]
437 } );
438 this.delayField = new OO.ui.FieldLayout( this.delayInput, {
439 label: mw.msg( 'rt-delay' ),
440 align: 'top'
441 } );
442
443 this.tooltipsForCommentsCheckbox = new OO.ui.CheckboxInputWidget( {
444 selected: tooltipsForComments
445 } );
446 this.tooltipsForCommentsField = new OO.ui.FieldLayout(
447 this.tooltipsForCommentsCheckbox,
448 {
449 label: new OO.ui.HtmlSnippet( mw.msg( 'rt-tooltipsForComments' ) ),
450 align: 'inline',
451 classes: [ 'rt-tooltipsForCommentsField' ]
452 }
453 );
454 new TooltippedElement(
455 this.tooltipsForCommentsField.$element.find(
456 '.' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' )
457 )
458 );
459
460 this.fieldset = new OO.ui.FieldsetLayout();
461 this.fieldset.addItems( [
462 this.activationMethodField,
463 this.delayField,
464 this.tooltipsForCommentsField
465 ] );
466
467 this.panelSettings = new OO.ui.PanelLayout( {
468 padded: true,
469 expanded: false
470 } );
471 this.panelSettings.$element.append(
472 this.enableSelect.$element,
473 $( '<hr>' ).addClass( 'rt-settingsFormSeparator' ),
474 this.fieldset.$element
475 );
476
477 this.panelDisabled = new OO.ui.PanelLayout( {
478 padded: true,
479 expanded: false
480 } );
481 this.panelDisabled.$element.append(
482 $( '<table>' )
483 .addClass( 'rt-disabledHelp' )
484 .append(
485 $( '<tr>' ).append(
486 $( '<td>' ).append(
487 $( '<img>' ).attr( 'src', 'https://oldschool.runescape.wiki/load.php?modules=ext.popups.images&image=footer&format=rasterized&lang=ru&skin=vector&version=0uotisb' )
488 ),
489 $( '<td>' )
490 .addClass( 'rt-disabledNote' )
491 .text( mw.msg( 'rt-disabledNote' ) )
492 )
493 )
494 );
495
496 this.stackLayout = new OO.ui.StackLayout( {
497 items: [ this.panelSettings, this.panelDisabled ]
498 } );
499
500 this.$body.append( this.stackLayout.$element );
501 };
502
503 SettingsDialog.prototype.getSetupProcess = function ( data ) {
504 return SettingsDialog.parent.prototype.getSetupProcess.call( this, data )
505 .next( function () {
506 this.stackLayout.setItem( this.panelSettings );
507 this.actions.setMode( 'basic' );
508 }, this );
509 };
510
511 SettingsDialog.prototype.getActionProcess = function ( action ) {
512 var dialog = this;
513
514 if ( action === 'save' ) {
515 return new OO.ui.Process( function () {
516 var newDelay = Number( dialog.delayInput.getValue() );
517
518 enabled = dialog.enableOption.isSelected();
519 if ( newDelay >= 0 && newDelay <= 5000 ) {
520 delay = newDelay;
521 }
522 activatedByClick = dialog.clickOption.isSelected();
523 tooltipsForComments = dialog.tooltipsForCommentsCheckbox.isSelected();
524
525 setSettingsCookie();
526
527 if ( enabled ) {
528 dialog.close();
529 disableRt();
530 rt( $content );
531 } else {
532 dialog.actions.setMode( 'disabled' );
533 dialog.stackLayout.setItem( dialog.panelDisabled );
534 disableRt();
535 addEnableLink();
536 }
537 } );
538 } else if ( action === 'deactivated' ) {
539 dialog.close();
540 }
541 return SettingsDialog.parent.prototype.getActionProcess.call( this, action );
542 };
543
544 SettingsDialog.prototype.getBodyHeight = function () {
545 return this.stackLayout.getCurrentItem().$element.outerHeight( true );
546 };
547
548 tooltip.upToTopParent( function adjustRightAndHide() {
549 if ( this.isPresent ) {
550 if ( this.$element[ 0 ].style.right ) {
551 this.$element.css(
552 'right',
553 '+=' + ( window.innerWidth - $window.width() )
554 );
555 }
556 this.te.hideRef( true );
557 }
558 } );
559
560 if ( !windowManager ) {
561 windowManager = new OO.ui.WindowManager();
562 $body.append( windowManager.$element );
563 }
564
565 settingsDialog = new SettingsDialog();
566 windowManager.addWindows( [ settingsDialog ] );
567 settingsWindow = windowManager.openWindow( settingsDialog );
568 settingsWindow.opened.then( function () {
569 settingsDialogOpening = false;
570 } );
571 settingsWindow.closed.then( function () {
572 windowManager.clearWindows();
573 } );
574 }
575
576 var tooltip = this;
577
578 // This variable can change: one tooltip can be called from a harvard-style reference link
579 // that is put into different tooltips
580 this.te = te;
581
582 switch ( this.te.type ) {
583 case 'supRef':
584 this.id = 'rt-' + this.te.$originalElement.attr( 'id' );
585 this.$content = this.te.$ref
586 .contents()
587 .filter( function ( i ) {
588 var $this = $( this );
589 return this.nodeType === Node.TEXT_NODE ||
590 !( $this.is( '.mw-cite-backlink' ) ||
591 ( i === 0 &&
592 // Template:Cnote, Template:Note
593 ( $this.is( 'b' ) ||
594 // Template:Note_label
595 $this.is( 'a' ) &&
596 $this.attr( 'href' ).indexOf( '#ref' ) === 0
597 )
598 )
599 );
600 } )
601 .clone( true );
602 break;
603 case 'harvardRef':
604 this.id = 'rt-' + this.te.$originalElement.closest( 'li' ).attr( 'id' );
605 this.$content = this.te.$ref
606 .clone( true )
607 .removeAttr( 'id' );
608 break;
609 case 'commentedText':
610 this.id = 'rt-' + String( Math.random() ).slice( 2 );
611 this.$content = $( document.createTextNode( this.te.comment ) );
612 break;
613 }
614 if ( !this.$content.length ) {
615 return;
616 }
617
618 this.insideWindow = Boolean( this.te.$element.closest( '.oo-ui-window' ).length );
619
620 this.$element = $( '<div>' )
621 .addClass( 'rt-tooltip' )
622 .attr( 'id', this.id )
623 .attr( 'role', 'tooltip' )
624 .data( 'tooltip', this );
625 if ( this.insideWindow ) {
626 this.$element.addClass( 'rt-tooltip-insideWindow' );
627 }
628
629 // We need the $content interlayer here in order for the settings icon to have correct
630 // margins
631 this.$content = this.$content
632 .wrapAll( '<div>' )
633 .parent()
634 .addClass( 'rt-tooltipContent' )
635 .addClass( 'mw-parser-output' )
636 .appendTo( this.$element );
637
638 if ( !activatedByClick ) {
639 this.$element
640 .mouseenter( function () {
641 if ( !tooltip.disappearing ) {
642 tooltip.upToTopParent( function () {
643 this.show();
644 } );
645 }
646 } )
647 .mouseleave( function ( e ) {
648 // https://stackoverflow.com/q/47649442 workaround. Relying on relatedTarget
649 // alone has pitfalls: when alt-tabbing, relatedTarget is empty too
650 if ( CLIENT_NAME !== 'chrome' ||
651 ( !e.originalEvent ||
652 e.originalEvent.relatedTarget !== null ||
653 !tooltip.clickedTime ||
654 $.now() - tooltip.clickedTime > 50
655 )
656 ) {
657 tooltip.upToTopParent( function () {
658 this.te.hideRef();
659 } );
660 }
661 } )
662 .click( function () {
663 tooltip.clickedTime = $.now();
664 } );
665 }
666
667 if ( !this.insideWindow ) {
668 $( '<div>' )
669 .addClass( 'rt-settingsLink' )
670 .attr( 'title', mw.msg( 'rt-settings' ) )
671 .click( function () {
672 if ( settingsDialogOpening ) {
673 return;
674 }
675 settingsDialogOpening = true;
676
677 if ( mw.loader.getState( 'oojs-ui' ) !== 'ready' ) {
678 if ( cursorWaitCss ) {
679 cursorWaitCss.disabled = false;
680 } else {
681 cursorWaitCss = mw.util.addCSS( 'body { cursor: wait; }' );
682 }
683 }
684 mw.loader.using( [ 'oojs', 'oojs-ui' ], openSettingsDialog );
685 } )
686 .prependTo( this.$content );
687 }
688
689 // Tooltip tail element is inside tooltip content element in order for the tooltip
690 // not to disappear when the mouse is above the tail
691 this.$tail = $( '<div>' )
692 .addClass( 'rt-tooltipTail' )
693 .prependTo( this.$element );
694
695 this.disappearing = false;
696
697 this.show = function () {
698 this.disappearing = false;
699 clearTimeout( this.te.hideTimer );
700 clearTimeout( this.te.removeTimer );
701
702 this.$element
703 .removeClass( CLASSES.FADE_OUT_DOWN )
704 .removeClass( CLASSES.FADE_OUT_UP );
705
706 if ( !this.isPresent ) {
707 $body.append( this.$element );
708 }
709
710 this.isPresent = true;
711 };
712
713 this.hide = function () {
714 var tooltip = this;
715
716 tooltip.disappearing = true;
717
718 if ( tooltip.$element.hasClass( 'rt-tooltip-above' ) ) {
719 tooltip.$element
720 .removeClass( CLASSES.FADE_IN_DOWN )
721 .addClass( CLASSES.FADE_OUT_UP );
722 } else {
723 tooltip.$element
724 .removeClass( CLASSES.FADE_IN_UP )
725 .addClass( CLASSES.FADE_OUT_DOWN );
726 }
727
728 tooltip.te.removeTimer = setTimeout( function () {
729 if ( tooltip.isPresent ) {
730 tooltip.$element.detach();
731
732 tooltip.$tail.css( 'left', '' );
733
734 if ( activatedByClick ) {
735 $body.off( 'click.rt touchstart.rt', tooltip.te.onBodyClick );
736 }
737 $window.off( 'resize.rt', tooltip.te.onWindowResize );
738
739 tooltip.isPresent = false;
740 }
741 }, 200 );
742 };
743
744 this.calculatePosition = function ( ePageX, ePageY ) {
745 var teElement, teOffsets, teOffset, tooltipTailOffsetX, tooltipTailLeft,
746 offsetYCorrection = 0;
747
748 this.$tail.css( 'left', '' );
749
750 teElement = this.te.$element.get( 0 );
751 if ( ePageX !== undefined ) {
752 tooltipTailOffsetX = ePageX;
753 teOffsets = teElement.getClientRects &&
754 teElement.getClientRects() ||
755 teElement.getBoundingClientRect();
756 if ( teOffsets.length > 1 ) {
757 for (var i = teOffsets.length - 1; i >= 0; i--) {
758 if ( ePageY >= Math.round( $window.scrollTop() + teOffsets[i].top ) &&
759 ePageY <= Math.round(
760 $window.scrollTop() + teOffsets[i].top + teOffsets[i].height
761 )
762 ) {
763 teOffset = teOffsets[i];
764 }
765 }
766 }
767 }
768
769 if ( !teOffset ) {
770 teOffset = teElement.getClientRects &&
771 teElement.getClientRects()[0] ||
772 teElement.getBoundingClientRect();
773 }
774 teOffset = {
775 top: $window.scrollTop() + teOffset.top,
776 left: $window.scrollLeft() + teOffset.left,
777 width: teOffset.width,
778 height: teOffset.height
779 };
780 if ( !tooltipTailOffsetX ) {
781 tooltipTailOffsetX = ( teOffset.left * 2 + teOffset.width ) / 2;
782 }
783 if ( CLIENT_NAME === 'msie' && this.te.type === 'supRef' ) {
784 offsetYCorrection = -Number(
785 this.te.$element.parent().css( 'font-size' ).replace( 'px', '' )
786 ) / 2;
787 }
788 this.$element.css( {
789 top: teOffset.top - this.$element.outerHeight() - 7 + offsetYCorrection,
790 left: tooltipTailOffsetX - 20,
791 right: ''
792 } );
793
794 // Is it squished against the right side of the page?
795 if ( this.$element.offset().left + this.$element.outerWidth() > $window.width() - 1 ) {
796 this.$element.css( {
797 left: '',
798 right: 0
799 } );
800 tooltipTailLeft = tooltipTailOffsetX - this.$element.offset().left - 5;
801 }
802
803 // Is a part of it above the top of the screen?
804 if ( teOffset.top < this.$element.outerHeight() + $window.scrollTop() + 6 ) {
805 this.$element
806 .removeClass( 'rt-tooltip-above' )
807 .addClass( 'rt-tooltip-below' )
808 .addClass( CLASSES.FADE_IN_UP )
809 .css( {
810 top: teOffset.top + teOffset.height + 9 + offsetYCorrection
811 } );
812 if ( tooltipTailLeft ) {
813 this.$tail.css( 'left', ( tooltipTailLeft + 12 ) + 'px' );
814 }
815 } else {
816 this.$element
817 .removeClass( 'rt-tooltip-below' )
818 .addClass( 'rt-tooltip-above' )
819 .addClass( CLASSES.FADE_IN_DOWN )
820 // A fix for cases when a tooltip shown once is then wrongly positioned when it
821 // is shown again after a window resize. We just repeat what is above.
822 .css( {
823 top: teOffset.top - this.$element.outerHeight() - 7 + offsetYCorrection
824 } );
825 if ( tooltipTailLeft ) {
826 // 12 is the tail element width/height
827 this.$tail.css( 'left', tooltipTailLeft + 'px' );
828 }
829 }
830 };
831
832 // Run some function for all the tooltips up to the top one in a tree. Its context will be
833 // the tooltip, while its parameters may be passed to Tooltip.upToTopParent as an array
834 // in the second parameter. If the third parameter passed to ToolTip.upToTopParent is true,
835 // the execution stops when the function in question returns true for the first time,
836 // and ToolTip.upToTopParent returns true as well.
837 this.upToTopParent = function ( func, parameters, stopAtTrue ) {
838 var returnValue,
839 currentTooltip = this;
840
841 do {
842 returnValue = func.apply( currentTooltip, parameters );
843 if ( stopAtTrue && returnValue ) {
844 break;
845 }
846 } while ( currentTooltip = currentTooltip.parent );
847
848 if ( stopAtTrue ) {
849 return returnValue;
850 }
851 };
852 }
853
854 if ( !enabled ) {
855 addEnableLink();
856 return;
857 }
858
859 teSelector = REF_LINK_SELECTOR;
860 if ( tooltipsForComments ) {
861 teSelector += ', ' + COMMENTED_TEXT_SELECTOR;
862 }
863 $content.find( teSelector ).each( function () {
864 new TooltippedElement( $( this ) );
865 } );
866 }
867
868 settingsString = mw.cookie.get( 'RTsettings', '' );
869 if ( settingsString ) {
870 settings = settingsString.split( '|' );
871 enabled = Boolean( Number( settings[ 0 ] ) );
872 delay = Number( settings[ 1 ] );
873 activatedByClick = Boolean( Number( settings[ 2 ] ) );
874 // The forth value was added later, so we provide for a default value. See comments below
875 // for why we use "IS_TOUCHSCREEN && IS_MOBILE".
876 tooltipsForComments = settings[ 3 ] === undefined ?
877 IS_TOUCHSCREEN && IS_MOBILE :
878 Boolean( Number( settings[ 3 ] ) );
879 } else {
880 enabled = true;
881 delay = 200;
882 // Since the mobile browser check is error-prone, adding IS_MOBILE condition here would probably
883 // leave cases where a user interacting with the browser using touches doesn't know how to call
884 // a tooltip in order to switch to activation by click. Some touch-supporting laptop users
885 // interacting by touch (though probably not the most popular use case) would not be happy too.
886 activatedByClick = IS_TOUCHSCREEN;
887 // Arguably we shouldn't convert native tooltips into gadget tooltips for devices that have
888 // mouse support, even if they have touchscreens (there are laptops with touchscreens).
889 // IS_TOUCHSCREEN check here is for reliability, since the mobile check is prone to false
890 // positives.
891 tooltipsForComments = IS_TOUCHSCREEN && IS_MOBILE;
892 }
893
894 mw.hook( 'wikipage.content' ).add( rt );
895
896 }() );