Toggle menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js

MediaWiki interface page
Revision as of 12:03, 1 August 2025 by CriticalResist (talk | contribs) (bringing back 'older' reading mode opti that worked. May probably take another look at it later.)

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)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */

//Matomo accurate tracking (no more 0 second visits)
//_paq.push(['enableHeartBeatTimer']);

/**
 * Dynamic Navigation Bars. See [[Wikipedia:NavFrame]]
 * 
 * Based on script from en.wikipedia.org, 2008-09-15.
 *
 * @source www.mediawiki.org/wiki/MediaWiki:Gadget-NavFrame.js
 * @maintainer Helder.wiki, 2012–2013
 * @maintainer Krinkle, 2013
 */
( function () {

// Set up the words in your language
var collapseCaption = 'hide';
var expandCaption = 'show';

var navigationBarHide = '[' + collapseCaption + ']';
var navigationBarShow = '[' + expandCaption + ']';

/**
 * Shows and hides content and picture (if available) of navigation bars.
 *
 * @param {number} indexNavigationBar The index of navigation bar to be toggled
 * @param {jQuery.Event} e Event object
 */
function toggleNavigationBar( indexNavigationBar, e ) {
	var navChild,
		navToggle = document.getElementById( 'NavToggle' + indexNavigationBar ),
		navFrame = document.getElementById( 'NavFrame' + indexNavigationBar );

	// Prevent browser from jumping to href "#"
	e.preventDefault();

	if ( !navFrame || !navToggle ) {
		return false;
	}

	// If shown now
	if ( navToggle.firstChild.data === navigationBarHide ) {
		for ( navChild = navFrame.firstChild; navChild !== null; navChild = navChild.nextSibling ) {
			if ( $( navChild ).hasClass( 'NavContent' ) || $( navChild ).hasClass( 'NavPic' ) ) {
				navChild.style.display = 'none';
			}
		}
		navToggle.firstChild.data = navigationBarShow;

	// If hidden now
	} else if ( navToggle.firstChild.data === navigationBarShow ) {
		for ( navChild = navFrame.firstChild; navChild !== null; navChild = navChild.nextSibling ) {
			if ( $( navChild ).hasClass( 'NavContent' ) || $( navChild ).hasClass( 'NavPic' ) ) {
				navChild.style.display = 'block';
			}
		}
		navToggle.firstChild.data = navigationBarHide;
	}
}

/** Adds show/hide-button to navigation bars.
 * @param {jQuery} $content
 */
function createNavigationBarToggleButton( $content ) {
	var i, j, navChild, navToggle, navToggleText, isCollapsed,
		indexNavigationBar = 0;
	// iterate over all < div >-elements
	var $divs = $content.find( 'div.NavFrame' );
	$divs.each( function ( i, navFrame ) {
		indexNavigationBar++;
		navToggle = document.createElement( 'a' );
		navToggle.className = 'NavToggle';
		navToggle.setAttribute( 'id', 'NavToggle' + indexNavigationBar );
		navToggle.setAttribute( 'href', '#' );
		$( navToggle ).on( 'click', $.proxy( toggleNavigationBar, null, indexNavigationBar ) );

		isCollapsed = $( navFrame ).hasClass( 'collapsed' );
		// backwards compatibility for old technique where the collapsed class was not yet used
		for ( navChild = navFrame.firstChild; navChild !== null && !isCollapsed; navChild = navChild.nextSibling ) {
			if ( $( navChild ).hasClass( 'NavPic' ) || $( navChild ).hasClass( 'NavContent' ) ) {
				if ( navChild.style.display === 'none' ) {
					isCollapsed = true;
				}
			}
		}
		if ( isCollapsed ) {
			for ( navChild = navFrame.firstChild; navChild !== null; navChild = navChild.nextSibling ) {
				if ( $( navChild ).hasClass( 'NavPic' ) || $( navChild ).hasClass( 'NavContent' ) ) {
					navChild.style.display = 'none';
				}
			}
		}
		navToggleText = document.createTextNode( isCollapsed ? navigationBarShow : navigationBarHide );
		navToggle.appendChild( navToggleText );

		// Find the NavHead and attach the toggle link (Must be this complicated because Moz's firstChild handling is borked)
		for ( j = 0; j < navFrame.childNodes.length; j++ ) {
			if ( $( navFrame.childNodes[j] ).hasClass( 'NavHead' ) ) {
				navToggle.style.color = navFrame.childNodes[j].style.color;
				navFrame.childNodes[j].appendChild( navToggle );
			}
		}
		navFrame.setAttribute( 'id', 'NavFrame' + indexNavigationBar );
	} );
}

mw.hook( 'wikipage.content' ).add( createNavigationBarToggleButton );

}());

