MediaWiki:Common.js: Difference between revisions
Appearance
v2.0: Dark mode toggle, animated counters, helpful widget, copy buttons, scroll reveal, keyboard shortcuts |
Add advanced JS logic for Create Article live validation |
||
| Line 217: | Line 217: | ||
}() ); | }() ); | ||
// --- Advanced Create Article Widget --- | |||
(function($, mw) { | |||
$(function() { | |||
var $input = $('#create-article-title'); | |||
var $btn = $('#create-article-btn'); | |||
var $status = $('#create-article-status'); | |||
var debounceTimer; | |||
if (!$input.length) return; // Only run on Main Page where widget exists | |||
$input.on('input', function() { | |||
var title = $(this).val().trim(); | |||
clearTimeout(debounceTimer); | |||
$btn.prop('disabled', true); | |||
$input.removeClass('is-valid is-invalid'); | |||
if (title.length === 0) { | |||
$status.html('').removeClass('status-error status-success status-checking'); | |||
return; | |||
} | |||
// Capitalize first letter to match MW behavior | |||
title = title.charAt(0).toUpperCase() + title.slice(1); | |||
$status.html('⏳ Checking availability...').attr('class', 'status-checking'); | |||
debounceTimer = setTimeout(function() { | |||
var api = new mw.Api(); | |||
api.get({ | |||
action: 'query', | |||
titles: title, | |||
format: 'json' | |||
}).done(function(data) { | |||
var pages = data.query.pages; | |||
var pageId = Object.keys(pages)[0]; | |||
if (pageId !== '-1') { | |||
// Page exists | |||
$status.html('⚠️ Article already exists! <a href="' + mw.util.getUrl(title) + '">View it here</a>').attr('class', 'status-error'); | |||
$input.addClass('is-invalid'); | |||
$btn.prop('disabled', true); | |||
} else { | |||
// Page does not exist | |||
var editUrl = mw.util.getUrl(title, { | |||
action: 'edit', | |||
preload: 'Template:New_CMS_Draft', | |||
editintro: 'Template:Create_Intro' | |||
}); | |||
$status.html('✅ Name is available!').attr('class', 'status-success'); | |||
$input.addClass('is-valid'); | |||
$btn.prop('disabled', false).off('click').on('click', function() { | |||
window.location.href = editUrl; | |||
}); | |||
// Also submit on Enter key | |||
$input.off('keypress').on('keypress', function(e) { | |||
if (e.which === 13) { | |||
window.location.href = editUrl; | |||
} | |||
}); | |||
} | |||
}).fail(function() { | |||
$status.html('❌ Error checking API.').attr('class', 'status-error'); | |||
}); | |||
}, 400); // 400ms debounce | |||
}); | |||
}); | |||
}(jQuery, mediaWiki)); | |||
// --- End Advanced Create Widget --- | |||
Revision as of 22:33, 29 July 2026
/* ════════════════════════════════════════════════════════════════════
ZelocoreCMS Wiki — Enhanced Common.js (v2.0 world-class upgrade)
Features: dark mode, animated counters, helpful widget,
keyboard shortcuts, reading progress, back-to-top
════════════════════════════════════════════════════════════════════ */
( function () {
'use strict';
var qs = function ( s, ctx ) { return ( ctx || document ).querySelector( s ); };
var qsa = function ( s, ctx ) { return Array.from( ( ctx || document ).querySelectorAll( s ) ); };
function ready( fn ) {
if ( document.readyState !== 'loading' ) { fn(); }
else { document.addEventListener( 'DOMContentLoaded', fn ); }
}
/* ── Reading progress bar ─────────────────────────── */
function initProgress() {
var bar = document.createElement( 'div' );
bar.id = 'cms-progress';
document.body.prepend( bar );
window.addEventListener( 'scroll', function () {
var h = document.documentElement;
var pct = ( h.scrollTop / ( h.scrollHeight - h.clientHeight ) ) * 100;
bar.style.width = Math.min( pct, 100 ) + '%';
}, { passive: true } );
}
/* ── Back to top ──────────────────────────────────── */
function initBackTop() {
var btn = document.createElement( 'button' );
btn.id = 'cms-back-top';
btn.title = 'Back to top';
btn.innerHTML = '↑';
document.body.appendChild( btn );
window.addEventListener( 'scroll', function () {
btn.classList.toggle( 'visible', window.scrollY > 400 );
}, { passive: true } );
btn.addEventListener( 'click', function () {
window.scrollTo( { top: 0, behavior: 'smooth' } );
} );
}
/* ── Dark mode toggle ─────────────────────────────── */
function initDarkMode() {
var btn = document.createElement( 'button' );
btn.id = 'cms-dark-toggle';
btn.title = 'Toggle dark mode';
btn.innerHTML = '🌙';
document.body.appendChild( btn );
var isDark = localStorage.getItem( 'cms-dark' ) === '1';
if ( isDark ) { document.body.classList.add( 'cms-dark-mode' ); btn.innerHTML = '☀️'; }
btn.addEventListener( 'click', function () {
isDark = !isDark;
document.body.classList.toggle( 'cms-dark-mode', isDark );
btn.innerHTML = isDark ? '☀️' : '🌙';
localStorage.setItem( 'cms-dark', isDark ? '1' : '0' );
} );
}
/* ── Animated number counters (main page stats) ───── */
function animateCounter( el, target ) {
var start = 0;
var duration = 1400;
var startTime = null;
function step( ts ) {
if ( !startTime ) { startTime = ts; }
var progress = Math.min( ( ts - startTime ) / duration, 1 );
var ease = 1 - Math.pow( 1 - progress, 3 );
el.textContent = Math.floor( ease * target ).toLocaleString();
if ( progress < 1 ) { requestAnimationFrame( step ); }
else { el.textContent = target.toLocaleString(); }
}
requestAnimationFrame( step );
}
function initCounters() {
qsa( '.mp-stat-num' ).forEach( function ( el ) {
var raw = el.textContent.replace( /,/g, '' );
var num = parseInt( raw, 10 );
if ( !isNaN( num ) && num > 0 ) {
var obs = new IntersectionObserver( function ( entries, o ) {
if ( entries[0].isIntersecting ) {
animateCounter( el, num );
o.disconnect();
}
}, { threshold: 0.3 } );
obs.observe( el );
}
} );
}
/* ── "Was this helpful?" widget ───────────────────── */
function initHelpfulWidget() {
var body = qs( '#mw-content-text .mw-parser-output' );
if ( !body ) { return; }
// Only on article pages, not main page
if ( document.body.classList.contains( 'page-Main_Page' ) ) { return; }
if ( !mw.config.get( 'wgIsArticle' ) ) { return; }
var widget = document.createElement( 'div' );
widget.id = 'cms-helpful';
widget.innerHTML = [
'<span id="cms-helpful-label">Was this article helpful?</span>',
'<button class="cms-helpful-btn yes" data-v="yes">👍 Yes</button>',
'<button class="cms-helpful-btn no" data-v="no">👎 No</button>',
'<span id="cms-helpful-thanks">Thanks for your feedback! 🎉</span>'
].join( '' );
body.appendChild( widget );
qsa( '.cms-helpful-btn', widget ).forEach( function ( btn ) {
btn.addEventListener( 'click', function () {
qsa( '.cms-helpful-btn', widget ).forEach( function ( b ) { b.classList.remove( 'active' ); } );
btn.classList.add( 'active' );
qs( '#cms-helpful-thanks' ).style.display = 'inline';
} );
} );
}
/* ── Keyboard shortcut: Ctrl+K = focus search ─────── */
function initSearchShortcut() {
document.addEventListener( 'keydown', function ( e ) {
if ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) {
e.preventDefault();
var input = qs( '#searchInput, .cdx-text-input__input' );
if ( input ) { input.focus(); input.select(); }
}
} );
}
/* ── Copy buttons on <pre> blocks ─────────────────── */
function addCopyButtons() {
qsa( 'pre' ).forEach( function ( pre ) {
if ( qs( '.cms-copy-btn', pre ) ) { return; }
var btn = document.createElement( 'button' );
btn.className = 'cms-copy-btn';
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:8px;right:10px;padding:3px 10px;' +
'background:rgba(255,255,255,.15);color:#e2e8f0;border:1px solid rgba(255,255,255,.25);' +
'border-radius:4px;font-size:.78em;cursor:pointer;transition:background .2s;';
btn.addEventListener( 'mouseenter', function () { btn.style.background = 'rgba(255,255,255,.28)'; } );
btn.addEventListener( 'mouseleave', function () { btn.style.background = 'rgba(255,255,255,.15)'; } );
btn.addEventListener( 'click', function () {
navigator.clipboard.writeText( pre.innerText ).then( function () {
btn.textContent = '✅ Copied!';
setTimeout( function () { btn.textContent = 'Copy'; }, 2000 );
} );
} );
pre.style.position = 'relative';
pre.appendChild( btn );
} );
}
/* ── Scroll reveal for portal cells ──────────────── */
function initScrollReveal() {
if ( !window.IntersectionObserver ) { return; }
var obs = new IntersectionObserver( function ( entries ) {
entries.forEach( function ( entry ) {
if ( entry.isIntersecting ) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
obs.unobserve( entry.target );
}
} );
}, { threshold: 0.06 } );
qsa( '.mp-portal-cell, .mp-cat-card, .mp-card, .mp-stat' ).forEach( function ( el ) {
el.style.opacity = '0';
el.style.transform = 'translateY(14px)';
el.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
obs.observe( el );
} );
}
/* ── Table row highlight ──────────────────────────── */
function initTableHighlight() {
qsa( '.wikitable tbody tr' ).forEach( function ( row ) {
row.style.transition = 'background .15s';
} );
}
/* ── External link icon ───────────────────────────── */
function initExternalLinks() {
qsa( '.mw-body-content a.external' ).forEach( function ( a ) {
if ( !qs( '.cms-ext', a ) ) {
var s = document.createElement( 'span' );
s.className = 'cms-ext';
s.setAttribute( 'aria-hidden', 'true' );
s.textContent = ' ↗';
s.style.cssText = 'font-size:.72em;color:#7c3aed;margin-left:1px;';
a.appendChild( s );
}
} );
}
/* ── Bootstrap ────────────────────────────────────── */
ready( function () {
initProgress();
initBackTop();
initDarkMode();
initCounters();
initSearchShortcut();
initScrollReveal();
initTableHighlight();
initExternalLinks();
setTimeout( addCopyButtons, 500 );
setTimeout( initHelpfulWidget, 300 );
if ( mw && mw.hook ) {
mw.hook( 've.deactivate' ).add( function () {
setTimeout( addCopyButtons, 600 );
} );
}
} );
}() );
// --- Advanced Create Article Widget ---
(function($, mw) {
$(function() {
var $input = $('#create-article-title');
var $btn = $('#create-article-btn');
var $status = $('#create-article-status');
var debounceTimer;
if (!$input.length) return; // Only run on Main Page where widget exists
$input.on('input', function() {
var title = $(this).val().trim();
clearTimeout(debounceTimer);
$btn.prop('disabled', true);
$input.removeClass('is-valid is-invalid');
if (title.length === 0) {
$status.html('').removeClass('status-error status-success status-checking');
return;
}
// Capitalize first letter to match MW behavior
title = title.charAt(0).toUpperCase() + title.slice(1);
$status.html('⏳ Checking availability...').attr('class', 'status-checking');
debounceTimer = setTimeout(function() {
var api = new mw.Api();
api.get({
action: 'query',
titles: title,
format: 'json'
}).done(function(data) {
var pages = data.query.pages;
var pageId = Object.keys(pages)[0];
if (pageId !== '-1') {
// Page exists
$status.html('⚠️ Article already exists! <a href="' + mw.util.getUrl(title) + '">View it here</a>').attr('class', 'status-error');
$input.addClass('is-invalid');
$btn.prop('disabled', true);
} else {
// Page does not exist
var editUrl = mw.util.getUrl(title, {
action: 'edit',
preload: 'Template:New_CMS_Draft',
editintro: 'Template:Create_Intro'
});
$status.html('✅ Name is available!').attr('class', 'status-success');
$input.addClass('is-valid');
$btn.prop('disabled', false).off('click').on('click', function() {
window.location.href = editUrl;
});
// Also submit on Enter key
$input.off('keypress').on('keypress', function(e) {
if (e.which === 13) {
window.location.href = editUrl;
}
});
}
}).fail(function() {
$status.html('❌ Error checking API.').attr('class', 'status-error');
});
}, 400); // 400ms debounce
});
});
}(jQuery, mediaWiki));
// --- End Advanced Create Widget ---