More languages
More actions
The error wasn't on Common.js, it was on Citizen.js. Reverting changes Tag: Undo |
cat cursor chase v1 |
||
| Line 1: | Line 1: | ||
/* Any JavaScript here will be loaded for all users on every page load. */ | |||
//Matomo accurate tracking (no more 0 second visits) | |||
//_paq.push(['enableHeartBeatTimer']); | |||
/** | |||
* Dynamic Navigation Bars. See [[Wikipedia:NavFrame]] | |||
* | |||
* Based on script from en.wikipedia.org, 2008-09-15. | |||
* | |||
* @source www.mediawiki.org/wiki/MediaWiki:Gadget-NavFrame.js | |||
* @maintainer Helder.wiki, 2012–2013 | |||
* @maintainer Krinkle, 2013 | |||
*/ | |||
( function () { | |||
// Set up the words in your language | |||
var collapseCaption = 'hide'; | |||
var expandCaption = 'show'; | |||
var navigationBarHide = '[' + collapseCaption + ']'; | |||
var navigationBarShow = '[' + expandCaption + ']'; | |||
/** | |||
* Shows and hides content and picture (if available) of navigation bars. | |||
* | |||
* @param {number} indexNavigationBar The index of navigation bar to be toggled | |||
* @param {jQuery.Event} e Event object | |||
*/ | |||
function toggleNavigationBar( indexNavigationBar, e ) { | |||
var navChild, | |||
navToggle = document.getElementById( 'NavToggle' + indexNavigationBar ), | |||
navFrame = document.getElementById( 'NavFrame' + indexNavigationBar ); | |||
// Prevent browser from jumping to href "#" | |||
e.preventDefault(); | |||
if ( !navFrame || !navToggle ) { | |||
return false; | |||
} | |||
// If shown now | |||
if ( navToggle.firstChild.data === navigationBarHide ) { | |||
for ( navChild = navFrame.firstChild; navChild !== null; navChild = navChild.nextSibling ) { | |||
if ( $( navChild ).hasClass( 'NavContent' ) || $( navChild ).hasClass( 'NavPic' ) ) { | |||
navChild.style.display = 'none'; | |||
} | |||
} | |||
navToggle.firstChild.data = navigationBarShow; | |||
// If hidden now | |||
} else if ( navToggle.firstChild.data === navigationBarShow ) { | |||
for ( navChild = navFrame.firstChild; navChild !== null; navChild = navChild.nextSibling ) { | |||
if ( $( navChild ).hasClass( 'NavContent' ) || $( navChild ).hasClass( 'NavPic' ) ) { | |||
navChild.style.display = 'block'; | |||
} | |||
} | |||
navToggle.firstChild.data = navigationBarHide; | |||
} | |||
} | |||
/** Adds show/hide-button to navigation bars. | |||
* @param {jQuery} $content | |||
*/ | |||
function createNavigationBarToggleButton( $content ) { | |||
var i, j, navChild, navToggle, navToggleText, isCollapsed, | |||
indexNavigationBar = 0; | |||
// iterate over all < div >-elements | |||
var $divs = $content.find( 'div.NavFrame' ); | |||
$divs.each( function ( i, navFrame ) { | |||
indexNavigationBar++; | |||
navToggle = document.createElement( 'a' ); | |||
navToggle.className = 'NavToggle'; | |||
navToggle.setAttribute( 'id', 'NavToggle' + indexNavigationBar ); | |||
navToggle.setAttribute( 'href', '#' ); | |||
$( navToggle ).on( 'click', $.proxy( toggleNavigationBar, null, indexNavigationBar ) ); | |||
isCollapsed = $( navFrame ).hasClass( 'collapsed' ); | |||
// backwards compatibility for old technique where the collapsed class was not yet used | |||
for ( navChild = navFrame.firstChild; navChild !== null && !isCollapsed; navChild = navChild.nextSibling ) { | |||
if ( $( navChild ).hasClass( 'NavPic' ) || $( navChild ).hasClass( 'NavContent' ) ) { | |||
if ( navChild.style.display === 'none' ) { | |||
isCollapsed = true; | |||
} | |||
} | |||
} | |||
if ( isCollapsed ) { | |||
for ( navChild = navFrame.firstChild; navChild !== null; navChild = navChild.nextSibling ) { | |||
if ( $( navChild ).hasClass( 'NavPic' ) || $( navChild ).hasClass( 'NavContent' ) ) { | |||
navChild.style.display = 'none'; | |||
} | |||
} | |||
} | |||
navToggleText = document.createTextNode( isCollapsed ? navigationBarShow : navigationBarHide ); | |||
navToggle.appendChild( navToggleText ); | |||
// Find the NavHead and attach the toggle link (Must be this complicated because Moz's firstChild handling is borked) | |||
for ( j = 0; j < navFrame.childNodes.length; j++ ) { | |||
if ( $( navFrame.childNodes[j] ).hasClass( 'NavHead' ) ) { | |||
navToggle.style.color = navFrame.childNodes[j].style.color; | |||
navFrame.childNodes[j].appendChild( navToggle ); | |||
} | |||
} | |||
navFrame.setAttribute( 'id', 'NavFrame' + indexNavigationBar ); | |||
} ); | |||
} | |||
mw.hook( 'wikipage.content' ).add( createNavigationBarToggleButton ); | |||
}()); | |||
/** Side bar customization */ | |||
function ModifySidebar( action, section, name, link ) { | |||
try { | |||
switch ( section ) { | |||
case 'languages': | |||
var target = 'p-lang'; | |||
break; | |||
case 'toolbox': | |||
var target = 'p-tb'; | |||
break; | |||
case 'navigation': | |||
var target = 'p-navigation'; | |||
break; | |||
default: | |||
var target = 'p-' + section; | |||
break; | |||
} | |||
if ( action == 'add' ) { | |||
var node = document.getElementById( target ) | |||
.getElementsByTagName( 'div' )[0] | |||
.getElementsByTagName( 'ul' )[0]; | |||
var aNode = document.createElement( 'a' ); | |||
var liNode = document.createElement( 'li' ); | |||
aNode.appendChild( document.createTextNode( name ) ); | |||
aNode.setAttribute( 'href', link ); | |||
liNode.appendChild( aNode ); | |||
liNode.className = 'plainlinks'; | |||
node.appendChild( liNode ); | |||
} | |||
if ( action == 'remove' ) { | |||
var list = document.getElementById( target ) | |||
.getElementsByTagName( 'div' )[0] | |||
.getElementsByTagName( 'ul' )[0]; | |||
var listelements = list.getElementsByTagName( 'li' ); | |||
for ( var i = 0; i < listelements.length; i++ ) { | |||
if ( | |||
listelements[i].getElementsByTagName( 'a' )[0].innerHTML == name || | |||
listelements[i].getElementsByTagName( 'a' )[0].href == link | |||
) | |||
{ | |||
list.removeChild( listelements[i] ); | |||
} | |||
} | |||
} | |||
} catch( e ) { | |||
// let's just ignore what's happened | |||
return; | |||
} | |||
} | |||
function CustomizeModificationsOfSidebar() { | |||
// adds [[Special:CategoryTree|Special:CategoryTree]] to toolbox | |||
ModifySidebar( 'add', 'toolbox', 'List of uploaded files', 'https://en.prolewiki.org/wiki/Special:ListFiles' ); | |||
} | |||
jQuery( CustomizeModificationsOfSidebar ); | |||
/** ==================== Citizen Skin Enhancements (sidebar icons, heading hyperlinks, collection refresh) ==================== **/ | |||
$(function() { | |||
// Add sidebar icons in correct order | |||
var $header = $('.citizen-drawer'); | |||
if ($header.length) { | |||
var icons = [ | |||
{cls: 'sidebar-library', href: '/wiki/ProleWiki:Library', title: 'Library'}, | |||
{cls: 'sidebar-essays', href: '/wiki/ProleWiki:Essays', title: 'Essays'} | |||
]; | |||
if (!mw.user.isAnon()) { | |||
icons.push({cls: 'sidebar-upload', href: '/wiki/Special:Upload', title: 'Upload file'}); | |||
} | |||
// Create elements in reverse order to ensure correct positioning | |||
for (var i = icons.length - 1; i >= 0; i--) { | |||
var icon = icons[i]; | |||
$header.after( | |||
'<a class="citizen-header__item ' + icon.cls + | |||
'" href="' + icon.href + '" title="' + icon.title + '">' + | |||
'<div class="citizen-header__button"></div></a>' | |||
); | |||
} | |||
} | |||
// Heading hyperlinks | |||
function setupHeadingLinks() { | |||
var $headings = $('h2, h3, h4, h5').find('.mw-headline'); | |||
if (!$headings.length) return; | |||
function smoothScroll(targetId) { | |||
var target = document.getElementById(targetId); | |||
if (target) { | |||
window.scrollTo({ | |||
top: target.offsetTop, | |||
behavior: 'smooth' | |||
}); | |||
} | |||
} | |||
// Create hyperlink buttons with proper event handling | |||
$headings.each(function() { | |||
var $headline = $(this), | |||
id = $headline.attr('id'); | |||
if (!id) return; | |||
var $link = $( | |||
'<a href="#' + id + '" class="heading-hyperlink citizen-ui-icon mw-ui-icon-wikimedia-link" ' + | |||
'title="Direct link to this section" aria-label="Direct link to this section" data-no-preview="true"></a>' | |||
); | |||
// Add specific click handler to prevent section collapse | |||
$link.on('click', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); // Prevent event from bubbling to section toggle | |||
smoothScroll(id); | |||
history.replaceState(null, null, '#' + id); | |||
}); | |||
$headline.append($link); | |||
}); | |||
// Handle other anchor clicks (not the hyperlink buttons) | |||
$(document).on('click', 'a[href^="#"]:not(.heading-hyperlink)', function(e) { | |||
var href = this.href; | |||
var id = href.substring(href.indexOf('#') + 1); | |||
if (!id) return; | |||
e.preventDefault(); | |||
smoothScroll(id); | |||
history.replaceState(null, null, '#' + id); | |||
}); | |||
} | |||
setupHeadingLinks(); | |||
// Collection rendering check | |||
if ($("#renderingProgress").length) { | |||
setTimeout(function() { | |||
location.reload(); | |||
}, 5000); | |||
} | |||
}); | |||
/** ==================== Combined Preferences Setup (custom typefaces, themes) ==================== **/ | |||
$(function() { | |||
// Shared setup for both theme and font pickers | |||
function initPreferences() { | |||
setupThemePicker(); | |||
setupFontPicker(); | |||
} | |||
/** ===== Theme Picker ===== **/ | |||
function setupThemePicker() { | |||
var html = document.documentElement; | |||
var currentMonth = new Date().getMonth(); | |||
var isJune = currentMonth === 5; | |||
var themes = [ | |||
{value: 'day', label: 'Light', className: 'skin-citizen-light'}, | |||
{value: 'os', label: 'Auto', className: 'skin-citizen-auto'}, | |||
{value: 'night', label: 'Dark', className: 'skin-citizen-dark'}, | |||
{value: 'sepia', label: 'Sepia', className: 'skin-citizen-sepia', labelId: 'sepia-style-label'} | |||
]; | |||
if (isJune) { | |||
themes.push({ | |||
value: 'trans', | |||
label: 'Trans', | |||
className: 'skin-citizen-trans', | |||
labelId: 'trans-style-label' | |||
}); | |||
} | |||
function applyTheme(themeValue) { | |||
// Batch class operations by looping and removing each class | |||
for (var i = 0; i < themes.length; i++) { | |||
html.classList.remove(themes[i].className); | |||
} | |||
for (var j = 0; j < themes.length; j++) { | |||
if (themes[j].value === themeValue) { | |||
html.classList.add(themes[j].className); | |||
break; | |||
} | |||
} | |||
localStorage.setItem('skin-theme-clientpref', themeValue); | |||
} | |||
var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day'; | |||
if (storedTheme === 'trans' && !isJune) storedTheme = 'day'; | |||
applyTheme(storedTheme); | |||
function addThemeOptions() { | |||
var themeContainer = document.getElementById('skin-client-prefs-skin-theme'); | |||
if (!themeContainer) { | |||
return setTimeout(addThemeOptions, 100); | |||
} | |||
var form = themeContainer.querySelector('form'); | |||
var template = form.querySelector('.citizen-client-prefs-radio'); | |||
if (!template) return; | |||
for (var i = 0; i < themes.length; i++) { | |||
var theme = themes[i]; | |||
var radioId = 'skin-client-pref-skin-theme-value-' + theme.value; | |||
if (document.getElementById(radioId)) continue; | |||
var radioElement = template.cloneNode(true); | |||
var input = radioElement.querySelector('input'); | |||
var label = radioElement.querySelector('label'); | |||
input.id = radioId; | |||
input.value = theme.value; | |||
input.name = 'skin-client-pref-skin-theme-group'; | |||
input.checked = (storedTheme === theme.value); | |||
label.setAttribute('for', radioId); | |||
label.textContent = theme.label; | |||
if (theme.labelId) { | |||
label.id = theme.labelId; | |||
} | |||
form.appendChild(radioElement); | |||
} | |||
} | |||
$(document).on('change', 'input[name="skin-client-pref-skin-theme-group"]', function() { | |||
applyTheme(this.value); | |||
}); | |||
addThemeOptions(); | |||
} | |||
/** ===== Typeface Picker ===== **/ | |||
function setupFontPicker() { | |||
var html = document.documentElement; | |||
var FONT_STORAGE_KEY = 'skin-font-clientpref'; | |||
var TEXT_SIZE_ID = 'skin-client-prefs-citizen-feature-custom-font-size'; | |||
var fonts = [ | |||
{value: 'sans', label: 'Sans', className: 'font-sans'}, | |||
{value: 'serif', label: 'Serif', className: 'font-serif'}, | |||
{value: 'dyslexic', label: 'Accessibility', className: 'font-dyslexic'} | |||
]; | |||
var activeFont = localStorage.getItem(FONT_STORAGE_KEY) || 'sans'; | |||
function applyFont(fontValue) { | |||
for (var i = 0; i < fonts.length; i++) { | |||
html.classList.remove(fonts[i].className); | |||
} | |||
if (fontValue !== 'sans') { | |||
for (var j = 0; j < fonts.length; j++) { | |||
if (fonts[j].value === fontValue) { | |||
html.classList.add(fonts[j].className); | |||
break; | |||
} | |||
} | |||
} | |||
localStorage.setItem(FONT_STORAGE_KEY, fontValue); | |||
} | |||
applyFont(activeFont); | |||
function addFontOptions() { | |||
var fontSection = document.getElementById('skin-client-prefs-font'); | |||
if (fontSection) return; | |||
var prefsPanel = document.getElementById('citizen-client-prefs'); | |||
if (!prefsPanel) { | |||
return setTimeout(addFontOptions, 100); | |||
} | |||
// Create new section using string HTML (ES5 safe) | |||
var section = document.createElement('div'); | |||
section.className = 'mw-portlet mw-portlet-js citizen-menu'; | |||
section.id = 'skin-client-prefs-font'; | |||
section.innerHTML = | |||
'<div class="citizen-menu__heading">Typeface</div>' + | |||
'<div class="citizen-menu__content">' + | |||
'<ul class="citizen-menu__content-list">' + | |||
'<li class="mw-list-item mw-list-item-js">' + | |||
'<form></form>' + | |||
'</li>' + | |||
'</ul>' + | |||
'</div>'; | |||
var textSizeSection = document.getElementById(TEXT_SIZE_ID); | |||
if (textSizeSection) { | |||
if (textSizeSection.nextSibling) { | |||
textSizeSection.parentNode.insertBefore(section, textSizeSection.nextSibling); | |||
} else { | |||
textSizeSection.parentNode.appendChild(section); | |||
} | |||
} else { | |||
prefsPanel.appendChild(section); | |||
} | |||
var template = document.querySelector('.citizen-client-prefs-radio'); | |||
var form = section.querySelector('form'); | |||
if (!template || !form) return; | |||
for (var i = 0; i < fonts.length; i++) { | |||
var font = fonts[i]; | |||
var radioId = 'skin-client-pref-font-value-' + font.value; | |||
var radioElement = template.cloneNode(true); | |||
var input = radioElement.querySelector('input'); | |||
var label = radioElement.querySelector('label'); | |||
input.id = radioId; | |||
input.value = font.value; | |||
input.name = 'skin-client-pref-font-group'; | |||
input.checked = (activeFont === font.value); | |||
label.setAttribute('for', radioId); | |||
label.textContent = font.label; | |||
// Add custom font class to the label | |||
label.className = label.className + ' ' + font.className; | |||
// Add ID for styling if needed | |||
label.id = font.value + '-font-label'; | |||
form.appendChild(radioElement); | |||
} | |||
} | |||
$(document).on('change', 'input[name="skin-client-pref-font-group"]', function() { | |||
applyFont(this.value); | |||
}); | |||
addFontOptions(); | |||
} | |||
initPreferences(); | |||
}); | |||
/** ==================== Reading mode and features ==================== **/ | |||
/** (trigger, hoverline, progress bar) */ | |||
// Skip loading reading mode if not on desktop device: | |||
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) { | |||
$(function() { | |||
var html = document.documentElement, | |||
readingModeClass = 'reading-mode', | |||
isReadingMode = false; | |||
// Toggle reading mode | |||
function toggleReadingMode() { | |||
isReadingMode = !isReadingMode; | |||
html.classList.toggle(readingModeClass, isReadingMode); | |||
if (isReadingMode && window._paq) { | |||
_paq.push(['trackGoal', 12]); | |||
} | |||
} | |||
// Keyboard shortcut (desktop only) | |||
if (!('ontouchstart' in window)) { | |||
$(document).on('keydown', function(e) { | |||
if ((e.keyCode === 48 || e.keyCode === 96) && // 0 key | |||
!$(e.target).is('input, textarea, [contenteditable]')) { | |||
toggleReadingMode(); | |||
} | |||
}); | |||
} | |||
// Hover line feature | |||
function setupHoverLine() { | |||
var $line = $('<div id="hover-line"></div>').css({ | |||
position: 'fixed', | |||
left: 0, | |||
width: '100%', | |||
height: '1.5em', | |||
pointerEvents: 'none', | |||
zIndex: 1, | |||
transition: 'top 0.1s ease, background-color 0.3s ease', | |||
display: 'none', | |||
backgroundColor: '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(); | |||
}); | |||
} //end reading mode IF statement | |||
/** ==================== Visual Editor Welcome Message ==================== **/ | |||
mw.loader.using(['ext.visualEditor.desktopArticleTarget.init']).then(function () { | |||
mw.hook('ve.activationComplete').add(function () { | |||
var checkDialog = setInterval(function () { | |||
var $dialogContent = $('.ve-init-mw-welcomeDialog-content'); | |||
if ($dialogContent.length) { | |||
clearInterval(checkDialog); | |||
// Only append once | |||
if (!$dialogContent.data('custom-appended')) { | |||
$dialogContent.append( | |||
'<br><br>Please read our <strong><a href="/wiki/Principles">Principles</a></strong> and <strong><a href="/wiki/Encyclopedic_tone_guide">Encyclopedic tone guide</a><strong> first to make sure your edit is approved!' | |||
); | |||
$dialogContent.data('custom-appended', true); | |||
// Resize the OOUI dialog to fit new content | |||
var dialog = OO.ui.WindowManager.static.currentWindow; | |||
if (dialog && typeof dialog.updateSize === 'function') { | |||
dialog.updateSize(); | |||
} | |||
} | |||
} | |||
}, 200); | |||
}); | |||
}); | |||
/** ==================== Accreq Form Autosave ==================== **/ | |||
$(function() { | |||
var $textarea = $('#wpBio'); | |||
if (!$textarea.length) return; | |||
var STORAGE_KEY = 'form-bio-autosave', | |||
$status = $('<div id="autosave-status" style="font-size:0.8em;color:gray;height:1.2em;margin-top:5px;"></div>'), | |||
lastValue = ''; | |||
$textarea.after($status); | |||
// Restore saved content | |||
var saved = localStorage.getItem(STORAGE_KEY); | |||
if (saved) { | |||
$textarea.val(saved); | |||
lastValue = saved; | |||
showStatus('Restored previous work'); | |||
} | |||
// Setup autosave | |||
$textarea.on('input', function() { | |||
var current = $textarea.val(); | |||
if (current === lastValue) return; | |||
showStatus('Saving...'); | |||
setTimeout(function() { | |||
localStorage.setItem(STORAGE_KEY, current); | |||
lastValue = current; | |||
showStatus('Saved locally'); | |||
}, 2000); | |||
}); | |||
// Clear on form submit | |||
$textarea.closest('form').on('submit', function() { | |||
localStorage.removeItem(STORAGE_KEY); | |||
}); | |||
function showStatus(msg) { | |||
$status.text(msg).stop().fadeIn().delay(3000).fadeOut(); | |||
} | |||
}); | |||
/** ==================== Join us homepage testimonials ==================== **/ | |||
$(function() { | |||
var $joinUs = $('#joinUs'), | |||
$testimonial = $('#homepage-testimonial'); | |||
if (!$joinUs.length || !$testimonial.length) return; | |||
var testimonials = [ | |||
'<span>"The ProleWiki community is very welcoming and diverse, 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: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(); | |||
} | |||
}); | |||
(function () { | |||
var imgUrl = 'https://shared.prolewiki.org/uploads/0/05/Cat.png '; // change to your cat image | |||
var cat = document.createElement('img'); | |||
cat.id = 'cat-follower'; | |||
cat.src = imgUrl; | |||
cat.alt = 'cat'; | |||
document.body.appendChild(cat); | |||
var x = window.innerWidth / 2; | |||
var y = window.innerHeight / 2; | |||
var mx = x; | |||
var my = y; | |||
// adjust the follow speed (smaller = smoother / more delay) | |||
var speed = 0.1; | |||
document.addEventListener('mousemove', function (e) { | |||
x = e.clientX; | |||
y = e.clientY; | |||
}); | |||
function animate() { | |||
mx += (x - mx) * speed; | |||
my += (y - my) * speed; | |||
cat.style.left = mx + 'px'; | |||
cat.style.top = my + 'px'; | |||
requestAnimationFrame(animate); | |||
} | |||
animate(); | |||
})(); | |||
Revision as of 05:41, 7 October 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 );
/** ==================== Citizen Skin Enhancements (sidebar icons, heading hyperlinks, collection refresh) ==================== **/
$(function() {
// Add sidebar icons in correct order
var $header = $('.citizen-drawer');
if ($header.length) {
var icons = [
{cls: 'sidebar-library', href: '/wiki/ProleWiki:Library', title: 'Library'},
{cls: 'sidebar-essays', href: '/wiki/ProleWiki:Essays', title: 'Essays'}
];
if (!mw.user.isAnon()) {
icons.push({cls: 'sidebar-upload', href: '/wiki/Special:Upload', title: 'Upload file'});
}
// Create elements in reverse order to ensure correct positioning
for (var i = icons.length - 1; i >= 0; i--) {
var icon = icons[i];
$header.after(
'<a class="citizen-header__item ' + icon.cls +
'" href="' + icon.href + '" title="' + icon.title + '">' +
'<div class="citizen-header__button"></div></a>'
);
}
}
// Heading hyperlinks
function setupHeadingLinks() {
var $headings = $('h2, h3, h4, h5').find('.mw-headline');
if (!$headings.length) return;
function smoothScroll(targetId) {
var target = document.getElementById(targetId);
if (target) {
window.scrollTo({
top: target.offsetTop,
behavior: 'smooth'
});
}
}
// Create hyperlink buttons with proper event handling
$headings.each(function() {
var $headline = $(this),
id = $headline.attr('id');
if (!id) return;
var $link = $(
'<a href="#' + id + '" class="heading-hyperlink citizen-ui-icon mw-ui-icon-wikimedia-link" ' +
'title="Direct link to this section" aria-label="Direct link to this section" data-no-preview="true"></a>'
);
// Add specific click handler to prevent section collapse
$link.on('click', function(e) {
e.preventDefault();
e.stopPropagation(); // Prevent event from bubbling to section toggle
smoothScroll(id);
history.replaceState(null, null, '#' + id);
});
$headline.append($link);
});
// Handle other anchor clicks (not the hyperlink buttons)
$(document).on('click', 'a[href^="#"]:not(.heading-hyperlink)', function(e) {
var href = this.href;
var id = href.substring(href.indexOf('#') + 1);
if (!id) return;
e.preventDefault();
smoothScroll(id);
history.replaceState(null, null, '#' + id);
});
}
setupHeadingLinks();
// Collection rendering check
if ($("#renderingProgress").length) {
setTimeout(function() {
location.reload();
}, 5000);
}
});
/** ==================== Combined Preferences Setup (custom typefaces, themes) ==================== **/
$(function() {
// Shared setup for both theme and font pickers
function initPreferences() {
setupThemePicker();
setupFontPicker();
}
/** ===== Theme Picker ===== **/
function setupThemePicker() {
var html = document.documentElement;
var currentMonth = new Date().getMonth();
var isJune = currentMonth === 5;
var themes = [
{value: 'day', label: 'Light', className: 'skin-citizen-light'},
{value: 'os', label: 'Auto', className: 'skin-citizen-auto'},
{value: 'night', label: 'Dark', className: 'skin-citizen-dark'},
{value: 'sepia', label: 'Sepia', className: 'skin-citizen-sepia', labelId: 'sepia-style-label'}
];
if (isJune) {
themes.push({
value: 'trans',
label: 'Trans',
className: 'skin-citizen-trans',
labelId: 'trans-style-label'
});
}
function applyTheme(themeValue) {
// Batch class operations by looping and removing each class
for (var i = 0; i < themes.length; i++) {
html.classList.remove(themes[i].className);
}
for (var j = 0; j < themes.length; j++) {
if (themes[j].value === themeValue) {
html.classList.add(themes[j].className);
break;
}
}
localStorage.setItem('skin-theme-clientpref', themeValue);
}
var storedTheme = localStorage.getItem('skin-theme-clientpref') || 'day';
if (storedTheme === 'trans' && !isJune) storedTheme = 'day';
applyTheme(storedTheme);
function addThemeOptions() {
var themeContainer = document.getElementById('skin-client-prefs-skin-theme');
if (!themeContainer) {
return setTimeout(addThemeOptions, 100);
}
var form = themeContainer.querySelector('form');
var template = form.querySelector('.citizen-client-prefs-radio');
if (!template) return;
for (var i = 0; i < themes.length; i++) {
var theme = themes[i];
var radioId = 'skin-client-pref-skin-theme-value-' + theme.value;
if (document.getElementById(radioId)) continue;
var radioElement = template.cloneNode(true);
var input = radioElement.querySelector('input');
var label = radioElement.querySelector('label');
input.id = radioId;
input.value = theme.value;
input.name = 'skin-client-pref-skin-theme-group';
input.checked = (storedTheme === theme.value);
label.setAttribute('for', radioId);
label.textContent = theme.label;
if (theme.labelId) {
label.id = theme.labelId;
}
form.appendChild(radioElement);
}
}
$(document).on('change', 'input[name="skin-client-pref-skin-theme-group"]', function() {
applyTheme(this.value);
});
addThemeOptions();
}
/** ===== Typeface Picker ===== **/
function setupFontPicker() {
var html = document.documentElement;
var FONT_STORAGE_KEY = 'skin-font-clientpref';
var TEXT_SIZE_ID = 'skin-client-prefs-citizen-feature-custom-font-size';
var fonts = [
{value: 'sans', label: 'Sans', className: 'font-sans'},
{value: 'serif', label: 'Serif', className: 'font-serif'},
{value: 'dyslexic', label: 'Accessibility', className: 'font-dyslexic'}
];
var activeFont = localStorage.getItem(FONT_STORAGE_KEY) || 'sans';
function applyFont(fontValue) {
for (var i = 0; i < fonts.length; i++) {
html.classList.remove(fonts[i].className);
}
if (fontValue !== 'sans') {
for (var j = 0; j < fonts.length; j++) {
if (fonts[j].value === fontValue) {
html.classList.add(fonts[j].className);
break;
}
}
}
localStorage.setItem(FONT_STORAGE_KEY, fontValue);
}
applyFont(activeFont);
function addFontOptions() {
var fontSection = document.getElementById('skin-client-prefs-font');
if (fontSection) return;
var prefsPanel = document.getElementById('citizen-client-prefs');
if (!prefsPanel) {
return setTimeout(addFontOptions, 100);
}
// Create new section using string HTML (ES5 safe)
var section = document.createElement('div');
section.className = 'mw-portlet mw-portlet-js citizen-menu';
section.id = 'skin-client-prefs-font';
section.innerHTML =
'<div class="citizen-menu__heading">Typeface</div>' +
'<div class="citizen-menu__content">' +
'<ul class="citizen-menu__content-list">' +
'<li class="mw-list-item mw-list-item-js">' +
'<form></form>' +
'</li>' +
'</ul>' +
'</div>';
var textSizeSection = document.getElementById(TEXT_SIZE_ID);
if (textSizeSection) {
if (textSizeSection.nextSibling) {
textSizeSection.parentNode.insertBefore(section, textSizeSection.nextSibling);
} else {
textSizeSection.parentNode.appendChild(section);
}
} else {
prefsPanel.appendChild(section);
}
var template = document.querySelector('.citizen-client-prefs-radio');
var form = section.querySelector('form');
if (!template || !form) return;
for (var i = 0; i < fonts.length; i++) {
var font = fonts[i];
var radioId = 'skin-client-pref-font-value-' + font.value;
var radioElement = template.cloneNode(true);
var input = radioElement.querySelector('input');
var label = radioElement.querySelector('label');
input.id = radioId;
input.value = font.value;
input.name = 'skin-client-pref-font-group';
input.checked = (activeFont === font.value);
label.setAttribute('for', radioId);
label.textContent = font.label;
// Add custom font class to the label
label.className = label.className + ' ' + font.className;
// Add ID for styling if needed
label.id = font.value + '-font-label';
form.appendChild(radioElement);
}
}
$(document).on('change', 'input[name="skin-client-pref-font-group"]', function() {
applyFont(this.value);
});
addFontOptions();
}
initPreferences();
});
/** ==================== Reading mode and features ==================== **/
/** (trigger, hoverline, progress bar) */
// Skip loading reading mode if not on desktop device:
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
$(function() {
var html = document.documentElement,
readingModeClass = 'reading-mode',
isReadingMode = false;
// Toggle reading mode
function toggleReadingMode() {
isReadingMode = !isReadingMode;
html.classList.toggle(readingModeClass, isReadingMode);
if (isReadingMode && window._paq) {
_paq.push(['trackGoal', 12]);
}
}
// Keyboard shortcut (desktop only)
if (!('ontouchstart' in window)) {
$(document).on('keydown', function(e) {
if ((e.keyCode === 48 || e.keyCode === 96) && // 0 key
!$(e.target).is('input, textarea, [contenteditable]')) {
toggleReadingMode();
}
});
}
// Hover line feature
function setupHoverLine() {
var $line = $('<div id="hover-line"></div>').css({
position: 'fixed',
left: 0,
width: '100%',
height: '1.5em',
pointerEvents: 'none',
zIndex: 1,
transition: 'top 0.1s ease, background-color 0.3s ease',
display: 'none',
backgroundColor: '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();
});
} //end reading mode IF statement
/** ==================== Visual Editor Welcome Message ==================== **/
mw.loader.using(['ext.visualEditor.desktopArticleTarget.init']).then(function () {
mw.hook('ve.activationComplete').add(function () {
var checkDialog = setInterval(function () {
var $dialogContent = $('.ve-init-mw-welcomeDialog-content');
if ($dialogContent.length) {
clearInterval(checkDialog);
// Only append once
if (!$dialogContent.data('custom-appended')) {
$dialogContent.append(
'<br><br>Please read our <strong><a href="/wiki/Principles">Principles</a></strong> and <strong><a href="/wiki/Encyclopedic_tone_guide">Encyclopedic tone guide</a><strong> first to make sure your edit is approved!'
);
$dialogContent.data('custom-appended', true);
// Resize the OOUI dialog to fit new content
var dialog = OO.ui.WindowManager.static.currentWindow;
if (dialog && typeof dialog.updateSize === 'function') {
dialog.updateSize();
}
}
}
}, 200);
});
});
/** ==================== Accreq Form Autosave ==================== **/
$(function() {
var $textarea = $('#wpBio');
if (!$textarea.length) return;
var STORAGE_KEY = 'form-bio-autosave',
$status = $('<div id="autosave-status" style="font-size:0.8em;color:gray;height:1.2em;margin-top:5px;"></div>'),
lastValue = '';
$textarea.after($status);
// Restore saved content
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
$textarea.val(saved);
lastValue = saved;
showStatus('Restored previous work');
}
// Setup autosave
$textarea.on('input', function() {
var current = $textarea.val();
if (current === lastValue) return;
showStatus('Saving...');
setTimeout(function() {
localStorage.setItem(STORAGE_KEY, current);
lastValue = current;
showStatus('Saved locally');
}, 2000);
});
// Clear on form submit
$textarea.closest('form').on('submit', function() {
localStorage.removeItem(STORAGE_KEY);
});
function showStatus(msg) {
$status.text(msg).stop().fadeIn().delay(3000).fadeOut();
}
});
/** ==================== Join us homepage testimonials ==================== **/
$(function() {
var $joinUs = $('#joinUs'),
$testimonial = $('#homepage-testimonial');
if (!$joinUs.length || !$testimonial.length) return;
var testimonials = [
'<span>"The ProleWiki community is very welcoming and diverse, 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: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();
}
});
(function () {
var imgUrl = 'https://shared.prolewiki.org/uploads/0/05/Cat.png '; // change to your cat image
var cat = document.createElement('img');
cat.id = 'cat-follower';
cat.src = imgUrl;
cat.alt = 'cat';
document.body.appendChild(cat);
var x = window.innerWidth / 2;
var y = window.innerHeight / 2;
var mx = x;
var my = y;
// adjust the follow speed (smaller = smoother / more delay)
var speed = 0.1;
document.addEventListener('mousemove', function (e) {
x = e.clientX;
y = e.clientY;
});
function animate() {
mx += (x - mx) * speed;
my += (y - my) * speed;
cat.style.left = mx + 'px';
cat.style.top = my + 'px';
requestAnimationFrame(animate);
}
animate();
})();