/** Side bar customization */
function ModifySidebar( action, section, name, link ) {
	try {
		switch ( section ) {
			case 'languages':
				var target = 'p-lang';
				break;
			case 'toolbox':
				var target = 'p-tb';
				break;
			case 'navigation':
				var target = 'p-navigation';
				break;
			default:
				var target = 'p-' + section;
				break;
		}

		if ( action == 'add' ) {
			var node = document.getElementById( target )
							   .getElementsByTagName( 'div' )[0]
							   .getElementsByTagName( 'ul' )[0];

			var aNode = document.createElement( 'a' );
			var liNode = document.createElement( 'li' );

			aNode.appendChild( document.createTextNode( name ) );
			aNode.setAttribute( 'href', link );
			liNode.appendChild( aNode );
			liNode.className = 'plainlinks';
			node.appendChild( liNode );
		}

		if ( action == 'remove' ) {
			var list = document.getElementById( target )
							   .getElementsByTagName( 'div' )[0]
							   .getElementsByTagName( 'ul' )[0];

			var listelements = list.getElementsByTagName( 'li' );

			for ( var i = 0; i < listelements.length; i++ ) {
				if (
					listelements[i].getElementsByTagName( 'a' )[0].innerHTML == name ||
					listelements[i].getElementsByTagName( 'a' )[0].href == link
				)
				{
					list.removeChild( listelements[i] );
				}
			}
		}


	} catch( e ) {
		// let's just ignore what's happened
		return;
	}
}

function CustomizeModificationsOfSidebar() {
	// adds [[Special:CategoryTree|Special:CategoryTree]] to toolbox
	ModifySidebar( 'add', 'toolbox', 'List of uploaded files', 'https://en.prolewiki.org/wiki/Special:ListFiles' );
}

jQuery( CustomizeModificationsOfSidebar );

