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)
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.
CriticalResist (talk | contribs)
2
 
(45 intermediate revisions by 3 users not shown)
Line 60: Line 60:
}
}


/**
/** Adds show/hide-button to navigation bars.
* Adds show/hide-button to navigation bars.
*
  * @param {jQuery} $content
  * @param {jQuery} $content
  */
  */
Line 112: Line 110:
}());
}());


/**
/** Side bar customization */
*
* Side bar customization
*
*/
function ModifySidebar( action, section, name, link ) {
function ModifySidebar( action, section, name, link ) {
try {
try {
Line 181: Line 175:
jQuery( CustomizeModificationsOfSidebar );
jQuery( CustomizeModificationsOfSidebar );


/** ==================== Combined Preferences Setup (ES5) ==================== **/
/** ==================== 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',
                condition: function() { return true; }
            },
            {
                cls: 'sidebar-essays',
                href: '/wiki/ProleWiki:Essays',
                title: 'Essays',
                condition: function() { return true; }
            },
            {
                cls: 'sidebar-upload',
                href: '/wiki/Special:Upload',
                title: 'Upload file',
                condition: function() { return !mw.user.isAnon(); }
            },
            {
                cls: 'sidebar-reading-mode',
                href: '#',
                title: 'Toggle reading mode (0) (beta)',
                condition: function() {
                    return !('ontouchstart' in window || navigator.maxTouchPoints > 0);
                }
            }
        ];
       
        // Create elements in array order
        for (var i = icons.length - 1; i >= 0; i--) {
            var icon = icons[i];
           
            // Check if this icon should be displayed
            if (icon.condition && icon.condition()) {
                $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() {
$(function() {
     // Shared setup for both theme and font pickers
     // Shared setup for both theme and font pickers
Line 198: Line 300:
             {value: 'os', label: 'Auto', className: 'skin-citizen-auto'},
             {value: 'os', label: 'Auto', className: 'skin-citizen-auto'},
             {value: 'night', label: 'Dark', className: 'skin-citizen-dark'},
             {value: 'night', label: 'Dark', className: 'skin-citizen-dark'},
             {value: 'sepia', label: 'Sepia', className: 'skin-citizen-sepia', labelId: 'sepia-style-label'}
             {value: 'sepia', label: 'Sepia', className: 'skin-citizen-sepia', labelId: 'sepia-style-label'},
            {value: 'neobrutalism', label:'Neobrut', className: 'skin-citizen-neobrutalism', labelId: 'neobrutalism-style-label'}
         ];
         ];


         if (isJune) {
         if (true) {
             themes.push({
             themes.push({
                 value: 'trans',  
                 value: 'trans',  
Line 226: Line 329:


         var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day';
         var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day';
         if (storedTheme === 'trans' && !isJune) storedTheme = 'day';
         if (storedTheme === 'trans' && !isJune && !true) { // Added !true condition for debugging
        storedTheme = 'day';
    }
         applyTheme(storedTheme);
         applyTheme(storedTheme);


Line 272: Line 377:


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


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


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


        applyFont(activeFont);
    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';


         function addFontOptions() {
         section.innerHTML =
             var fontSection = document.getElementById('skin-client-prefs-font');
            '<div class="citizen-menu__heading">Typeface</div>' +
            if (fontSection) return;
            '<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 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');
            }
           
            input.id = radioId;
            input.value = font.value;
            input.name = 'skin-client-pref-font-group';
            input.checked = (activeFont === font.value);
              
              
             // Create new section using string HTML (ES5 safe)
             label.setAttribute('for', radioId);
            var section = document.createElement('div');
             label.textContent = font.label;
             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);
             // Add custom font class to the label
             if (textSizeSection) {
             label.className = label.className + ' ' + font.className;
                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');
             // Add ID for styling if needed
            var form = section.querySelector('form');
            label.id = font.value + '-font-label';
            if (!template || !form) return;
              
              
             for (var i = 0; i < fonts.length; i++) {
             form.appendChild(radioElement);
                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() {
    $(document).on('change', 'input[name="skin-client-pref-font-group"]', function() {
            applyFont(this.value);
        applyFont(this.value);
        });
    });
       
   
        addFontOptions();
    addFontOptions();
    }
}
      
      
     initPreferences();
     initPreferences();
});
});


/** ==================== Optimized Reading Mode (ES5) ==================== **/
/** ==================== Reading mode and features ==================== **/
/** (trigger, hoverline, progress bar) */
// 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,
    var readingModeClass = 'reading-mode';
        readingModeClass = 'reading-mode',
    var isReadingMode = false;
        isReadingMode = false;
    var progressBar, progressTooltip, hoverLine;
      
      
     // Toggle reading mode
     // Toggle reading mode
     function toggleReadingMode() {
     function toggleReadingMode() {
         isReadingMode = !isReadingMode;
         isReadingMode = !isReadingMode;
         // Toggle the class on the html element
         html.classList.toggle(readingModeClass, isReadingMode);
        if (isReadingMode) {
            html.classList.add(readingModeClass);
        } else {
            html.classList.remove(readingModeClass);
        }
          
          
         if (isReadingMode && window._paq) {
         if (isReadingMode && window._paq) {
Line 395: Line 502:
     }
     }
      
      
     // Keyboard shortcut
     // Keyboard shortcut (desktop only)
     $(document).on('keydown', function(e) {
     if (!('ontouchstart' in window)) {
        if ((e.keyCode === 48 || e.keyCode === 96) &&  
        $(document).on('keydown', function(e) {
            !$(e.target).is('input, textarea, [contenteditable]')) {
            if ((e.keyCode === 48 || e.keyCode === 96) && // 0 key
            toggleReadingMode();
                !$(e.target).is('input, textarea, [contenteditable]')) {
         }
                toggleReadingMode();
     });
            }
         });
     }
      
      
     // Hover line - throttled
     // Hover line feature
     function initHoverLine() {
     function setupHoverLine() {
         hoverLine = document.createElement('div');
         var $line = $('<div id="hover-line"></div>').css({
        hoverLine.id = 'hover-line';
            position: 'fixed',
        hoverLine.style.position = 'fixed';
            left: 0,
        hoverLine.style.left = '0';
            width: '100%',
        hoverLine.style.width = '100%';
            height: '1.5em',
        hoverLine.style.height = '1.5em';
            pointerEvents: 'none',
        hoverLine.style.pointerEvents = 'none';
            zIndex: 1,
        hoverLine.style.zIndex = '1';
            transition: 'top 0.1s ease, background-color 0.3s ease',
        hoverLine.style.display = 'none';
            display: 'none',
        hoverLine.style.transition = 'top 0.1s ease, background-color 0.3s ease';
            backgroundColor: 'var(--color-hoverline, rgba(255,255,150,0.2))'
        hoverLine.style.backgroundColor = 'rgba(255,255,150,0.2)';
         }).appendTo('body');
         document.body.appendChild(hoverLine);
          
          
         var lastMove = 0;
         $(document).on('mousemove', function(e) {
        document.addEventListener('mousemove', function(e) {
             if (!isReadingMode) {
             if (!isReadingMode) {
                 hoverLine.style.display = 'none';
                 $line.hide();
                 return;
                 return;
             }
             }
              
              
             var now = Date.now();
             var el = document.elementFromPoint(e.clientX, e.clientY);
             if (now - lastMove < 16) return; // ~60fps throttle
             if (!el) {
             lastMove = now;
                $line.hide();
                return;
             }
              
              
             updateHoverLine(e);
             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();
      
      
     function updateHoverLine(e) {
    // Progress bar feature with hover tooltip
         var el = document.elementFromPoint(e.clientX, e.clientY);
     function setupProgressBar() {
        if (!el) {
        // Create wrapper container
             hoverLine.style.display = 'none';
         var $wrapper = $('<div id="reading-progress-wrapper"></div>').css({
             return;
            position: 'fixed',
         }
            bottom: 0,
            left: 0,
            width: '100%',
             height: '12px',
            zIndex: 9999,
             pointerEvents: 'auto'
         }).appendTo('body');
          
          
         // Find the closest paragraph or list item
         // Create progress bar
         var paragraph = el;
         var $bar = $('<div id="reading-progress-bar"></div>').css({
        while (paragraph && paragraph !== document) {
             position: 'absolute',
             if (paragraph.tagName === 'P' || paragraph.tagName === 'LI') break;
            bottom: 0,
             paragraph = paragraph.parentNode;
            left: 0,
        }
            height: '4px',
        if (!paragraph || (paragraph.tagName !== 'P' && paragraph.tagName !== 'LI')) {
            backgroundColor: 'var(--color-progress-bar)',
             hoverLine.style.display = 'none';
             width: '0%',
            return;
            transition: 'width 0.1s ease-out',
         }
             pointerEvents: 'none'
         }).appendTo($wrapper);
          
          
         var range;
         // Create tooltip
         if (document.caretRangeFromPoint) {
         var $tooltip = $('<div id="reading-progress-tooltip"></div>').css({
             range = document.caretRangeFromPoint(e.clientX, e.clientY);
             position: 'fixed',
        }
            padding: '2px 6px',
        if (!range) {
            backgroundColor: '#333',
             hoverLine.style.display = 'none';
            color: '#fff',
             return;
            fontSize: '1em',
         }
            borderRadius: '3px',
             pointerEvents: 'none',
            zIndex: 10000,
             display: 'none'
         }).appendTo('body');
          
          
         var rect = range.getBoundingClientRect();
         var lastPercent = 0;
        if (!rect || rect.height === 0) {
            hoverLine.style.display = 'none';
            return;
        }
          
          
         hoverLine.style.top = rect.top + 'px';
         // Update progress on scroll
         hoverLine.style.height = rect.height + 'px';
         $(window).on('scroll', function() {
        hoverLine.style.display = 'block';
            if (!isReadingMode) {
    }
                $wrapper.hide();
   
                return;
    // Progress bar with RAF
            }
    function initProgressBar() {
           
        var wrapper = document.createElement('div');
            $wrapper.show();
        wrapper.id = 'reading-progress-wrapper';
            var scrollTop = $(window).scrollTop(),
        wrapper.style.position = 'fixed';
                docHeight = $(document).height(),
        wrapper.style.bottom = '0';
                winHeight = $(window).height(),
        wrapper.style.left = '0';
                percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
        wrapper.style.width = '100%';
           
        wrapper.style.height = '12px';
            if (Math.abs(percent - lastPercent) > 0.5) {
        wrapper.style.zIndex = '9999';
                $bar.width(percent + '%');
        wrapper.style.pointerEvents = 'auto';
                 lastPercent = percent;
       
        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() {
         // Show/hide tooltip on hover
            progressTooltip.style.display = 'none';
        $wrapper.hover(
         });
            function() {
                if (!isReadingMode) return;
                $tooltip.show();
            },
            function() {
                $tooltip.hide();
            }
         );
          
          
         wrapper.addEventListener('mousemove', function(e) {
         // Update tooltip position and text
        $wrapper.on('mousemove', function(e) {
             if (!isReadingMode) return;
             if (!isReadingMode) return;
             updateProgressTooltip(e);
              
            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'
                  });
         });
         });
       
    }
        // RAF-based scroll handler
    setupProgressBar();
         var rafId;
   
         function updateProgress() {
    // Click handler for reading mode icon (NEW)
             if (!isReadingMode) {
    $(document).on('click', '.sidebar-reading-mode', function(e) {
                 wrapper.style.display = 'none';
         e.preventDefault();
                rafId = null;
         toggleReadingMode();
                 return;
    });
});
} //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);
            wrapper.style.display = 'block';
    });
            var scrollTop = window.scrollY || document.documentElement.scrollTop;
});
            var docHeight = Math.max(
 
                document.body.scrollHeight, document.documentElement.scrollHeight,
/** ==================== Accreq Form Autosave ==================== **/
                document.body.offsetHeight, document.documentElement.offsetHeight,
$(function() {
                document.body.clientHeight, document.documentElement.clientHeight
    var $textarea = $('#wpBio');
            );
    if (!$textarea.length) return;
            var winHeight = window.innerHeight;
   
            var percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
    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>'),
            progressBar.style.width = percent + '%';
        lastValue = '';
            rafId = requestAnimationFrame(updateProgress);
   
         }
    $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;
          
          
         window.addEventListener('scroll', function() {
         showStatus('Saving...');
             if (!rafId) {
        setTimeout(function() {
                rafId = requestAnimationFrame(updateProgress);
             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();
     }
     }
});
/** Add two extra checkboxes to the Terms of Service fieldset on the account request page **/
(function() {
    var tos = document.getElementById('wpToS');
    if (!tos) return;
    var p = tos.closest('p');
    if (!p) return;
    p.insertAdjacentHTML('afterend',
        '<p><input type="checkbox" name="wpAdditional1" id="wpAdditional1" value="1"> <label for="wpAdditional1">I understand that my edits may be seen by potentially thousands of people, and will do my best in service of our readers.</label></p>' +
        '<p><input type="checkbox" name="wpAdditional2" id="wpAdditional2" value="1"> <label for="wpAdditional2">I agree that ProleWiki is a collective project, and I will work with my fellow editors to always elevate the project as a whole.</label></p>'
    );
})();
/** ============ Ctrl+K search for selected text ============
* Desktop only implementation
*/
(function() {
    'use strict';
   
    // Early exit for mobile devices
    if (window.innerWidth < 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) return;
      
      
     function updateProgressTooltip(e) {
     document.addEventListener('keydown', function(e) {
         var scrollTop = window.scrollY || document.documentElement.scrollTop;
         if (!((e.ctrlKey || e.metaKey) && e.key === 'k')) return;
         var docHeight = Math.max(
       
             document.body.scrollHeight, document.documentElement.scrollHeight,
        e.preventDefault();
             document.body.offsetHeight, document.documentElement.offsetHeight,
       
             document.body.clientHeight, document.documentElement.clientHeight
        // Skip if editing
         var active = document.activeElement;
        var isEditing = active && (
             active.tagName === 'TEXTAREA' ||
            active.tagName === 'INPUT' ||
            active.isContentEditable ||
             document.querySelector('.ace_editor, .ve-ui-surface, .ve-init-mw-desktopArticleTarget') ||
             new URLSearchParams(window.location.search).get('action') === 'edit'
         );
         );
        var winHeight = window.innerHeight;
        var percent = Math.min(100, scrollTop / (docHeight - winHeight) * 100);
          
          
         progressTooltip.textContent = Math.round(percent) + '%';
         if (isEditing) return;
         progressTooltip.style.left = (e.clientX + 8) + 'px';
       
        progressTooltip.style.bottom = '16px';
        var selectedText = window.getSelection().toString().trim();
        if (selectedText) performSearch(selectedText);
    });
   
    function performSearch(query) {
        if (!query) return;
       
        var apiUrl = new URL('/api.php', window.location.origin);
        apiUrl.searchParams.set('action', 'query');
        apiUrl.searchParams.set('titles', query);
         apiUrl.searchParams.set('format', 'json');
        apiUrl.searchParams.set('origin', '*');
       
        fetch(apiUrl)
            .then(function(response) {
                return response.json();
            })
            .then(function(data) {
                var pages = data.query.pages;
                var pageId = Object.keys(pages)[0];
                var url = new URL('/index.php', window.location.origin);
               
                if (pageId !== '-1' && !pages[pageId].missing) {
                    url.searchParams.set('title', pages[pageId].title);
                } else {
                    url.searchParams.set('title', 'Special:Search');
                    url.searchParams.set('fulltext', '1');
                    url.searchParams.set('search', query);
                }
                window.location.href = url.toString();
            })
            .catch(function(error) {
                console.error('Search API error:', error);
                var searchUrl = new URL('/index.php', window.location.origin);
                searchUrl.searchParams.set('title', 'Special:Search');
                searchUrl.searchParams.set('fulltext', '1');
                searchUrl.searchParams.set('search', query);
                window.location.href = searchUrl.toString();
            });
     }
     }
})();
/** ==================== Join us homepage testimonials ==================== **/
$(function() {
    var $joinUs = $('#joinUs'),
        $testimonial = $('#homepage-testimonial');
      
      
     // Initialize features
     if (!$joinUs.length || !$testimonial.length) return;
     initHoverLine();
   
    initProgressBar();
    var testimonials = [
    '<span>"The ProleWiki community is very welcoming and diverse, with a genuine curiosity towards a broad spectrum of topics, plus a democratic and empathetic approach to learning and decision-making."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:ComradeTrash">ComradeTrash</a>',
        '<span>"Working with ProleWiki has given me the space and drive to learn more, both as a communist and as a professional. I never feel throttled or unable to voice my ideas to the project, and then make them into reality."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:CriticalResist">CriticalResist</a>',
        '<span>"ProleWiki is a lively environment for self-directed socialist education. They are very supportive of indigenous peoples globally, which in this era is hard to find, so among socialists ProleWiki are very principled. The editorship strives for quality."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:Charhapiti">Charhapiti</a>',
        '<span>"As a trans woman, I have never felt more accepted by any other community but that of ProleWiki. It is one of the few leftist spaces online which is principled enough to truly uphold its stance as a consistent ally of the queer community."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:Local_Gay_Communist_-_LGC">Local Gay Communist - LGC'
     ];
   
    if ($joinUs.height() >= 375) {
        $testimonial.html(
            testimonials[Math.floor(Math.random() * testimonials.length)]
        ).show();
    }
});
});
}

Latest revision as of 06:21, 25 February 2026

/* 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',
                condition: function() { return true; }
            },
            {
                cls: 'sidebar-essays', 
                href: '/wiki/ProleWiki:Essays', 
                title: 'Essays',
                condition: function() { return true; }
            },
            {
                cls: 'sidebar-upload', 
                href: '/wiki/Special:Upload', 
                title: 'Upload file',
                condition: function() { return !mw.user.isAnon(); }
            },
            {
                cls: 'sidebar-reading-mode', 
                href: '#', 
                title: 'Toggle reading mode (0) (beta)',
                condition: function() { 
                    return !('ontouchstart' in window || navigator.maxTouchPoints > 0); 
                }
            }
        ];
        
        // Create elements in array order
        for (var i = icons.length - 1; i >= 0; i--) {
            var icon = icons[i];
            
            // Check if this icon should be displayed
            if (icon.condition && icon.condition()) {
                $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'},
            {value: 'neobrutalism', label:'Neobrut', className: 'skin-citizen-neobrutalism', labelId: 'neobrutalism-style-label'}
        ];

        if (true) {
            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 && !true) { // Added !true condition for debugging
        	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: 'var(--color-hoverline, 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();
    
    // Click handler for reading mode icon (NEW)
    $(document).on('click', '.sidebar-reading-mode', function(e) {
        e.preventDefault();
        toggleReadingMode();
    });
});
} //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();
    }
});

/** Add two extra checkboxes to the Terms of Service fieldset on the account request page **/
(function() {
    var tos = document.getElementById('wpToS');
    if (!tos) return;
    var p = tos.closest('p');
    if (!p) return;
    p.insertAdjacentHTML('afterend',
        '<p><input type="checkbox" name="wpAdditional1" id="wpAdditional1" value="1"> <label for="wpAdditional1">I understand that my edits may be seen by potentially thousands of people, and will do my best in service of our readers.</label></p>' +
        '<p><input type="checkbox" name="wpAdditional2" id="wpAdditional2" value="1"> <label for="wpAdditional2">I agree that ProleWiki is a collective project, and I will work with my fellow editors to always elevate the project as a whole.</label></p>'
    );
})();

/** ============ Ctrl+K search for selected text ============ 
 * Desktop only implementation
 */
(function() {
    'use strict';
    
    // Early exit for mobile devices
    if (window.innerWidth < 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) return;
    
    document.addEventListener('keydown', function(e) {
        if (!((e.ctrlKey || e.metaKey) && e.key === 'k')) return;
        
        e.preventDefault();
        
        // Skip if editing
        var active = document.activeElement;
        var isEditing = active && (
            active.tagName === 'TEXTAREA' || 
            active.tagName === 'INPUT' || 
            active.isContentEditable ||
            document.querySelector('.ace_editor, .ve-ui-surface, .ve-init-mw-desktopArticleTarget') ||
            new URLSearchParams(window.location.search).get('action') === 'edit'
        );
        
        if (isEditing) return;
        
        var selectedText = window.getSelection().toString().trim();
        if (selectedText) performSearch(selectedText);
    });
    
    function performSearch(query) {
        if (!query) return;
        
        var apiUrl = new URL('/api.php', window.location.origin);
        apiUrl.searchParams.set('action', 'query');
        apiUrl.searchParams.set('titles', query);
        apiUrl.searchParams.set('format', 'json');
        apiUrl.searchParams.set('origin', '*');
        
        fetch(apiUrl)
            .then(function(response) {
                return response.json();
            })
            .then(function(data) {
                var pages = data.query.pages;
                var pageId = Object.keys(pages)[0];
                var url = new URL('/index.php', window.location.origin);
                
                if (pageId !== '-1' && !pages[pageId].missing) {
                    url.searchParams.set('title', pages[pageId].title);
                } else {
                    url.searchParams.set('title', 'Special:Search');
                    url.searchParams.set('fulltext', '1');
                    url.searchParams.set('search', query);
                }
                window.location.href = url.toString();
            })
            .catch(function(error) {
                console.error('Search API error:', error);
                var searchUrl = new URL('/index.php', window.location.origin);
                searchUrl.searchParams.set('title', 'Special:Search');
                searchUrl.searchParams.set('fulltext', '1');
                searchUrl.searchParams.set('search', query);
                window.location.href = searchUrl.toString();
            });
    }
})();

/** ==================== 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, with a genuine curiosity towards a broad spectrum of topics, plus a democratic and empathetic approach to learning and decision-making."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:ComradeTrash">ComradeTrash</a>',
        '<span>"Working with ProleWiki has given me the space and drive to learn more, both as a communist and as a professional. I never feel throttled or unable to voice my ideas to the project, and then make them into reality."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:CriticalResist">CriticalResist</a>',
        '<span>"ProleWiki is a lively environment for self-directed socialist education. They are very supportive of indigenous peoples globally, which in this era is hard to find, so among socialists ProleWiki are very principled. The editorship strives for quality."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:Charhapiti">Charhapiti</a>',
        '<span>"As a trans woman, I have never felt more accepted by any other community but that of ProleWiki. It is one of the few leftist spaces online which is principled enough to truly uphold its stance as a consistent ally of the queer community."</span><br><a href="https://en.prolewiki.org/wiki/Comrade:Local_Gay_Communist_-_LGC">Local Gay Communist - LGC'
    ];
    
    if ($joinUs.height() >= 375) {
        $testimonial.html(
            testimonials[Math.floor(Math.random() * testimonials.length)]
        ).show();
    }
});