|
|
| (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">●</span>', '◦']; |
| | 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 );
| |
|
| |
| }() );
| |