/** ==================== Citizen Skin Enhancements (sidebar icons, heading hyperlinks, collection refresh) ==================== **/
$(function() {
    // Add sidebar icons in correct order
    var $header = $('.citizen-drawer');
    if ($header.length) {
        var icons = [
            {cls: 'sidebar-library', href: '/wiki/ProleWiki:Library', title: 'Library'},
            {cls: 'sidebar-essays', href: '/wiki/ProleWiki:Essays', title: 'Essays'}
        ];
        
        if (!mw.user.isAnon()) {
            icons.push({cls: 'sidebar-upload', href: '/wiki/Special:Upload', title: 'Upload file'});
        }
        
        // Create elements in reverse order to ensure correct positioning
        for (var i = icons.length - 1; i >= 0; i--) {
            var icon = icons[i];
            $header.after(
                '<a class="citizen-header__item ' + icon.cls + 
                '" href="' + icon.href + '" title="' + icon.title + '">' +
                '<div class="citizen-header__button"></div></a>'
            );
        }
    }

    // Heading hyperlinks
    function setupHeadingLinks() {
        var $headings = $('h2, h3, h4, h5').find('.mw-headline');
        if (!$headings.length) return;

        function smoothScroll(targetId) {
            var target = document.getElementById(targetId);
            if (target) {
                window.scrollTo({
                    top: target.offsetTop,
                    behavior: 'smooth'
                });
            }
        }

        // Create hyperlink buttons with proper event handling
        $headings.each(function() {
            var $headline = $(this),
                id = $headline.attr('id');
            
            if (!id) return;
            
            var $link = $(
                '<a href="#' + id + '" class="heading-hyperlink citizen-ui-icon mw-ui-icon-wikimedia-link" ' +
                'title="Direct link to this section" aria-label="Direct link to this section" data-no-preview="true"></a>'
            );
            
            // Add specific click handler to prevent section collapse
            $link.on('click', function(e) {
                e.preventDefault();
                e.stopPropagation(); // Prevent event from bubbling to section toggle
                smoothScroll(id);
                history.replaceState(null, null, '#' + id);
            });
            
            $headline.append($link);
        });

        // Handle other anchor clicks (not the hyperlink buttons)
        $(document).on('click', 'a[href^="#"]:not(.heading-hyperlink)', function(e) {
            var href = this.href;
            var id = href.substring(href.indexOf('#') + 1);
            if (!id) return;
            
            e.preventDefault();
            smoothScroll(id);
            history.replaceState(null, null, '#' + id);
        });
    }
    setupHeadingLinks();

    // Collection rendering check
    if ($("#renderingProgress").length) {
        setTimeout(function() {
            location.reload();
        }, 5000);
    }
});

