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

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
CriticalResist (talk | contribs)
typeface picker further optimization, last rev works
CriticalResist (talk | contribs)
rev below works great. Final optimization pass here. Prior functions were left alone as I have no idea how to test if the refactor worked since I don't know what they do.
Line 181: Line 181:
jQuery( CustomizeModificationsOfSidebar );
jQuery( CustomizeModificationsOfSidebar );


/** ==================== Citizen Skin Enhancements ==================== **/
/** ==================== Combined Preferences Setup (ES5) ==================== **/
$(function() {
$(function() {
     // Add sidebar icons in correct order
     // Shared setup for both theme and font pickers
     var $header = $('.citizen-drawer');
     function initPreferences() {
    if ($header.length) {
        setupThemePicker();
         var icons = [
        setupFontPicker();
             {cls: 'sidebar-library', href: '/wiki/ProleWiki:Library', title: 'Library'},
    }
             {cls: 'sidebar-essays', href: '/wiki/ProleWiki:Essays', title: 'Essays'}
   
    /** ===== 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 (!mw.user.isAnon()) {
         if (isJune) {
             icons.push({cls: 'sidebar-upload', href: '/wiki/Special:Upload', title: 'Upload file'});
             themes.push({
                value: 'trans',  
                label: 'Trans',
                className: 'skin-citizen-trans',
                labelId: 'trans-style-label'
            });
         }
         }
          
 
        // Create elements in reverse order to ensure correct positioning
         function applyTheme(themeValue) {
        for (var i = icons.length - 1; i >= 0; i--) {
            // Batch class operations by looping and removing each class
            var icon = icons[i];
            for (var i = 0; i < themes.length; i++) {
             $header.after(
                html.classList.remove(themes[i].className);
                '<a class="citizen-header__item ' + icon.cls +  
             }
                 '" href="' + icon.href + '" title="' + icon.title + '">' +
           
                 '<div class="citizen-header__button"></div></a>'
            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);
         }
         }
    }


    // Heading hyperlinks
         var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day';
    function setupHeadingLinks() {
        if (storedTheme === 'trans' && !isJune) storedTheme = 'day';
         var $headings = $('h2, h3, h4, h5').find('.mw-headline');
         applyTheme(storedTheme);
         if (!$headings.length) return;


         function smoothScroll(targetId) {
         function addThemeOptions() {
             var target = document.getElementById(targetId);
             var themeContainer = document.getElementById('skin-client-prefs-skin-theme');
             if (target) {
             if (!themeContainer) {
                 window.scrollTo({
                 return setTimeout(addThemeOptions, 100);
                    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 form = themeContainer.querySelector('form');
            var template = form.querySelector('.citizen-client-prefs-radio');
             if (!template) return;
              
              
             var $link = $(
             for (var i = 0; i < themes.length; i++) {
                 '<a href="#' + id + '" class="heading-hyperlink citizen-ui-icon mw-ui-icon-wikimedia-link" ' +
                var theme = themes[i];
                 'title="Direct link to this section" aria-label="Direct link to this section" data-no-preview="true"></a>'
                 var radioId = 'skin-client-pref-skin-theme-value-' + theme.value;
            );
                if (document.getElementById(radioId)) continue;
           
               
            // Add specific click handler to prevent section collapse
                 var radioElement = template.cloneNode(true);
            $link.on('click', function(e) {
                var input = radioElement.querySelector('input');
                 e.preventDefault();
                 var label = radioElement.querySelector('label');
                 e.stopPropagation(); // Prevent event from bubbling to section toggle
                  
                 smoothScroll(id);
                input.id = radioId;
                 history.replaceState(null, null, '#' + id);
                input.value = theme.value;
            });
                input.name = 'skin-client-pref-skin-theme-group';
           
                 input.checked = (storedTheme === theme.value);
            $headline.append($link);
                  
         });
                label.setAttribute('for', radioId);
                label.textContent = theme.label;
               
                if (theme.labelId) {
                    label.id = theme.labelId;
                }
               
                form.appendChild(radioElement);
            }
         }


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


     // Collection rendering check
     /** ===== Typeface Picker ===== **/
     if ($("#renderingProgress").length) {
     function setupFontPicker() {
         setTimeout(function() {
        var html = document.documentElement;
            location.reload();
         var FONT_STORAGE_KEY = 'skin-font-clientpref';
         }, 5000);
        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'}
        ];


/** ==================== Theme Management ==================== **/
        var activeFont = localStorage.getItem(FONT_STORAGE_KEY) || 'sans';
$(function() {
    // Configuration - optimized for minimal DOM lookups
    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'}
    ];


    // Add Trans theme only in June
         function applyFont(fontValue) {
    if (isJune) {
            for (var i = 0; i < fonts.length; i++) {
        themes.push({
                html.classList.remove(fonts[i].className);
            value: 'trans',
            }
            label: 'Trans',
           
            className: 'skin-citizen-trans',
            if (fontValue !== 'sans') {
            labelId: 'trans-style-label'
                for (var j = 0; j < fonts.length; j++) {
         });
                    if (fonts[j].value === fontValue) {
    }
                        html.classList.add(fonts[j].className);
 
                        break;
    // Theme application - optimized with classList operations
                    }
    function applyTheme(themeValue) {
                }
        // Remove all theme classes in a single operation
        for (var i = 0; i < themes.length; i++) {
            html.classList.remove(themes[i].className);
        }
       
        // Find and apply new theme
        for (var j = 0; j < themes.length; j++) {
            if (themes[j].value === themeValue) {
                html.classList.add(themes[j].className);
                break;
             }
             }
            localStorage.setItem(FONT_STORAGE_KEY, fontValue);
         }
         }
        localStorage.setItem('skin-theme-clientpref', themeValue);
    }


    // Theme initialization with localStorage check
        applyFont(activeFont);
    var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day';
    if (storedTheme === 'trans' && !isJune) storedTheme = 'day';
    applyTheme(storedTheme);


    // Theme options injection - optimized for Citizen structure
        function addFontOptions() {
    function addThemeOptions() {
            var fontSection = document.getElementById('skin-client-prefs-font');
        // Target the specific theme container
             if (fontSection) return;
        var themeContainer = document.getElementById('skin-client-prefs-skin-theme');
        if (!themeContainer) {
            setTimeout(addThemeOptions, 100);
             return;
        }
       
        // Get the form element where options should be added
        var form = themeContainer.querySelector('form');
        if (!form) return;
       
        // Get the first radio option as template
        var template = form.querySelector('.citizen-client-prefs-radio');
        if (!template) return;
       
        // Create all options at once
        for (var i = 0; i < themes.length; i++) {
            var theme = themes[i];
            var radioId = 'skin-client-pref-skin-theme-value-' + theme.value;
           
            // Skip if option already exists
            if (document.getElementById(radioId)) continue;
              
              
            // Clone template for efficiency
             var prefsPanel = document.getElementById('citizen-client-prefs');
            var radioElement = template.cloneNode(true);
             if (!prefsPanel) {
             var input = radioElement.querySelector('input');
                return setTimeout(addFontOptions, 100);
            var label = radioElement.querySelector('label');
           
            // Update input properties
            input.id = radioId;
            input.value = theme.value;
            input.name = 'skin-client-pref-skin-theme-group';
            input.checked = (storedTheme === theme.value);
           
            // Update label properties
            label.setAttribute('for', radioId);
             label.textContent = theme.label;
           
            // Apply custom label ID if specified (for styling)
            if (theme.labelId) {
                label.id = theme.labelId;
             }
             }
              
              
             // Add to form
             // Create new section using string HTML (ES5 safe)
             form.appendChild(radioElement);
             var section = document.createElement('div');
        }
            section.className = 'mw-portlet mw-portlet-js citizen-menu';
    }
            section.id = 'skin-client-prefs-font';


    // Efficient event delegation for theme changes
            section.innerHTML =
    $(document).on('change', 'input[name="skin-client-pref-skin-theme-group"]', function() {
                '<div class="citizen-menu__heading">Typeface</div>' +
        applyTheme(this.value);
                '<div class="citizen-menu__content">' +
    });
                    '<ul class="citizen-menu__content-list">' +
   
                        '<li class="mw-list-item mw-list-item-js">' +
    // Initialize theme options
                            '<form></form>' +
    addThemeOptions();
                        '</li>' +
});
                    '</ul>' +
 
                '</div>';
/** ==================== Typeface Picker ==================== **/
           
$(function() {
            var textSizeSection = document.getElementById(TEXT_SIZE_ID);
    var html = document.documentElement;
            if (textSizeSection) {
    var FONT_STORAGE_KEY = 'skin-font-clientpref';
                if (textSizeSection.nextSibling) {
    var TEXT_SIZE_ID = 'skin-client-prefs-citizen-feature-custom-font-size';
                    textSizeSection.parentNode.insertBefore(section, textSizeSection.nextSibling);
   
                } else {
    var fonts = [
                     textSizeSection.parentNode.appendChild(section);
        {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) {
        // Remove all font classes
        for (var i = 0; i < fonts.length; i++) {
            html.classList.remove(fonts[i].className);
        }
       
        // Only apply if not default sans
        if (fontValue !== 'sans') {
            for (var j = 0; j < fonts.length; j++) {
                if (fonts[j].value === fontValue) {
                     html.classList.add(fonts[j].className);
                    break;
                 }
                 }
            } else {
                prefsPanel.appendChild(section);
             }
             }
        }
        localStorage.setItem(FONT_STORAGE_KEY, fontValue);
    }
    applyFont(activeFont);
    function addFontOptions() {
        // Check if font section already exists
        var fontSection = document.getElementById('skin-client-prefs-font');
        if (fontSection) return; // Section already exists
       
        // Get preferences container
        var prefsPanel = document.getElementById('citizen-client-prefs');
        if (!prefsPanel) {
            setTimeout(addFontOptions, 100);
            return;
        }
       
        // Create new section
        var section = document.createElement('div');
        section.className = 'mw-portlet mw-portlet-js citizen-menu';
        section.id = 'skin-client-prefs-font';
        var heading = document.createElement('div');
        heading.className = 'citizen-menu__heading';
        heading.textContent = 'Typeface';
        section.appendChild(heading);
        var content = document.createElement('div');
        content.className = 'citizen-menu__content';
        var ul = document.createElement('ul');
        ul.className = 'citizen-menu__content-list';
        var li = document.createElement('li');
        li.className = 'mw-list-item mw-list-item-js';
        var fontForm = document.createElement('form');
        li.appendChild(fontForm);
        ul.appendChild(li);
        content.appendChild(ul);
        section.appendChild(content);
        // Insert after text size section
        var textSizeSection = document.getElementById(TEXT_SIZE_ID);
        if (textSizeSection && textSizeSection.nextSibling) {
            textSizeSection.parentNode.insertBefore(section, textSizeSection.nextSibling);
        } else if (textSizeSection) {
            textSizeSection.parentNode.appendChild(section);
        } else {
            prefsPanel.appendChild(section);
        }
       
        // Get a radio template from existing preferences
        var template = document.querySelector('.citizen-client-prefs-radio');
        if (!template) return;
       
        // Add font options
        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 template = document.querySelector('.citizen-client-prefs-radio');
             var input = radioElement.querySelector('input');
             var form = section.querySelector('form');
             var label = radioElement.querySelector('label');
             if (!template || !form) return;
              
              
             // Configure input
             for (var i = 0; i < fonts.length; i++) {
            input.id = radioId;
                var font = fonts[i];
            input.value = font.value;
                var radioId = 'skin-client-pref-font-value-' + font.value;
            input.name = 'skin-client-pref-font-group';
               
            input.checked = (activeFont === font.value);
                var radioElement = template.cloneNode(true);
           
                var input = radioElement.querySelector('input');
            // Configure label
                var label = radioElement.querySelector('label');
            label.setAttribute('for', radioId);
               
            label.textContent = font.label;
                input.id = radioId;
           
                input.value = font.value;
            fontForm.appendChild(radioElement);
                input.name = 'skin-client-pref-font-group';
                input.checked = (activeFont === font.value);
               
                label.setAttribute('for', radioId);
                label.textContent = font.label;
               
                form.appendChild(radioElement);
            }
         }
         }
    }


    $(document).on('change', 'input[name="skin-client-pref-font-group"]', function() {
        $(document).on('change', 'input[name="skin-client-pref-font-group"]', function() {
        applyFont(this.value);
            applyFont(this.value);
    });
        });
   
          
    addFontOptions();
         addFontOptions();
});
 
/** ==================== 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
     initPreferences();
    $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();
    }
});
});


/**
/** ==================== Optimized Reading Mode (ES5) ==================== **/
* Reading mode TEST (press 0 to toggle on or off, or refresh page)
* Does not save yet on page refresh for the test
**/
/** ==================== Reading Mode Features ==================== **/
// Skip loading reading mode if not on desktop device:
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
$(function() {
$(function() {
     var html = document.documentElement,
     var html = document.documentElement;
        readingModeClass = 'reading-mode',
    var readingModeClass = 'reading-mode';
        isReadingMode = false;
    var isReadingMode = false;
    var progressBar, progressTooltip, hoverLine;
      
      
     // Toggle reading mode
     // Toggle reading mode
     function toggleReadingMode() {
     function toggleReadingMode() {
         isReadingMode = !isReadingMode;
         isReadingMode = !isReadingMode;
         html.classList.toggle(readingModeClass, isReadingMode);
         // Toggle the class on the html element
        if (isReadingMode) {
            html.classList.add(readingModeClass);
        } else {
            html.classList.remove(readingModeClass);
        }
          
          
         if (isReadingMode && window._paq) {
         if (isReadingMode && window._paq) {
Line 548: Line 395:
     }
     }
      
      
     // Keyboard shortcut (desktop only)
     // Keyboard shortcut
     if (!('ontouchstart' in window)) {
     $(document).on('keydown', function(e) {
        $(document).on('keydown', function(e) {
        if ((e.keyCode === 48 || e.keyCode === 96) &&  
            if ((e.keyCode === 48 || e.keyCode === 96) && // 0 key
            !$(e.target).is('input, textarea, [contenteditable]')) {
                !$(e.target).is('input, textarea, [contenteditable]')) {
            toggleReadingMode();
                toggleReadingMode();
        }
            }
    });
        });
    }
      
      
     // Hover line feature
     // Hover line - throttled
     function setupHoverLine() {
     function initHoverLine() {
         var $line = $('<div id="hover-line"></div>').css({
         hoverLine = document.createElement('div');
            position: 'fixed',
        hoverLine.id = 'hover-line';
            left: 0,
        hoverLine.style.position = 'fixed';
            width: '100%',
        hoverLine.style.left = '0';
            height: '1.5em',
        hoverLine.style.width = '100%';
            pointerEvents: 'none',
        hoverLine.style.height = '1.5em';
            zIndex: 1,
        hoverLine.style.pointerEvents = 'none';
            transition: 'top 0.1s ease, background-color 0.3s ease',
        hoverLine.style.zIndex = '1';
            display: 'none',
        hoverLine.style.display = 'none';
            backgroundColor: 'rgba(255,255,150,0.2)'
        hoverLine.style.transition = 'top 0.1s ease, background-color 0.3s ease';
         }).appendTo('body');
        hoverLine.style.backgroundColor = 'rgba(255,255,150,0.2)';
         document.body.appendChild(hoverLine);
          
          
         $(document).on('mousemove', function(e) {
         var lastMove = 0;
        document.addEventListener('mousemove', function(e) {
             if (!isReadingMode) {
             if (!isReadingMode) {
                 $line.hide();
                 hoverLine.style.display = 'none';
                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;
                 return;
             }
             }
              
              
             var rect = range.getBoundingClientRect();
             var now = Date.now();
             if (!rect || rect.height === 0) {
             if (now - lastMove < 16) return; // ~60fps throttle
                $line.hide();
             lastMove = now;
                return;
             }
              
              
             $line.css({
             updateHoverLine(e);
                top: rect.top + 'px',
                height: rect.height + 'px',
                display: 'block'
            });
         });
         });
     }
     }
    setupHoverLine();
      
      
     // Progress bar feature with hover tooltip
    function updateHoverLine(e) {
     function setupProgressBar() {
        var el = document.elementFromPoint(e.clientX, e.clientY);
        // Create wrapper container
        if (!el) {
         var $wrapper = $('<div id="reading-progress-wrapper"></div>').css({
            hoverLine.style.display = 'none';
            position: 'fixed',
            return;
            bottom: 0,
        }
            left: 0,
       
            width: '100%',
        // Find the closest paragraph or list item
            height: '12px',
        var paragraph = el;
            zIndex: 9999,
        while (paragraph && paragraph !== document) {
            pointerEvents: 'auto'
            if (paragraph.tagName === 'P' || paragraph.tagName === 'LI') break;
         }).appendTo('body');
            paragraph = paragraph.parentNode;
        }
        if (!paragraph || (paragraph.tagName !== 'P' && paragraph.tagName !== 'LI')) {
            hoverLine.style.display = 'none';
            return;
        }
       
        var range;
        if (document.caretRangeFromPoint) {
            range = document.caretRangeFromPoint(e.clientX, e.clientY);
        }
        if (!range) {
            hoverLine.style.display = 'none';
            return;
        }
       
        var rect = range.getBoundingClientRect();
        if (!rect || rect.height === 0) {
            hoverLine.style.display = 'none';
            return;
        }
       
        hoverLine.style.top = rect.top + 'px';
        hoverLine.style.height = rect.height + 'px';
        hoverLine.style.display = 'block';
    }
   
     // Progress bar with RAF
     function initProgressBar() {
         var wrapper = document.createElement('div');
        wrapper.id = 'reading-progress-wrapper';
        wrapper.style.position = 'fixed';
        wrapper.style.bottom = '0';
        wrapper.style.left = '0';
        wrapper.style.width = '100%';
        wrapper.style.height = '12px';
        wrapper.style.zIndex = '9999';
        wrapper.style.pointerEvents = 'auto';
       
         progressBar = document.createElement('div');
        progressBar.id = 'reading-progress-bar';
        progressBar.style.position = 'absolute';
        progressBar.style.bottom = '0';
        progressBar.style.left = '0';
        progressBar.style.height = '4px';
        progressBar.style.backgroundColor = 'var(--color-progress-bar)';
        progressBar.style.width = '0%';
        progressBar.style.transition = 'width 0.1s ease-out';
        progressBar.style.pointerEvents = 'none';
        wrapper.appendChild(progressBar);
        document.body.appendChild(wrapper);
          
          
         // Create progress bar
         progressTooltip = document.createElement('div');
        var $bar = $('<div id="reading-progress-bar"></div>').css({
        progressTooltip.id = 'reading-progress-tooltip';
            position: 'absolute',
        progressTooltip.style.position = 'fixed';
            bottom: 0,
        progressTooltip.style.padding = '2px 6px';
            left: 0,
        progressTooltip.style.backgroundColor = '#333';
            height: '4px',
        progressTooltip.style.color = '#fff';
            backgroundColor: 'var(--color-progress-bar)',
        progressTooltip.style.fontSize = '1em';
            width: '0%',
        progressTooltip.style.borderRadius = '3px';
            transition: 'width 0.1s ease-out',
        progressTooltip.style.pointerEvents = 'none';
            pointerEvents: 'none'
        progressTooltip.style.zIndex = '10000';
         }).appendTo($wrapper);
        progressTooltip.style.display = 'none';
         document.body.appendChild(progressTooltip);
          
          
         // Create tooltip
         // Event handlers
         var $tooltip = $('<div id="reading-progress-tooltip"></div>').css({
         wrapper.addEventListener('mouseenter', function() {
             position: 'fixed',
             if (isReadingMode) {
            padding: '2px 6px',
                progressTooltip.style.display = 'block';
             backgroundColor: '#333',
             }
            color: '#fff',
         });
            fontSize: '1em',
            borderRadius: '3px',
            pointerEvents: 'none',
            zIndex: 10000,
            display: 'none'
         }).appendTo('body');
          
          
         var lastPercent = 0;
         wrapper.addEventListener('mouseleave', function() {
            progressTooltip.style.display = 'none';
        });
          
          
         // Update progress on scroll
        wrapper.addEventListener('mousemove', function(e) {
         $(window).on('scroll', function() {
            if (!isReadingMode) return;
            updateProgressTooltip(e);
        });
       
         // RAF-based scroll handler
        var rafId;
         function updateProgress() {
             if (!isReadingMode) {
             if (!isReadingMode) {
                 $wrapper.hide();
                 wrapper.style.display = 'none';
                rafId = null;
                 return;
                 return;
             }
             }
              
              
             $wrapper.show();
             wrapper.style.display = 'block';
             var scrollTop = $(window).scrollTop(),
             var scrollTop = window.scrollY || document.documentElement.scrollTop;
                 docHeight = $(document).height(),
            var docHeight = Math.max(
                 winHeight = $(window).height(),
                document.body.scrollHeight, document.documentElement.scrollHeight,
                percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
                 document.body.offsetHeight, document.documentElement.offsetHeight,
                 document.body.clientHeight, document.documentElement.clientHeight
            );
            var winHeight = window.innerHeight;
            var percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
              
              
             if (Math.abs(percent - lastPercent) > 0.5) {
             progressBar.style.width = percent + '%';
                $bar.width(percent + '%');
            rafId = requestAnimationFrame(updateProgress);
                 lastPercent = percent;
        }
       
        window.addEventListener('scroll', function() {
            if (!rafId) {
                 rafId = requestAnimationFrame(updateProgress);
             }
             }
         });
         });
       
    }
        // Show/hide tooltip on hover
   
        $wrapper.hover(
    function updateProgressTooltip(e) {
            function() {
        var scrollTop = window.scrollY || document.documentElement.scrollTop;
                if (!isReadingMode) return;
        var docHeight = Math.max(
                $tooltip.show();
             document.body.scrollHeight, document.documentElement.scrollHeight,
             },
             document.body.offsetHeight, document.documentElement.offsetHeight,
             function() {
             document.body.clientHeight, document.documentElement.clientHeight
                $tooltip.hide();
             }
         );
         );
        var winHeight = window.innerHeight;
        var percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
          
          
         // Update tooltip position and text
         progressTooltip.textContent = Math.round(percent) + '%';
        $wrapper.on('mousemove', function(e) {
        progressTooltip.style.left = (e.clientX + 8) + 'px';
            if (!isReadingMode) return;
        progressTooltip.style.bottom = '16px';
           
            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);
    });
});
/** ==================== Join us homepage testimonials ==================== **/
$(function() {
    var $joinUs = $('#joinUs'),
        $testimonial = $('#homepage-testimonial');
      
      
     if (!$joinUs.length || !$testimonial.length) return;
     // Initialize features
   
     initHoverLine();
    var testimonials = [
     initProgressBar();
        '<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();
    }
});
});
}

Revision as of 11:50, 1 August 2025

/* 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 );

/** ==================== Combined Preferences Setup (ES5) ==================== **/
$(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;
                
                form.appendChild(radioElement);
            }
        }

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

/** ==================== Optimized Reading Mode (ES5) ==================== **/
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
$(function() {
    var html = document.documentElement;
    var readingModeClass = 'reading-mode';
    var isReadingMode = false;
    var progressBar, progressTooltip, hoverLine;
    
    // Toggle reading mode
    function toggleReadingMode() {
        isReadingMode = !isReadingMode;
        // Toggle the class on the html element
        if (isReadingMode) {
            html.classList.add(readingModeClass);
        } else {
            html.classList.remove(readingModeClass);
        }
        
        if (isReadingMode && window._paq) {
            _paq.push(['trackGoal', 12]);
        }
    }
    
    // Keyboard shortcut
    $(document).on('keydown', function(e) {
        if ((e.keyCode === 48 || e.keyCode === 96) && 
            !$(e.target).is('input, textarea, [contenteditable]')) {
            toggleReadingMode();
        }
    });
    
    // Hover line - throttled
    function initHoverLine() {
        hoverLine = document.createElement('div');
        hoverLine.id = 'hover-line';
        hoverLine.style.position = 'fixed';
        hoverLine.style.left = '0';
        hoverLine.style.width = '100%';
        hoverLine.style.height = '1.5em';
        hoverLine.style.pointerEvents = 'none';
        hoverLine.style.zIndex = '1';
        hoverLine.style.display = 'none';
        hoverLine.style.transition = 'top 0.1s ease, background-color 0.3s ease';
        hoverLine.style.backgroundColor = 'rgba(255,255,150,0.2)';
        document.body.appendChild(hoverLine);
        
        var lastMove = 0;
        document.addEventListener('mousemove', function(e) {
            if (!isReadingMode) {
                hoverLine.style.display = 'none';
                return;
            }
            
            var now = Date.now();
            if (now - lastMove < 16) return; // ~60fps throttle
            lastMove = now;
            
            updateHoverLine(e);
        });
    }
    
    function updateHoverLine(e) {
        var el = document.elementFromPoint(e.clientX, e.clientY);
        if (!el) {
            hoverLine.style.display = 'none';
            return;
        }
        
        // Find the closest paragraph or list item
        var paragraph = el;
        while (paragraph && paragraph !== document) {
            if (paragraph.tagName === 'P' || paragraph.tagName === 'LI') break;
            paragraph = paragraph.parentNode;
        }
        if (!paragraph || (paragraph.tagName !== 'P' && paragraph.tagName !== 'LI')) {
            hoverLine.style.display = 'none';
            return;
        }
        
        var range;
        if (document.caretRangeFromPoint) {
            range = document.caretRangeFromPoint(e.clientX, e.clientY);
        }
        if (!range) {
            hoverLine.style.display = 'none';
            return;
        }
        
        var rect = range.getBoundingClientRect();
        if (!rect || rect.height === 0) {
            hoverLine.style.display = 'none';
            return;
        }
        
        hoverLine.style.top = rect.top + 'px';
        hoverLine.style.height = rect.height + 'px';
        hoverLine.style.display = 'block';
    }
    
    // Progress bar with RAF
    function initProgressBar() {
        var wrapper = document.createElement('div');
        wrapper.id = 'reading-progress-wrapper';
        wrapper.style.position = 'fixed';
        wrapper.style.bottom = '0';
        wrapper.style.left = '0';
        wrapper.style.width = '100%';
        wrapper.style.height = '12px';
        wrapper.style.zIndex = '9999';
        wrapper.style.pointerEvents = 'auto';
        
        progressBar = document.createElement('div');
        progressBar.id = 'reading-progress-bar';
        progressBar.style.position = 'absolute';
        progressBar.style.bottom = '0';
        progressBar.style.left = '0';
        progressBar.style.height = '4px';
        progressBar.style.backgroundColor = 'var(--color-progress-bar)';
        progressBar.style.width = '0%';
        progressBar.style.transition = 'width 0.1s ease-out';
        progressBar.style.pointerEvents = 'none';
        wrapper.appendChild(progressBar);
        document.body.appendChild(wrapper);
        
        progressTooltip = document.createElement('div');
        progressTooltip.id = 'reading-progress-tooltip';
        progressTooltip.style.position = 'fixed';
        progressTooltip.style.padding = '2px 6px';
        progressTooltip.style.backgroundColor = '#333';
        progressTooltip.style.color = '#fff';
        progressTooltip.style.fontSize = '1em';
        progressTooltip.style.borderRadius = '3px';
        progressTooltip.style.pointerEvents = 'none';
        progressTooltip.style.zIndex = '10000';
        progressTooltip.style.display = 'none';
        document.body.appendChild(progressTooltip);
        
        // Event handlers
        wrapper.addEventListener('mouseenter', function() {
            if (isReadingMode) {
                progressTooltip.style.display = 'block';
            }
        });
        
        wrapper.addEventListener('mouseleave', function() {
            progressTooltip.style.display = 'none';
        });
        
        wrapper.addEventListener('mousemove', function(e) {
            if (!isReadingMode) return;
            updateProgressTooltip(e);
        });
        
        // RAF-based scroll handler
        var rafId;
        function updateProgress() {
            if (!isReadingMode) {
                wrapper.style.display = 'none';
                rafId = null;
                return;
            }
            
            wrapper.style.display = 'block';
            var scrollTop = window.scrollY || document.documentElement.scrollTop;
            var docHeight = Math.max(
                document.body.scrollHeight, document.documentElement.scrollHeight,
                document.body.offsetHeight, document.documentElement.offsetHeight,
                document.body.clientHeight, document.documentElement.clientHeight
            );
            var winHeight = window.innerHeight;
            var percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
            
            progressBar.style.width = percent + '%';
            rafId = requestAnimationFrame(updateProgress);
        }
        
        window.addEventListener('scroll', function() {
            if (!rafId) {
                rafId = requestAnimationFrame(updateProgress);
            }
        });
    }
    
    function updateProgressTooltip(e) {
        var scrollTop = window.scrollY || document.documentElement.scrollTop;
        var docHeight = Math.max(
            document.body.scrollHeight, document.documentElement.scrollHeight,
            document.body.offsetHeight, document.documentElement.offsetHeight,
            document.body.clientHeight, document.documentElement.clientHeight
        );
        var winHeight = window.innerHeight;
        var percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
        
        progressTooltip.textContent = Math.round(percent) + '%';
        progressTooltip.style.left = (e.clientX + 8) + 'px';
        progressTooltip.style.bottom = '16px';
    }
    
    // Initialize features
    initHoverLine();
    initProgressBar();
});
}