MediaWiki:Gadget-ReferenceTooltips.js

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 }() );