/** ==================== Combined Preferences Setup (custom typefaces, themes) ==================== **/
$(function() {
    // Shared setup for both theme and font pickers
    function initPreferences() {
        setupThemePicker();
        setupFontPicker();
    }
    
    /** ===== Theme Picker ===== **/
    function setupThemePicker() {
        var html = document.documentElement;
        var currentMonth = new Date().getMonth();
        var isJune = currentMonth === 5;
        var themes = [
            {value: 'day', label: 'Light', className: 'skin-citizen-light'},
            {value: 'os', label: 'Auto', className: 'skin-citizen-auto'},
            {value: 'night', label: 'Dark', className: 'skin-citizen-dark'},
            {value: 'sepia', label: 'Sepia', className: 'skin-citizen-sepia', labelId: 'sepia-style-label'}
        ];

        if (isJune) {
            themes.push({
                value: 'trans', 
                label: 'Trans', 
                className: 'skin-citizen-trans',
                labelId: 'trans-style-label'
            });
        }

        function applyTheme(themeValue) {
            // Batch class operations by looping and removing each class
            for (var i = 0; i < themes.length; i++) {
                html.classList.remove(themes[i].className);
            }
            
            for (var j = 0; j < themes.length; j++) {
                if (themes[j].value === themeValue) {
                    html.classList.add(themes[j].className);
                    break;
                }
            }
            localStorage.setItem('skin-theme-clientpref', themeValue);
        }

        var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day';
        if (storedTheme === 'trans' && !isJune) storedTheme = 'day';
        applyTheme(storedTheme);

        function addThemeOptions() {
            var themeContainer = document.getElementById('skin-client-prefs-skin-theme');
            if (!themeContainer) {
                return setTimeout(addThemeOptions, 100);
            }
            
            var form = themeContainer.querySelector('form');
            var template = form.querySelector('.citizen-client-prefs-radio');
            if (!template) return;
            
            for (var i = 0; i < themes.length; i++) {
                var theme = themes[i];
                var radioId = 'skin-client-pref-skin-theme-value-' + theme.value;
                if (document.getElementById(radioId)) continue;
                
                var radioElement = template.cloneNode(true);
                var input = radioElement.querySelector('input');
                var label = radioElement.querySelector('label');
                
                input.id = radioId;
                input.value = theme.value;
                input.name = 'skin-client-pref-skin-theme-group';
                input.checked = (storedTheme === theme.value);
                
                label.setAttribute('for', radioId);
                label.textContent = theme.label;
                
                if (theme.labelId) {
                    label.id = theme.labelId;
                }
                
                form.appendChild(radioElement);
            }
        }

        $(document).on('change', 'input[name="skin-client-pref-skin-theme-group"]', function() {
            applyTheme(this.value);
        });
        
        addThemeOptions();
    }

    /** ===== Typeface Picker ===== **/
function setupFontPicker() {
    var html = document.documentElement;
    var FONT_STORAGE_KEY = 'skin-font-clientpref';
    var TEXT_SIZE_ID = 'skin-client-prefs-citizen-feature-custom-font-size';
    
    var fonts = [
        {value: 'sans', label: 'Sans', className: 'font-sans'},
        {value: 'serif', label: 'Serif', className: 'font-serif'},
        {value: 'dyslexic', label: 'Accessibility', className: 'font-dyslexic'}
    ];

    var activeFont = localStorage.getItem(FONT_STORAGE_KEY) || 'sans';

    function applyFont(fontValue) {
        for (var i = 0; i < fonts.length; i++) {
            html.classList.remove(fonts[i].className);
        }
        
        if (fontValue !== 'sans') {
            for (var j = 0; j < fonts.length; j++) {
                if (fonts[j].value === fontValue) {
                    html.classList.add(fonts[j].className);
                    break;
                }
            }
        }
        localStorage.setItem(FONT_STORAGE_KEY, fontValue);
    }

    applyFont(activeFont);

    function addFontOptions() {
        var fontSection = document.getElementById('skin-client-prefs-font');
        if (fontSection) return;
        
        var prefsPanel = document.getElementById('citizen-client-prefs');
        if (!prefsPanel) {
            return setTimeout(addFontOptions, 100);
        }
        
        // Create new section using string HTML (ES5 safe)
        var section = document.createElement('div');
        section.className = 'mw-portlet mw-portlet-js citizen-menu';
        section.id = 'skin-client-prefs-font';

        section.innerHTML = 
            '<div class="citizen-menu__heading">Typeface</div>' +
            '<div class="citizen-menu__content">' +
                '<ul class="citizen-menu__content-list">' +
                    '<li class="mw-list-item mw-list-item-js">' +
                        '<form></form>' +
                    '</li>' +
                '</ul>' +
            '</div>';
        
        var textSizeSection = document.getElementById(TEXT_SIZE_ID);
        if (textSizeSection) {
            if (textSizeSection.nextSibling) {
                textSizeSection.parentNode.insertBefore(section, textSizeSection.nextSibling);
            } else {
                textSizeSection.parentNode.appendChild(section);
            }
        } else {
            prefsPanel.appendChild(section);
        }
        
        var template = document.querySelector('.citizen-client-prefs-radio');
        var form = section.querySelector('form');
        if (!template || !form) return;
        
        for (var i = 0; i < fonts.length; i++) {
            var font = fonts[i];
            var radioId = 'skin-client-pref-font-value-' + font.value;
            
            var radioElement = template.cloneNode(true);
            var input = radioElement.querySelector('input');
            var label = radioElement.querySelector('label');
            
            input.id = radioId;
            input.value = font.value;
            input.name = 'skin-client-pref-font-group';
            input.checked = (activeFont === font.value);
            
            label.setAttribute('for', radioId);
            label.textContent = font.label;
            
            // Add custom font class to the label
            label.className = label.className + ' ' + font.className;
            
            // Add ID for styling if needed
            label.id = font.value + '-font-label';
            
            form.appendChild(radioElement);
        }
    }

    $(document).on('change', 'input[name="skin-client-pref-font-group"]', function() {
        applyFont(this.value);
    });
    
    addFontOptions();
}
    
    initPreferences();
});

