MediaWiki:Common.js
MediaWiki interface page
More actions
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
( function () {
'use strict';
var ACTIVE_CLASS = 'citizen-toc-list-item--active';
var ACTIVE_TOP_CLASS = 'citizen-toc-level-1--active';
var LINK_SELECTOR = '.citizen-toc-link:not(.citizen-toc-top)';
var NAVIGATION_KEYS = [
'ArrowDown',
'ArrowUp',
'End',
'Home',
'PageDown',
'PageUp',
' '
];
function setupHomeCarousel() {
var carousel = document.querySelector( '.lcd-home-carousel' );
if ( !carousel || carousel.dataset.techdocsCarouselReady === '1' ) {
return;
}
var slides = Array.prototype.slice.call(
carousel.querySelectorAll( '.lcd-home-carousel__slide' )
);
if ( slides.length === 0 ) {
return;
}
carousel.dataset.techdocsCarouselReady = '1';
var interval = parseInt( carousel.dataset.interval, 10 );
if ( !Number.isFinite( interval ) || interval < 1000 ) {
interval = 4000;
}
var currentIndex = Math.max( 0, slides.findIndex( function ( slide ) {
return slide.classList.contains( 'lcd-home-carousel__slide--active' );
} ) );
var timerId = 0;
var dots = [];
var dotsContainer = document.createElement( 'div' );
dotsContainer.className = 'lcd-home-carousel__dots';
dotsContainer.setAttribute( 'role', 'group' );
dotsContainer.setAttribute( 'aria-label', '轮播进度' );
function showSlide( index ) {
currentIndex = ( index + slides.length ) % slides.length;
slides.forEach( function ( slide, slideIndex ) {
var isActive = slideIndex === currentIndex;
slide.classList.toggle(
'lcd-home-carousel__slide--active',
isActive
);
slide.setAttribute( 'aria-hidden', isActive ? 'false' : 'true' );
} );
dots.forEach( function ( dot, dotIndex ) {
if ( dotIndex === currentIndex ) {
dot.setAttribute( 'aria-current', 'true' );
} else {
dot.removeAttribute( 'aria-current' );
}
} );
}
function stopTimer() {
if ( timerId ) {
window.clearInterval( timerId );
timerId = 0;
}
}
function startTimer() {
stopTimer();
if ( slides.length > 1 && !document.hidden ) {
timerId = window.setInterval( function () {
showSlide( currentIndex + 1 );
}, interval );
}
}
slides.forEach( function ( slide, slideIndex ) {
var dot = document.createElement( 'button' );
dot.type = 'button';
dot.className = 'lcd-home-carousel__dot';
dot.setAttribute(
'aria-label',
'显示第 ' + ( slideIndex + 1 ) + ' 张轮播图片'
);
dot.addEventListener( 'click', function () {
showSlide( slideIndex );
startTimer();
} );
dots.push( dot );
dotsContainer.appendChild( dot );
} );
carousel.appendChild( dotsContainer );
showSlide( currentIndex );
startTimer();
document.addEventListener( 'visibilitychange', function () {
if ( document.hidden ) {
stopTimer();
} else {
startTimer();
}
} );
window.addEventListener( 'pagehide', stopTimer );
}
function setupUniqueProductTargets() {
var candidateFields = Array.prototype.slice.call(
document.querySelectorAll(
'input[name^="首页产品卡["][name$="[页面]"]'
)
);
var fields = candidateFields.filter( function ( field ) {
return field.name.indexOf( '[map_field]' ) === -1;
} );
var form = fields.length > 0 ? fields[ 0 ].closest( 'form' ) : null;
if ( !form || form.dataset.techdocsUniqueProductsReady === '1' ) {
return;
}
form.dataset.techdocsUniqueProductsReady = '1';
var message = document.createElement( 'div' );
message.className = 'errorbox';
message.hidden = true;
message.setAttribute( 'role', 'alert' );
form.insertBefore( message, form.firstChild );
form.addEventListener( 'input', function () {
message.hidden = true;
message.textContent = '';
} );
form.addEventListener( 'submit', function ( event ) {
var seen = Object.create( null );
var duplicate = null;
var currentFields = Array.prototype.filter.call(
form.querySelectorAll(
'input[name^="首页产品卡["][name$="[页面]"]'
),
function ( field ) {
return field.name.indexOf( '[map_field]' ) === -1;
}
);
currentFields.some( function ( field ) {
var value = field.value.trim();
if ( value && seen[ value ] ) {
duplicate = field;
return true;
}
if ( value ) {
seen[ value ] = true;
}
return false;
} );
if ( !duplicate ) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
message.textContent =
'同一产品页面不能被多张首页卡片重复使用,请为每张卡片选择独立产品。';
message.hidden = false;
duplicate.focus();
message.scrollIntoView( { block: 'center' } );
}, true );
}
function decodeFragment( hash ) {
if ( !hash || hash.charAt( 0 ) !== '#' ) {
return '';
}
try {
return decodeURIComponent( hash.slice( 1 ) );
} catch ( error ) {
return hash.slice( 1 );
}
}
function setupSingleActiveToc() {
var toc = document.querySelector( '.citizen-toc' );
var indicator = toc && toc.querySelector( '.citizen-toc-indicator' );
if ( !toc || !indicator || toc.dataset.techdocsSingleActiveToc === '1' ) {
return;
}
var entries = Array.prototype.map.call(
toc.querySelectorAll( LINK_SELECTOR ),
function ( link ) {
var target = document.getElementById( decodeFragment( link.hash ) );
var item = link.closest( '.citizen-toc-list-item' );
return target && item ? {
item: item,
link: link,
target: target
} : null;
}
).filter( Boolean );
if ( entries.length === 0 ) {
return;
}
toc.dataset.techdocsSingleActiveToc = '1';
var applying = false;
var frameId = 0;
var manualEntry = null;
function getActivationTop() {
var value = parseFloat(
window.getComputedStyle( document.documentElement ).scrollPaddingTop
);
return Number.isFinite( value ) ? value : 0;
}
function findEntryForHash() {
var id = decodeFragment( window.location.hash );
return entries.find( function ( entry ) {
return entry.target.id === id;
} ) || null;
}
function findEntryForScroll() {
var activationTop = getActivationTop();
var selected = entries[ 0 ];
entries.some( function ( entry ) {
if ( entry.target.getBoundingClientRect().top <= activationTop + 1 ) {
selected = entry;
return false;
}
return true;
} );
return selected;
}
function updateIndicator( entry ) {
var positioningParent = indicator.offsetParent || toc;
var parentRect = positioningParent.getBoundingClientRect();
var linkRect = entry.link.getBoundingClientRect();
var unitHeight = linkRect.height || entry.link.offsetHeight || 32;
var top = linkRect.top - parentRect.top + positioningParent.scrollTop;
indicator.style.setProperty( '--indicator-unit-height', unitHeight + 'px' );
indicator.style.setProperty( '--indicator-top', top + 'px' );
indicator.style.setProperty( '--indicator-scale', '1' );
}
function applyEntry( entry ) {
if ( !entry ) {
return;
}
applying = true;
var topItem = entry.item.closest( '.citizen-toc-level-1' );
entries.forEach( function ( candidate ) {
var isActive = candidate.item === entry.item;
candidate.item.classList.toggle( ACTIVE_CLASS, isActive );
candidate.item.classList.toggle(
ACTIVE_TOP_CLASS,
candidate.item === topItem
);
if ( isActive ) {
candidate.link.setAttribute( 'aria-current', 'location' );
} else {
candidate.link.removeAttribute( 'aria-current' );
}
} );
if ( topItem && !entries.some( function ( candidate ) {
return candidate.item === topItem;
} ) ) {
topItem.classList.add( ACTIVE_TOP_CLASS );
}
updateIndicator( entry );
applying = false;
}
function update() {
frameId = 0;
applyEntry( manualEntry || findEntryForScroll() );
}
function scheduleUpdate() {
if ( frameId === 0 ) {
frameId = window.requestAnimationFrame( update );
}
}
function releaseManualEntry() {
if ( manualEntry ) {
manualEntry = null;
scheduleUpdate();
}
}
toc.addEventListener( 'click', function ( event ) {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
var link = event.target.closest( LINK_SELECTOR );
if ( !link || !toc.contains( link ) ) {
return;
}
var entry = entries.find( function ( candidate ) {
return candidate.link === link;
} );
if ( !entry ) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
manualEntry = entry;
applyEntry( entry );
var hash = link.getAttribute( 'href' );
if ( hash ) {
window.history.pushState( null, '', hash );
}
window.requestAnimationFrame( function () {
var targetTop = window.scrollY +
entry.target.getBoundingClientRect().top -
getActivationTop();
window.scrollTo( 0, Math.max( 0, targetTop ) );
} );
}, true );
window.addEventListener( 'scroll', scheduleUpdate, { passive: true } );
window.addEventListener( 'resize', scheduleUpdate );
window.addEventListener( 'wheel', releaseManualEntry, { passive: true } );
window.addEventListener( 'touchstart', releaseManualEntry, { passive: true } );
window.addEventListener( 'pointerdown', function ( event ) {
if ( !toc.contains( event.target ) ) {
releaseManualEntry();
}
}, true );
window.addEventListener( 'keydown', function ( event ) {
if ( NAVIGATION_KEYS.indexOf( event.key ) !== -1 ) {
releaseManualEntry();
}
} );
window.addEventListener( 'hashchange', function () {
manualEntry = findEntryForHash();
scheduleUpdate();
} );
var classObserver = new MutationObserver( function () {
if ( !applying ) {
scheduleUpdate();
}
} );
classObserver.observe( toc, {
attributes: true,
attributeFilter: [ 'class' ],
subtree: true
} );
scheduleUpdate();
}
mw.loader.using( 'skins.citizen.scripts' ).then( function () {
window.requestAnimationFrame( function () {
setupHomeCarousel();
setupUniqueProductTargets();
setupSingleActiveToc();
} );
} );
}() );
/* ===== Custom VE Foreground Color Tool =====
Registers a 'foreground' tool in VisualEditor's tool factory.
VEForAll's toolbar config references 'foreground' in text-style include.
This tool shows a color palette popup and applies the selected color
to the currently selected text in the VE surface. */
(function () {
function initColorTool() {
if (typeof ve === 'undefined' || !ve.ui || !ve.ui.Tool || !OO) {
setTimeout(initColorTool, 200);
return;
}
// Skip if already registered
if (ve.ui.toolFactory.lookup('foreground')) return;
function ForegroundColorTool() {
ForegroundColorTool.super.apply(this, arguments);
this.$popup = null;
}
OO.inheritClass(ForegroundColorTool, ve.ui.Tool);
ForegroundColorTool.static.name = 'foreground';
ForegroundColorTool.static.group = 'textStyle';
ForegroundColorTool.static.icon = 'highlight';
ForegroundColorTool.static.title = OO.ui.deferMsg('visualeditor-foreground-color-tool', 'Text color');
ForegroundColorTool.static.autoAdd = false;
ForegroundColorTool.static.supportsPressEvents = true;
ForegroundColorTool.prototype.onSelect = function () {
var tool = this;
var surface = tool.toolbar.getSurface();
if (!surface) return;
// Remove existing popup
if (tool.$popup) { tool.$popup.remove(); tool.$popup = null; return; }
var colors = [
'#000000', '#666666', '#999999', '#cccccc', '#ffffff',
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db',
'#9b59b6', '#e84393', '#34B0B0', '#0984e3', '#00b894'
];
var $popup = $('<div>').css({
position: 'absolute',
background: '#fff',
border: '1px solid #ccc',
borderRadius: '6px',
padding: '8px',
display: 'grid',
gridTemplateColumns: 'repeat(5, 1fr)',
gap: '4px',
zIndex: 10000,
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
});
colors.forEach(function (color) {
$('<div>').css({
width: '22px',
height: '22px',
background: color,
cursor: 'pointer',
border: '1px solid #ddd',
borderRadius: '3px'
}).on('mousedown', function (e) {
e.preventDefault();
// Ensure surface has focus
var surfaceView = surface.getView();
if (surfaceView) surfaceView.focus();
// Apply color via execCommand on the contentEditable
document.execCommand('foreColor', false, color);
// Also try VE native annotation if available
try {
var fragment = surface.getModel().getFragment();
var view = surface.getView();
if (view && view.$attachedRootNode) {
// Force VE to pick up the DOM change
surface.getModel().emit('select', surface.getModel().getSelection());
}
} catch (err) {}
$popup.remove();
tool.$popup = null;
}).appendTo($popup);
});
// Position below the button
var offset = tool.$element.offset();
$popup.css({
top: offset.top + tool.$element.outerHeight() + 4,
left: offset.left
});
$('body').append($popup);
tool.$popup = $popup;
// Close on outside click
setTimeout(function () {
$(document).one('mousedown', function (e) {
if (!$(e.target).closest($popup).length) {
$popup.remove();
tool.$popup = null;
}
});
}, 50);
};
ForegroundColorTool.prototype.onUpdateState = function () {};
ve.ui.toolFactory.register(ForegroundColorTool);
console.log('[VEForAll] Foreground color tool registered');
}
// Try immediately, then poll
initColorTool();
// Also try when VE modules load
if (typeof mw !== 'undefined' && mw.loader) {
mw.loader.using(['ext.veforall.target']).done(initColorTool).fail(function(){ setTimeout(initColorTool, 500); });
}
})();