MediaWiki:Common.js: Difference between revisions

No edit summary
Tag: Reverted
mNo edit summary
 
(83 intermediate revisions by the same user not shown)
Line 2: Line 2:
mw.loader.load( 'ext.cite.styles' );
mw.loader.load( 'ext.cite.styles' );


// See [[mw:Reference Tooltips]]
/* Allows Template:Notice to be dismissable. */
// Source https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js
mw.loader.using(['mediawiki.user', 'mediawiki.util'], function () {
  if (mw.user.isAnon()) {
    // Hide the dismiss button for anonymous users
    $('.user-only-dismiss').hide();
    return;
  }


/*eslint space-in-parens: ["error", "always"], array-bracket-spacing: ["error", "always"]*/
  const pageTitle = mw.config.get('wgPageName');


( function () {
  // Enable dismissal for logged-in users if allowed
  $('.maybe-dismissible').addClass('dismissible-notice');


// If you're loading the script from another wiki and want to set your settings, do that in `window`
   $('.dismissible-notice').each(function (i) {
// properties with `rt_` prefix, e.g.
    const notice = $(this);
//   window.rt_REF_LINK_SELECTOR = '...';
    const content = notice.text().trim();
// They will be used instead of enwiki detaults.
var REF_LINK_SELECTOR = window.rt_REF_LINK_SELECTOR || '.reference, a[href^="#CITEREF"]',
COMMENTED_TEXT_CLASS = window.rt_COMMENTED_TEXT_CLASS || 'rt-commentedText',
COMMENTED_TEXT_SELECTOR = (
window.rt_COMMENTED_TEXT_SELECTOR ||
( COMMENTED_TEXT_CLASS ? '.' + COMMENTED_TEXT_CLASS + ', ' : '' ) +
'abbr[title]'
);


if ( mw.messages.get( 'rt-settings' ) === null ) {
    // Generate a unique key for each notice (based on content + index)
mw.messages.set( {
    const hash = generateHash(content);
'rt-settings': 'Reference Tooltips settings',
    const key = `dismissed-notice-${pageTitle}-${hash}-${i}`;
'rt-enable-footer': 'Enable Reference Tooltips',
'rt-settings-title': 'Reference Tooltips',
'rt-save': 'Save',
'rt-enable': 'Enable Reference Tooltips',
'rt-activationMethod': 'Show a tooltip when I\'m',
'rt-hovering': 'hovering a reference',
'rt-clicking': 'clicking a reference',
'rt-delay': 'Delay before the tooltip appears (in milliseconds)',
'rt-tooltipsForComments': 'Show the tooltip 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)',
'rt-disabledNote': 'You can re-enable Reference Tooltips using a link in the footer of the page.',
'rt-done': 'Done',
'rt-enabled': 'Reference Tooltips are enabled'
} );
}
 
// "Global" variables
var SECONDS_IN_A_DAY = 60 * 60 * 24,
CLASSES = {
FADE_IN_DOWN: 'rt-fade-in-down',
FADE_IN_UP: 'rt-fade-in-up',
FADE_OUT_DOWN: 'rt-fade-out-down',
FADE_OUT_UP: 'rt-fade-out-up'
},
IS_TOUCHSCREEN = 'ontouchstart' in document.documentElement,
// Quite a rough check for mobile browsers, a mix of what is advised at
// https://stackoverflow.com/a/24600597 (sends to
// https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent)
// and https://stackoverflow.com/a/14301832
IS_MOBILE = /Mobi|Android/i.test( navigator.userAgent ) ||
typeof window.orientation !== 'undefined',
CLIENT_NAME = $.client.profile().name,
settingsString, settings, enabled, delay, activatedByClick, tooltipsForComments, cursorWaitCss,
windowManager, $teleportTarget,
$body = $( document.body ),
$window = $( window ),
$overlay = $( '<div>' )
.addClass( 'rt-overlay' )
.appendTo( $body );
 
// Can't use before https://phabricator.wikimedia.org/T369880 is resolved
// mw.loader.using( 'mediawiki.page.ready' ).then( function ( require ) {
// $teleportTarget = $( require( 'mediawiki.page.ready' ).teleportTarget );
// $overlay.appendTo( $teleportTarget );
// } );
 
function rt( $content ) {
// Popups gadget
if ( window.pg ) {
return;
}
 
var teSelector,
settingsDialogOpening = false;
 
function setSettingsCookie() {
mw.cookie.set(
'RTsettings',
(
Number( enabled ) +
'|' +
delay +
'|' +
Number( activatedByClick ) +
'|' +
Number( tooltipsForComments )
),
{ path: '/', expires: 90 * SECONDS_IN_A_DAY, prefix: '' }
);
}
 
function enableRt() {
enabled = true;
setSettingsCookie();
$( '.rt-enableItem' ).remove();
rt( $content );
mw.notify( mw.msg( 'rt-enabled' ) );
}
 
function disableRt() {
$content.find( teSelector ).removeClass( 'rt-commentedText' ).off( '.rt' );
$body.off( '.rt' );
$window.off( '.rt' );
}
 
function addEnableLink() {
// #footer-places – Vector
// #f-list – Timeless, Monobook, Modern
// parent of #footer li – Cologne Blue
var $footer = $( '#footer-places, #f-list' );
if ( !$footer.length ) {
$footer = $( '#footer li' ).parent();
}
if ( !$footer.find( '.rt-enableItem' ).length ) {
$footer.append(
$( '<li>' )
.addClass( 'rt-enableItem' )
.append(
$( '<a>' )
.text( mw.msg( 'rt-enable-footer' ) )
.attr( 'href', '#' )
.click( function ( e ) {
e.preventDefault();
enableRt();
} )
)
);
}
}
 
function TooltippedElement( $element ) {
var events,
te = this;
 
function onStartEvent( e ) {
var showRefArgs;
 
if ( activatedByClick && te.type !== 'commentedText' && e.type !== 'contextmenu' ) {
e.preventDefault();
}
if ( !te.noRef ) {
showRefArgs = [ $( this ) ];
if ( te.type !== 'supRef' ) {
showRefArgs.push( e.pageX, e.pageY );
}
te.showRef.apply( te, showRefArgs );
}
}
 
function onEndEvent() {
if ( !te.noRef ) {
te.hideRef();
}
}
 
if ( !$element ) {
return;
}
 
// TooltippedElement.$element and TooltippedElement.$originalElement will be different when
// the first is changed after its cloned version is hovered in a tooltip
this.$element = $element;
this.$originalElement = $element;
if ( this.$element.is( REF_LINK_SELECTOR ) ) {
if ( this.$element.prop( 'tagName' ) === 'SUP' ) {
this.type = 'supRef';
} else {
this.type = 'harvardRef';
}
} else {
this.type = 'commentedText';
this.comment = this.$element.attr( 'title' );
if ( !this.comment ) {
return;
}
this.$element.addClass( 'rt-commentedText' );
}
 
if ( activatedByClick ) {
events = {
'click.rt': onStartEvent
};
// Adds an ability to see tooltips for links
if (
this.type === 'commentedText' &&
( this.$element.closest( 'a' ).length || this.$element.has( 'a' ).length )
) {
events[ 'contextmenu.rt' ] = onStartEvent;
}
} else {
events = {
'mouseenter.rt': onStartEvent,
'mouseleave.rt': onEndEvent
};
}
 
this.$element.on( events );
 
this.hideRef = function ( immediately ) {
clearTimeout( te.showTimer );
 
if ( this.type === 'commentedText' ) {
this.$element.attr( 'title', this.comment );
}
 
if ( this.tooltip && this.tooltip.isPresent ) {
if ( activatedByClick || immediately ) {
this.tooltip.hide();
} else {
this.hideTimer = setTimeout( function () {
te.tooltip.hide();
}, 200 );
}
} else if ( this.$ref && this.$ref.hasClass( 'rt-target' ) ) {
this.$ref.removeClass( 'rt-target' );
if ( activatedByClick ) {
$body.off( 'click.rt touchstart.rt', this.onBodyClick );
}
}
};
 
this.showRef = function ( $element, ePageX, ePageY ) {
// Popups gadget
if ( window.pg ) {
disableRt();
return;
}
 
if ( this.tooltip && !this.tooltip.$content.length ) {
return;
}
 
var tooltipInitiallyPresent = this.tooltip && this.tooltip.isPresent;
 
function reallyShow() {
var viewportTop, refOffsetTop, teHref;
 
if ( !te.$ref && !te.comment ) {
teHref = te.type === 'supRef' ?
te.$element.find( 'a' ).attr( 'href' ) :
te.$element.attr( 'href' ); // harvardRef
te.$ref = teHref &&
$( '#' + $.escapeSelector( teHref.slice( 1 ) ) );
if ( !te.$ref || !te.$ref.length || !te.$ref.text() ) {
te.noRef = true;
return;
}
}


if ( !tooltipInitiallyPresent && !te.comment ) {
    // Hide the notice if it has already been dismissed
viewportTop = $window.scrollTop();
    if (localStorage.getItem(key) === 'true') {
refOffsetTop = te.$ref.offset().top;
      notice.hide();
if (
    }
!activatedByClick &&
viewportTop < refOffsetTop &&
viewportTop + $window.height() > refOffsetTop + te.$ref.height() &&
// There can be gadgets/scripts that make references horizontally scrollable.
$window.width() > te.$ref.offset().left + te.$ref.width()
) {
// Highlight the reference itself
te.$ref.addClass( 'rt-target' );
return;
}
}


if ( !te.tooltip ) {
    // Enable dismissal
te.tooltip = new Tooltip( te );
    notice.find('.dismiss-button').on('click', function () {
if ( !te.tooltip.$content.length ) {
      notice.hide();
return;
      localStorage.setItem(key, 'true');
}
    });
}


// If this tooltip is called from inside another tooltip. We can't define it
    // Track if the content has changed to clear the dismissal
// in the constructor since a ref can be cloned but have the same Tooltip object;
    const originalContent = notice.html();
// so, Tooltip.parent is a floating value.
    const mutationObserver = new MutationObserver(function () {
te.tooltip.parent = te.$element.closest( '.rt-tooltip' ).data( 'tooltip' );
      const currentContent = notice.html();
if ( te.tooltip.parent && te.tooltip.parent.disappearing ) {
      if (originalContent !== currentContent) {
return;
        localStorage.removeItem(key); // Clear the dismissal if content changes
}
      }
    });


te.tooltip.show();
    mutationObserver.observe(notice[0], { childList: true, subtree: true });
  });


if ( tooltipInitiallyPresent ) {
  // Add Reset Notices tab to action menu
if ( te.tooltip.$element.hasClass( 'rt-tooltip-above' ) ) {
  mw.util.addPortletLink(
te.tooltip.$element.addClass( CLASSES.FADE_IN_DOWN );
    'p-cactions', // Add to the "Actions" menu
} else {
    '#',
te.tooltip.$element.addClass( CLASSES.FADE_IN_UP );
    'Reset Notices',
}
    'ca-reset-notices',
return;
    'Reset all dismissed notices on this page',
}
    null,
    '#ca-history'
  );


te.tooltip.calculatePosition( ePageX, ePageY );
  // Reset logic
  $('#ca-reset-notices a').on('click', function (e) {
    e.preventDefault();


$window.on( 'resize.rt', te.onWindowResize );
    const prefix = `dismissed-notice-${pageTitle}-`;
}
    const keysToRemove = [];


// We redefine this.$element here because e.target can be a reference link inside
    // Collect all localStorage keys that are related to dismissed notices
// a reference tooltip, not a link that was initially assigned to this.$element
    for (let i = 0; i < localStorage.length; i++) {
this.$element = $element;
      const key = localStorage.key(i);
      if (key && key.startsWith(prefix)) {
        keysToRemove.push(key);
      }
    }


if ( this.type === 'commentedText' ) {
    // Remove all matching keys
this.$element.attr( 'title', '' );
    keysToRemove.forEach(key => localStorage.removeItem(key));
}


if ( activatedByClick ) {
    // Reload to re-show all notices
if (
    location.reload();
tooltipInitiallyPresent ||
  });
( this.$ref && this.$ref.hasClass( 'rt-target' ) )
) {
return;
} else {
setTimeout( function () {
$body.on( 'click.rt touchstart.rt', te.onBodyClick );
}, 0 );
}
}


if ( activatedByClick || tooltipInitiallyPresent ) {
  // Basic hash function
reallyShow();
  function generateHash(str) {
} else {
    let hash = 0;
this.showTimer = setTimeout( reallyShow, delay );
    for (let i = 0; i < str.length; i++) {
}
      hash = ((hash << 5) - hash) + str.charCodeAt(i);
};
      hash |= 0; // Convert to 32bit integer
    }
    return hash;
  }
});


this.onBodyClick = function ( e ) {
/* Allows bullet points in to-do list in Template:User to toggle on and off. */
if ( !te.tooltip && !( te.$ref && te.$ref.hasClass( 'rt-target' ) ) ) {
$(function() {
return;
  const bullets = document.querySelectorAll('.toggle-bullet');
}
  const bulletChars = ['<span style="color: green">&#9679;</span>', '&#9702;'];
  const localStorageKey = 'userInfoboxBulletState';


var $current = $( e.target );
  bullets.forEach((bullet, index) => {
    let currentIndex = localStorage.getItem(localStorageKey + index) === '0' ? 0 : 1; // Default to black if no stored state
    bullet.innerHTML = bulletChars[currentIndex];


function contextMatchesParameter( parameter ) {
    bullet.addEventListener('click', function() {
return this === parameter;
      currentIndex = (currentIndex + 1) % bulletChars.length;
}
      this.innerHTML = bulletChars[currentIndex];
      localStorage.setItem(localStorageKey + index, currentIndex.toString());
    });
  });
});


// The last condition is used to determine cases when a clicked tooltip is the current
/* EDITORIAL NOTATION TOOPTIP INCREASED SPEED */
// element's tooltip or one of its descendants
$(function() {
while (
  $('.editorial-notation-tooltip').on('mouseover', function(e) {
$current.length &&
    var $this = $(this);
(
    var $innerSpan = $this.find('span');
!$current.hasClass( 'rt-tooltip' ) ||
    var titleText = $innerSpan.attr('title');
!$current.data( 'tooltip' ) ||
!$current.data( 'tooltip' ).upToTopParent(
contextMatchesParameter, [ te.tooltip ],
true
)
)
) {
$current = $current.parent();
}
if ( !$current.length ) {
te.hideRef();
}
};


this.onWindowResize = function () {
    if (titleText) {
te.tooltip.calculatePosition();
      var $tooltip = $('<div class="editorial-notation-tooltip-box"></div>').text(titleText);
};
      $('body').append($tooltip);
}


function Tooltip( te ) {
      $tooltip.css({
function openSettingsDialog() {
        top: e.pageY + 10,
var settingsDialog, settingsWindow;
        left: e.pageX + 10,
        position: 'absolute'
      }).fadeIn('normal');


if ( cursorWaitCss ) {
      $innerSpan.data('original-title', titleText).removeAttr('title'); // Store and remove title
cursorWaitCss.disabled = true;
    }
}
  }).on('mouseout', function() {
    var $this = $(this);
    var $innerSpan = $this.find('span');
    var tooltipText = $('.editorial-notation-tooltip-box').text();


function SettingsDialog() {
    $('.editorial-notation-tooltip-box').fadeOut('normal', function() {
SettingsDialog.parent.call( this );
      $(this).remove();
}
      $innerSpan.attr('title', $innerSpan.data('original-title')); // Restore the original title
OO.inheritClass( SettingsDialog, OO.ui.ProcessDialog );
    });
  }).on('mousemove', function(e) {
    $('.editorial-notation-tooltip-box').css({
      top: e.pageY + 10,
      left: e.pageX + 10
    });
  });
});
$(function() {
  // Remove default link tooltip for citation needed links
  $('.editorial-notation-tooltip a').removeAttr('title');
});


SettingsDialog.static.name = 'settingsDialog';
/* MAKES "CONTENT PAGES" THE DEFUALT TAB WHEN SEARCHING (INSTALLING SMW MADE THE DEFAULT "ADVANCED") */
SettingsDialog.static.title = mw.msg( 'rt-settings-title' );
$(function () {
SettingsDialog.static.actions = [
    if (mw.config.get('wgCanonicalSpecialPageName') === 'Search') {
{
        const urlParams = new URLSearchParams(window.location.search);
modes: 'main',
        if (!urlParams.has('profile')) {
action: 'save',
            urlParams.set('profile', 'default'); // Use Content pages
label: mw.msg( 'rt-save' ),
            window.location.search = urlParams.toString();
flags: [ 'primary', 'progressive' ]
        }
},
    }
{
});
modes: 'main',
flags: [ 'safe', 'close' ]
},
{
modes: 'disabled',
action: 'deactivated',
label: mw.msg( 'rt-done' ),
flags: [ 'primary', 'progressive' ]
}
];


SettingsDialog.prototype.initialize = function () {
/* Initial Spoiler Pop-up */
var dialog = this;
function showDialog (name, text) {
  var container = document.createElement('div');
  container.style.display = 'flex';
  container.style.alignItems = 'center';
  container.style.justifyContent = 'center';
  container.style.position = 'fixed';
  container.style.left = '0';
  container.style.right = '0';
  container.style.top = '0';
  container.style.bottom = '0';
  container.style.zIndex = '10000';
  container.style.background = 'rgba(0, 0, 0, 0.5)';
  container.style.filter = 'initial';


SettingsDialog.parent.prototype.initialize.apply( this, arguments );
  var window = document.createElement('div');
  window.className = 'thereadandrite-spoiler-warning';
  window.style.boxShadow = '0 4px 30px 0 rgba(0, 0, 0, 0.2)';
  window.style.width = '90%';
  window.style.maxWidth = '600px';
  window.style.borderRadius = '3px';
  window.style.background = 'white';
  window.style.display = 'flex';
  window.style.flexDirection = 'column';
  window.style.boxSizing = 'border-box';
  window.style.padding = '16px';
  window.style.fontSize = '1.5em';
  window.style.maxHeight = '90%';
  container.appendChild(window);


this.enableCheckbox = new OO.ui.CheckboxInputWidget( {
  window.innerHTML = '<h2>⚠️ Spoiler Warning</h2>';
selected: true
  window.innerHTML += text;
} );
this.enableCheckbox.on( 'change', function ( selected ) {
dialog.activationMethodSelect.setDisabled( !selected );
dialog.delayInput.setDisabled( !selected || dialog.clickOption.isSelected() );
dialog.tooltipsForCommentsCheckbox.setDisabled( !selected );
} );
this.enableField = new OO.ui.FieldLayout( this.enableCheckbox, {
label: mw.msg( 'rt-enable' ),
align: 'inline',
classes: [ 'rt-enableField' ]
} );


this.hoverOption = new OO.ui.RadioOptionWidget( {
  var button = document.createElement('button');
label: mw.msg( 'rt-hovering' )
  button.className = 'mw-ui-button mw-ui-destructive';
} );
  button.style.alignSelf = 'center';
this.clickOption = new OO.ui.RadioOptionWidget( {
  button.style.fontSize = '1em';
label: mw.msg( 'rt-clicking' )
  button.innerHTML = 'Proceed';
} );
  button.onclick = function () {
this.activationMethodSelect = new OO.ui.RadioSelectWidget( {
    localStorage.setItem(name, 'seen');
items: [ this.hoverOption, this.clickOption ]
    document.body.removeChild(container);
} );
    $(document.body).removeClass('spoiler-warning-blur');
this.activationMethodSelect.selectItem(
  }
activatedByClick ? this.clickOption : this.hoverOption
  window.appendChild(button);
);
this.activationMethodSelect.on( 'choose', function ( item ) {
dialog.delayInput.setDisabled( item === dialog.clickOption );
} );
this.activationMethodField = new OO.ui.FieldLayout( this.activationMethodSelect, {
label: mw.msg( 'rt-activationMethod' ),
align: 'top'
} );


this.delayInput = new OO.ui.NumberInputWidget( {
  var style = document.createElement('style');
input: { value: delay },
  style.type = 'text/css';
step: 50,
  style.innerHTML = '.spoiler-warning-blur > * { filter: blur(50px); }';
min: 0,
  document.getElementsByTagName('head')[0].appendChild(style);
max: 5000,
disabled: activatedByClick,
classes: [ 'rt-numberInput' ]
} );
this.delayField = new OO.ui.FieldLayout( this.delayInput, {
label: mw.msg( 'rt-delay' ),
align: 'top'
} );


this.tooltipsForCommentsCheckbox = new OO.ui.CheckboxInputWidget( {
  $(document.body).addClass('spoiler-warning-blur');
selected: tooltipsForComments
  document.body.appendChild(container);
} );
  $('#spoiler-warning__time-machine-link').click(function () { localStorage.setItem(name, 'seen'); })
this.tooltipsForCommentsField = new OO.ui.FieldLayout(
  $('#spoiler-warning__spoiler-policy-link').click(function () { localStorage.setItem(name, 'seen'); })
this.tooltipsForCommentsCheckbox,
{
label: new OO.ui.HtmlSnippet( mw.msg( 'rt-tooltipsForComments' ) ),
align: 'inline',
classes: [ 'rt-tooltipsForCommentsField' ]
}
);
new TooltippedElement(
this.tooltipsForCommentsField.$element.find(
'.' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' )
)
);
 
this.fieldset = new OO.ui.FieldsetLayout();
this.fieldset.addItems( [
this.enableField,
this.activationMethodField,
this.delayField,
this.tooltipsForCommentsField
] );
 
this.panelSettings = new OO.ui.PanelLayout( {
padded: true,
expanded: false
} );
this.panelSettings.$element.append( this.fieldset.$element );
 
this.panelDisabled = new OO.ui.PanelLayout( {
padded: true,
expanded: false
} );
this.panelDisabled.$element.append(
$( '<table>' )
.addClass( 'rt-disabledHelp' )
.append(
$( '<tr>' ).append(
$( '<td>' ).append(
$( '<img>' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/c/c0/MediaWiki_footer_link_ltr.svg' )
),
$( '<td>' )
.addClass( 'rt-disabledNote' )
.text( mw.msg( 'rt-disabledNote' ) )
)
)
);
 
this.stackLayout = new OO.ui.StackLayout( {
items: [ this.panelSettings, this.panelDisabled ]
} );
 
this.$body.append( this.stackLayout.$element );
};
 
SettingsDialog.prototype.getSetupProcess = function ( data ) {
return SettingsDialog.parent.prototype.getSetupProcess.call( this, data )
.next( function () {
this.stackLayout.setItem( this.panelSettings );
this.actions.setMode( 'main' );
}, this );
};
 
SettingsDialog.prototype.getActionProcess = function ( action ) {
var dialog = this;
 
if ( action === 'save' ) {
return new OO.ui.Process( function () {
var newDelay = Number( dialog.delayInput.getValue() );
 
enabled = dialog.enableCheckbox.isSelected();
if ( newDelay >= 0 && newDelay <= 5000 ) {
delay = newDelay;
}
activatedByClick = dialog.clickOption.isSelected();
tooltipsForComments = dialog.tooltipsForCommentsCheckbox.isSelected();
 
setSettingsCookie();
 
if ( enabled ) {
dialog.close();
disableRt();
rt( $content );
} else {
dialog.actions.setMode( 'disabled' );
dialog.stackLayout.setItem( dialog.panelDisabled );
disableRt();
addEnableLink();
}
} );
} else if ( action === 'deactivated' ) {
dialog.close();
}
return SettingsDialog.parent.prototype.getActionProcess.call( this, action );
};
 
SettingsDialog.prototype.getBodyHeight = function () {
return this.stackLayout.getCurrentItem().$element.outerHeight( true );
};
 
tooltip.upToTopParent( function adjustRightAndHide() {
if ( this.isPresent ) {
if ( this.$element[ 0 ].style.right ) {
this.$element.css(
'right',
'+=' + ( window.innerWidth - $window.width() )
);
}
this.te.hideRef( true );
}
} );
 
if ( !windowManager ) {
windowManager = new OO.ui.WindowManager();
$body.append( windowManager.$element );
}
 
settingsDialog = new SettingsDialog();
windowManager.addWindows( [ settingsDialog ] );
settingsWindow = windowManager.openWindow( settingsDialog );
settingsWindow.opened.then( function () {
settingsDialogOpening = false;
} );
settingsWindow.closed.then( function () {
windowManager.clearWindows();
} );
}
 
var tooltip = this;
 
// This variable can change: one tooltip can be called from a harvard-style reference link
// that is put into different tooltips
this.te = te;
 
switch ( this.te.type ) {
case 'supRef':
this.id = 'rt-' + this.te.$originalElement.attr( 'id' );
this.$content = this.te.$ref
.contents()
.filter( function ( i ) {
var $this = $( this );
return (
this.nodeType === Node.TEXT_NODE ||
!(
$this.is( '.mw-cite-backlink' ) ||
(
i === 0 &&
// Template:Cnote, Template:Note
( $this.is( 'b' ) ||
// Template:Note_label
$this.is( 'a' ) &&
$this.attr( 'href' ).indexOf( '#ref' ) === 0
)
)
)
);
} )
.clone( true );
break;
case 'harvardRef':
this.id = 'rt-' + this.te.$originalElement.closest( 'li' ).attr( 'id' );
this.$content = this.te.$ref
.clone( true )
.removeAttr( 'id' );
break;
case 'commentedText':
this.id = 'rt-' + String( Math.random() ).slice( 2 );
this.$content = $( document.createTextNode( this.te.comment ) );
break;
}
if ( !this.$content.length ) {
return;
}
 
this.isInsideWindow = Boolean( this.te.$element.closest( '.oo-ui-window' ).length );
 
this.$element = $( '<div>' )
.addClass( 'rt-tooltip' )
.attr( 'id', this.id )
.attr( 'role', 'tooltip' )
.data( 'tooltip', this );
 
var $hoverArea = $( '<div>' )
.addClass( 'rt-hoverArea' )
.appendTo( this.$element );
var $scroll = $( '<div>' )
.addClass( 'rt-scroll' )
.appendTo( $hoverArea );
 
this.$content = this.$content
.wrapAll( '<div>' )
.parent()
.addClass( 'rt-content' )
.addClass( 'mw-parser-output' )
.appendTo( $scroll );
 
if ( !activatedByClick ) {
this.$element
.mouseenter( function () {
if ( !tooltip.disappearing ) {
tooltip.upToTopParent( function () {
this.show();
} );
}
} )
.mouseleave( function ( e ) {
// https://stackoverflow.com/q/47649442 workaround. Relying on relatedTarget
// alone has pitfalls: when alt-tabbing, relatedTarget is empty too
if (
CLIENT_NAME !== 'chrome' ||
(
!e.originalEvent ||
e.originalEvent.relatedTarget !== null ||
!tooltip.clickedTime ||
$.now() - tooltip.clickedTime > 50
)
) {
tooltip.upToTopParent( function () {
this.te.hideRef();
} );
}
} )
.click( function () {
tooltip.clickedTime = $.now();
} );
}
 
if ( !this.isInsideWindow ) {
$( '<a>' )
.addClass( 'rt-settingsLink' )
.attr( 'role', 'button' )
.attr( 'href', '#' )
.attr( 'title', mw.msg( 'rt-settings' ) )
.click( function ( e ) {
e.preventDefault();
if ( settingsDialogOpening ) {
return;
}
settingsDialogOpening = true;
 
if ( mw.loader.getState( 'oojs-ui' ) !== 'ready' ) {
if ( cursorWaitCss ) {
cursorWaitCss.disabled = false;
} else {
cursorWaitCss = mw.util.addCSS( 'body { cursor: wait; }' );
}
}
mw.loader.using( [ 'oojs', 'oojs-ui' ], openSettingsDialog );
} )
.prependTo( this.$content );
}
 
// Tooltip tail element is inside tooltip content element in order for the tooltip
// not to disappear when the mouse is above the tail
this.$tail = $( '<div>' )
.addClass( 'rt-tail' )
.prependTo( this.$element );
 
this.disappearing = false;
 
this.show = function () {
this.disappearing = false;
clearTimeout( this.te.hideTimer );
clearTimeout( this.te.removeTimer );
 
this.$element
.removeClass( CLASSES.FADE_OUT_DOWN )
.removeClass( CLASSES.FADE_OUT_UP );
 
if ( !this.isPresent ) {
$overlay.append( this.$element );
}
 
this.isPresent = true;
};
 
this.hide = function () {
var tooltip = this;
 
tooltip.disappearing = true;
 
if ( tooltip.$element.hasClass( 'rt-tooltip-above' ) ) {
tooltip.$element
.removeClass( CLASSES.FADE_IN_DOWN )
.addClass( CLASSES.FADE_OUT_UP );
} else {
tooltip.$element
.removeClass( CLASSES.FADE_IN_UP )
.addClass( CLASSES.FADE_OUT_DOWN );
}
 
tooltip.te.removeTimer = setTimeout( function () {
if ( tooltip.isPresent ) {
tooltip.$element.detach();
 
tooltip.$tail.css( 'left', '' );
 
if ( activatedByClick ) {
$body.off( 'click.rt touchstart.rt', tooltip.te.onBodyClick );
}
$window.off( 'resize.rt', tooltip.te.onWindowResize );
 
tooltip.isPresent = false;
}
}, 200 );
};
 
this.calculatePosition = function ( ePageX, ePageY ) {
var teElement, teOffsets, teOffset, targetTailOffsetX, tailLeft;
 
this.$tail.css( 'left', '' );
 
teElement = this.te.$element.get( 0 );
if ( ePageX !== undefined ) {
targetTailOffsetX = ePageX;
teOffsets = ( teElement.getClientRects && teElement.getClientRects() ) ||
teElement.getBoundingClientRect();
if ( teOffsets.length > 1 ) {
for ( var i = teOffsets.length - 1; i >= 0; i-- ) {
if (
ePageY >= Math.round( $window.scrollTop() + teOffsets[ i ].top ) &&
ePageY <= Math.round(
$window.scrollTop() + teOffsets[i].top + teOffsets[ i ].height
)
) {
teOffset = teOffsets[ i ];
}
}
}
}
 
if ( !teOffset ) {
teOffset = ( teElement.getClientRects && teElement.getClientRects()[ 0 ] ) ||
teElement.getBoundingClientRect();
}
teOffset = {
top: $window.scrollTop() + teOffset.top,
left: $window.scrollLeft() + teOffset.left,
width: teOffset.width,
height: teOffset.height
};
if ( !targetTailOffsetX ) {
targetTailOffsetX = teOffset.left + ( teOffset.width / 2 );
}
 
// Value of `left` in `.rt-tooltip-above .rt-tail`
var defaultTailLeft = 19;
 
// Value of `width` in `.rt-tail`
var tailSideWidth = 13;
 
// We tilt the square 45 degrees, so we need square root to calculate the distance.
var tailWidth = tailSideWidth * Math.SQRT2;
var tailHeight = tailWidth / 2;
var tailCenterDelta = tailSideWidth + 1 - ( tailWidth / 2 );
 
var tooltip = this;
var getTop = function ( isBelow ) {
var delta = isBelow ?
teOffset.height + tailHeight :
-tooltip.$element.outerHeight() - tailHeight + 1;
return teOffset.top + delta;
};
 
this.$element.css( {
top: getTop(),
left: targetTailOffsetX - defaultTailLeft - tailCenterDelta,
right: ''
} );
 
// Is it squished against the right side of the page?
if ( this.$element.offset().left + this.$element.outerWidth() > $window.width() - 1 ) {
this.$element.css( {
left: '',
right: 0
} );
tailLeft = targetTailOffsetX - this.$element.offset().left - tailCenterDelta;
}
 
// Is a part of it above the top of the screen?
if ( teOffset.top < this.$element.outerHeight() + $window.scrollTop() + tailHeight ) {
this.$element
.removeClass( 'rt-tooltip-above' )
.addClass( 'rt-tooltip-below' )
.addClass( CLASSES.FADE_IN_UP )
.css( {
top: getTop( true )
} );
if ( tailLeft ) {
this.$tail.css( 'left', ( tailLeft + tailSideWidth ) + 'px' );
}
} else {
this.$element
.removeClass( 'rt-tooltip-below' )
.addClass( 'rt-tooltip-above' )
.addClass( CLASSES.FADE_IN_DOWN )
// A fix for cases when a tooltip shown once is then wrongly positioned when it
// is shown again after a window resize.
.css( {
top: getTop()
} );
if ( tailLeft ) {
this.$tail.css( 'left', tailLeft + 'px' );
}
}
};
 
// Run some function for all the tooltips up to the top one in a tree. Its context will be
// the tooltip, while its parameters may be passed to Tooltip.upToTopParent as an array
// in the second parameter. If the third parameter passed to ToolTip.upToTopParent is true,
// the execution stops when the function in question returns true for the first time,
// and ToolTip.upToTopParent returns true as well.
this.upToTopParent = function ( func, parameters, stopAtTrue ) {
var returnValue,
currentTooltip = this;
 
do {
returnValue = func.apply( currentTooltip, parameters );
if ( stopAtTrue && returnValue ) {
break;
}
} while ( ( currentTooltip = currentTooltip.parent ) );
 
if ( stopAtTrue ) {
return returnValue;
}
};
}
 
if ( !enabled ) {
addEnableLink();
return;
}
 
teSelector = REF_LINK_SELECTOR;
if ( tooltipsForComments ) {
teSelector += ', ' + COMMENTED_TEXT_SELECTOR;
}
$content.find( teSelector ).each( function () {
new TooltippedElement( $( this ) );
} );
}
}
settingsString = mw.cookie.get( 'RTsettings', '' );
if ( settingsString ) {
settings = settingsString.split( '|' );
enabled = Boolean( Number( settings[ 0 ] ) );
delay = Number( settings[ 1 ] );
activatedByClick = Boolean( Number( settings[ 2 ] ) );
// The forth value was added later, so we provide for a default value. See comments below
// for why we use "IS_TOUCHSCREEN && IS_MOBILE".
tooltipsForComments = settings[ 3 ] === undefined ?
IS_TOUCHSCREEN && IS_MOBILE :
Boolean( Number( settings[ 3 ] ) );
} else {
enabled = true;
delay = 200;
// Since the mobile browser check is error-prone, adding IS_MOBILE condition here would probably
// leave cases where a user interacting with the browser using touches doesn't know how to call
// a tooltip in order to switch to activation by click. Some touch-supporting laptop users
// interacting by touch (though probably not the most popular use case) would not be happy too.
activatedByClick = IS_TOUCHSCREEN;
// Arguably we shouldn't convert native tooltips into gadget tooltips for devices that have
// mouse support, even if they have touchscreens (there are laptops with touchscreens).
// IS_TOUCHSCREEN check here is for reliability, since the mobile check is prone to false
// positives.
tooltipsForComments = IS_TOUCHSCREEN && IS_MOBILE;
}
mw.hook( 'wikipage.content' ).add( rt );
}() );