/** ==================== Reading mode and features ==================== **/
/** (trigger, hoverline, progress bar) */
// Skip loading reading mode if not on desktop device:
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
$(function() {
    var html = document.documentElement,
        readingModeClass = 'reading-mode',
        isReadingMode = false;
    
    // Toggle reading mode
    function toggleReadingMode() {
        isReadingMode = !isReadingMode;
        html.classList.toggle(readingModeClass, isReadingMode);
        
        if (isReadingMode && window._paq) {
            _paq.push(['trackGoal', 12]);
        }
    }
    
    // Keyboard shortcut (desktop only)
    if (!('ontouchstart' in window)) {
        $(document).on('keydown', function(e) {
            if ((e.keyCode === 48 || e.keyCode === 96) && // 0 key
                !$(e.target).is('input, textarea, [contenteditable]')) {
                toggleReadingMode();
            }
        });
    }
    
    // Hover line feature
    function setupHoverLine() {
        var $line = $('<div id="hover-line"></div>').css({
            position: 'fixed',
            left: 0,
            width: '100%',
            height: '1.5em',
            pointerEvents: 'none',
            zIndex: 1,
            transition: 'top 0.1s ease, background-color 0.3s ease',
            display: 'none',
            backgroundColor: 'rgba(255,255,150,0.2)'
        }).appendTo('body');
        
        $(document).on('mousemove', function(e) {
            if (!isReadingMode) {
                $line.hide();
                return;
            }
            
            var el = document.elementFromPoint(e.clientX, e.clientY);
            if (!el) {
                $line.hide();
                return;
            }
            
            var paragraph = $(el).closest('p, li')[0];
            if (!paragraph) {
                $line.hide();
                return;
            }
            
            var range;
            if (document.caretRangeFromPoint) {
                range = document.caretRangeFromPoint(e.clientX, e.clientY);
            } else if (document.caretPositionFromPoint) {
                var pos = document.caretPositionFromPoint(e.clientX, e.clientY);
                if (pos) {
                    range = document.createRange();
                    range.setStart(pos.offsetNode, pos.offset);
                    range.collapse(true);
                }
            }
            
            if (!range) {
                $line.hide();
                return;
            }
            
            var rect = range.getBoundingClientRect();
            if (!rect || rect.height === 0) {
                $line.hide();
                return;
            }
            
            $line.css({
                top: rect.top + 'px',
                height: rect.height + 'px',
                display: 'block'
            });
        });
    }
    setupHoverLine();
    
    // Progress bar feature with hover tooltip
    function setupProgressBar() {
        // Create wrapper container
        var $wrapper = $('<div id="reading-progress-wrapper"></div>').css({
            position: 'fixed',
            bottom: 0,
            left: 0,
            width: '100%',
            height: '12px',
            zIndex: 9999,
            pointerEvents: 'auto'
        }).appendTo('body');
        
        // Create progress bar
        var $bar = $('<div id="reading-progress-bar"></div>').css({
            position: 'absolute',
            bottom: 0,
            left: 0,
            height: '4px',
            backgroundColor: 'var(--color-progress-bar)',
            width: '0%',
            transition: 'width 0.1s ease-out',
            pointerEvents: 'none'
        }).appendTo($wrapper);
        
        // Create tooltip
        var $tooltip = $('<div id="reading-progress-tooltip"></div>').css({
            position: 'fixed',
            padding: '2px 6px',
            backgroundColor: '#333',
            color: '#fff',
            fontSize: '1em',
            borderRadius: '3px',
            pointerEvents: 'none',
            zIndex: 10000,
            display: 'none'
        }).appendTo('body');
        
        var lastPercent = 0;
        
        // Update progress on scroll
        $(window).on('scroll', function() {
            if (!isReadingMode) {
                $wrapper.hide();
                return;
            }
            
            $wrapper.show();
            var scrollTop = $(window).scrollTop(),
                docHeight = $(document).height(),
                winHeight = $(window).height(),
                percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
            
            if (Math.abs(percent - lastPercent) > 0.5) {
                $bar.width(percent + '%');
                lastPercent = percent;
            }
        });
        
        // Show/hide tooltip on hover
        $wrapper.hover(
            function() {
                if (!isReadingMode) return;
                $tooltip.show();
            },
            function() {
                $tooltip.hide();
            }
        );
        
        // Update tooltip position and text
        $wrapper.on('mousemove', function(e) {
            if (!isReadingMode) return;
            
            var scrollTop = $(window).scrollTop(),
                docHeight = $(document).height(),
                winHeight = $(window).height(),
                percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
            
            $tooltip.text(Math.round(percent) + '%')
                   .css({
                       left: (e.clientX + 8) + 'px',
                       bottom: '16px'
                   });
        });
    }
    setupProgressBar();
});
} //end reading mode IF statement

