More languages
More actions
typeface picker further optimization, last rev works |
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 ); | ||
/** ==================== | /** ==================== Combined Preferences Setup (ES5) ==================== **/ | ||
$(function() { | $(function() { | ||
// | // Shared setup for both theme and font pickers | ||
var | function initPreferences() { | ||
setupThemePicker(); | |||
var | 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 ( | 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'; | |||
var | applyTheme(storedTheme); | ||
function | function addThemeOptions() { | ||
var | var themeContainer = document.getElementById('skin-client-prefs-skin-theme'); | ||
if ( | if (!themeContainer) { | ||
return setTimeout(addThemeOptions, 100); | |||
} | } | ||
if (! | var form = themeContainer.querySelector('form'); | ||
var template = form.querySelector('.citizen-client-prefs-radio'); | |||
if (!template) return; | |||
var | 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() { | |||
$(document).on(' | 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) { | |||
var | 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 | var template = document.querySelector('.citizen-client-prefs-radio'); | ||
var | 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) ==================== **/ | |||
/** ==================== Reading Mode | |||
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'; | |||
var isReadingMode = false; | |||
var progressBar, progressTooltip, hoverLine; | |||
// Toggle reading mode | // Toggle reading mode | ||
function toggleReadingMode() { | function toggleReadingMode() { | ||
isReadingMode = !isReadingMode; | isReadingMode = !isReadingMode; | ||
html.classList. | // 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 | // Keyboard shortcut | ||
$(document).on('keydown', function(e) { | |||
if ((e.keyCode === 48 || e.keyCode === 96) && | |||
!$(e.target).is('input, textarea, [contenteditable]')) { | |||
toggleReadingMode(); | |||
} | |||
}); | |||
// Hover line | // Hover line - throttled | ||
function | 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) { | if (!isReadingMode) { | ||
hoverLine.style.display = 'none'; | |||
return; | return; | ||
} | } | ||
var | var now = Date.now(); | ||
if ( | if (now - lastMove < 16) return; // ~60fps throttle | ||
lastMove = now; | |||
updateHoverLine(e); | |||
}); | }); | ||
} | } | ||
// Progress bar | function updateHoverLine(e) { | ||
function | var el = document.elementFromPoint(e.clientX, e.clientY); | ||
if (!el) { | |||
var | 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) { | if (!isReadingMode) { | ||
wrapper.style.display = 'none'; | |||
rafId = null; | |||
return; | return; | ||
} | } | ||
wrapper.style.display = 'block'; | |||
var scrollTop = | var scrollTop = window.scrollY || document.documentElement.scrollTop; | ||
var docHeight = Math.max( | |||
winHeight = | 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(); | |||
}); | }); | ||
} | |||
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();
});
}