/** ==================== Visual Editor Welcome Message ==================== **/
mw.loader.using(['ext.visualEditor.desktopArticleTarget.init']).then(function () {
    mw.hook('ve.activationComplete').add(function () {
        var checkDialog = setInterval(function () {
            var $dialogContent = $('.ve-init-mw-welcomeDialog-content');
            if ($dialogContent.length) {
                clearInterval(checkDialog);

                // Only append once
                if (!$dialogContent.data('custom-appended')) {
                    $dialogContent.append(
                        '<br><br>Please read our <strong><a href="/wiki/Principles">Principles</a></strong> and <strong><a href="/wiki/Encyclopedic_tone_guide">Encyclopedic tone guide</a><strong> first to make sure your edit is approved!'
                    );
                    $dialogContent.data('custom-appended', true);

                    // Resize the OOUI dialog to fit new content
                    var dialog = OO.ui.WindowManager.static.currentWindow;
                    if (dialog && typeof dialog.updateSize === 'function') {
                        dialog.updateSize();
                    }
                }
            }
        }, 200);
    });
});

/** ==================== Accreq Form Autosave ==================== **/
$(function() {
    var $textarea = $('#wpBio');
    if (!$textarea.length) return;
    
    var STORAGE_KEY = 'form-bio-autosave',
        $status = $('<div id="autosave-status" style="font-size:0.8em;color:gray;height:1.2em;margin-top:5px;"></div>'),
        lastValue = '';
    
    $textarea.after($status);
    
    // Restore saved content
    var saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
        $textarea.val(saved);
        lastValue = saved;
        showStatus('Restored previous work');
    }
    
    // Setup autosave
    $textarea.on('input', function() {
        var current = $textarea.val();
        if (current === lastValue) return;
        
        showStatus('Saving...');
        setTimeout(function() {
            localStorage.setItem(STORAGE_KEY, current);
            lastValue = current;
            showStatus('Saved locally');
        }, 2000);
    });
    
    // Clear on form submit
    $textarea.closest('form').on('submit', function() {
        localStorage.removeItem(STORAGE_KEY);
    });
    
    function showStatus(msg) {
        $status.text(msg).stop().fadeIn().delay(3000).fadeOut();
    }
});

/** ==================== Join us homepage testimonials ==================== **/
$(function() {
    var $joinUs = $('#joinUs'),
        $testimonial = $('#homepage-testimonial');
    
    if (!$joinUs.length || !$testimonial.length) return;
    
    var testimonials = [
        '<span>"The ProleWiki community is very welcoming and diverse..."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:ComradeTrash">ComradeTrash</a>',
        '<span>"Working with ProleWiki has given me space to learn..."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:CriticalResist">CriticalResist</a>',
        '<span>"ProleWiki is a lively environment for socialist education..."<br><a href="https://en.prolewiki.org/wiki/Comrade:Charhapiti">Charhapiti</a>',
        '<span>"As a trans woman, I have never felt more accepted..."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:Local_Gay_Communist_-_LGC">Local Gay Communist</a>'
    ];
    
    if ($joinUs.height() >= 375) {
        $testimonial.html(
            testimonials[Math.floor(Math.random() * testimonials.length)]
        ).show();
    }
});