Toggle menu
Toggle preferences menu
Toggle personal menu
Administrator Login Only
Access is restricted to site administrators.

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
Git release 76669c0710f1
Git release 7dd2928f7307
 
(8 intermediate revisions by the same user not shown)
Line 112: Line 112:
} );
} );
window.addEventListener( 'pagehide', stopTimer );
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 );
}
}


Line 325: Line 390:
window.requestAnimationFrame( function () {
window.requestAnimationFrame( function () {
setupHomeCarousel();
setupHomeCarousel();
setupUniqueProductTargets();
setupSingleActiveToc();
setupSingleActiveToc();
} );
} );
} );
}() );
/* ===== VisualEditor foreground color tool =====
* Register a real VisualEditor annotation instead of trying to insert span
* nodes through TransactionBuilder. This keeps the selection in the VE model,
* renders the color immediately, and round-trips as
* <span style="color: ...">...</span> through Parsoid.
*/
( function () {
'use strict';
var retryTimer = 0;
var annotationName = 'textStyle/foregroundColor';
var palette = [
'#000000', '#666666', '#999999', '#cccccc', '#ffffff',
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db',
'#9b59b6', '#e84393', '#34b0b0', '#0984e3', '#00b894'
];
function registerAnnotation() {
if ( !ve.dm.modelRegistry.lookup( annotationName ) ) {
ve.dm.ForegroundColorAnnotation = function VeDmForegroundColorAnnotation() {
ve.dm.ForegroundColorAnnotation.super.apply( this, arguments );
};
OO.inheritClass(
ve.dm.ForegroundColorAnnotation,
ve.dm.TextStyleAnnotation
);
ve.dm.ForegroundColorAnnotation.static.name = annotationName;
ve.dm.ForegroundColorAnnotation.static.matchTagNames = [ 'span' ];
ve.dm.ForegroundColorAnnotation.static.matchFunction = function ( domElement ) {
return Boolean( domElement.style && domElement.style.color );
};
ve.dm.ForegroundColorAnnotation.static.toDataElement = function ( domElements ) {
return {
type: annotationName,
attributes: {
color: domElements[ 0 ].style.color
}
};
};
ve.dm.ForegroundColorAnnotation.static.toDomElements = function ( dataElement, doc ) {
var span = doc.createElement( 'span' );
span.style.color = dataElement.attributes.color;
return [ span ];
};
ve.dm.ForegroundColorAnnotation.prototype.getComparableObject = function () {
return {
type: annotationName,
color: this.getAttribute( 'color' )
};
};
ve.dm.modelRegistry.register( ve.dm.ForegroundColorAnnotation );
}
if ( !ve.ce.annotationFactory.lookup( annotationName ) ) {
ve.ce.ForegroundColorAnnotation = function VeCeForegroundColorAnnotation() {
ve.ce.ForegroundColorAnnotation.super.apply( this, arguments );
this.$element.css( 'color', this.model.getAttribute( 'color' ) );
};
OO.inheritClass(
ve.ce.ForegroundColorAnnotation,
ve.ce.TextStyleAnnotation
);
ve.ce.ForegroundColorAnnotation.static.name = annotationName;
ve.ce.ForegroundColorAnnotation.static.tagName = 'span';
ve.ce.annotationFactory.register( ve.ce.ForegroundColorAnnotation );
}
}
function registerTool() {
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 = 'Text color';
ForegroundColorTool.static.autoAdd = false;
ForegroundColorTool.static.supportsPressEvents = true;
ForegroundColorTool.prototype.applyColor = function ( surface, color ) {
var surfaceModel = surface.getModel();
var fragment = surfaceModel.getFragment();
var selection = fragment.getSelection();
if ( selection.isCollapsed() ) {
surface.getView().focus();
return;
}
fragment = fragment.trimLinearSelection();
fragment.annotateContent( 'clear', annotationName );
if ( color ) {
fragment.annotateContent(
'set',
new ve.dm.ForegroundColorAnnotation( {
type: annotationName,
attributes: { color: color }
} )
);
}
surface.getView().focus();
};
ForegroundColorTool.prototype.onSelect = function () {
var tool = this;
var surface = tool.toolbar.getSurface();
var offset;
if ( !surface ) {
return;
}
if ( tool.$popup ) {
tool.$popup.remove();
tool.$popup = null;
return;
}
tool.$popup = $( '<div>' )
.addClass( 'techdocs-ve-color-palette' )
.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)'
} );
$( '<button>' )
.attr( {
type: 'button',
title: '默认颜色(跟随网页样式)',
'aria-label': '默认颜色(跟随网页样式)'
} )
.text( '默认颜色' )
.css( {
gridColumn: '1 / -1',
height: '26px',
padding: '0 8px',
background: '#fff',
color: '#202122',
cursor: 'pointer',
border: '1px solid #a2a9b1',
borderRadius: '3px',
fontSize: '12px',
lineHeight: '24px',
textAlign: 'center'
} )
.on( 'mousedown', function ( event ) {
event.preventDefault();
event.stopPropagation();
tool.applyColor( surface, null );
tool.$popup.remove();
tool.$popup = null;
} )
.appendTo( tool.$popup );
palette.forEach( function ( color ) {
$( '<button>' )
.attr( {
type: 'button',
title: color,
'aria-label': color
} )
.css( {
width: '22px',
height: '22px',
padding: 0,
background: color,
cursor: 'pointer',
border: '1px solid #ddd',
borderRadius: '3px'
} )
.on( 'mousedown', function ( event ) {
event.preventDefault();
event.stopPropagation();
tool.applyColor( surface, color );
tool.$popup.remove();
tool.$popup = null;
} )
.appendTo( tool.$popup );
} );
offset = tool.$element.offset();
tool.$popup.css( {
top: offset.top + tool.$element.outerHeight() + 4,
left: offset.left
} );
$( 'body' ).append( tool.$popup );
setTimeout( function () {
$( document ).one( 'mousedown', function ( event ) {
if (
tool.$popup &&
!$( event.target ).closest( tool.$popup ).length
) {
tool.$popup.remove();
tool.$popup = null;
}
} );
}, 50 );
};
ForegroundColorTool.prototype.onUpdateState = function () {
this.setDisabled( false );
};
ve.ui.toolFactory.register( ForegroundColorTool );
}
function initialize() {
if (
typeof ve === 'undefined' ||
!ve.dm ||
!ve.dm.TextStyleAnnotation ||
!ve.ce ||
!ve.ce.TextStyleAnnotation ||
!ve.ui ||
!ve.ui.Tool ||
typeof OO === 'undefined'
) {
if ( !retryTimer ) {
retryTimer = setTimeout( function () {
retryTimer = 0;
initialize();
}, 200 );
}
return;
}
registerAnnotation();
registerTool();
}
initialize();
mw.loader.using( 'ext.veforall.target' ).then( initialize, function () {
if ( !retryTimer ) {
retryTimer = setTimeout( initialize, 500 );
}
} );
} );
}() );
}() );

Latest revision as of 21:50, 10 August 2026

( 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();
		} );
	} );
}() );

/* ===== VisualEditor foreground color tool =====
 * Register a real VisualEditor annotation instead of trying to insert span
 * nodes through TransactionBuilder. This keeps the selection in the VE model,
 * renders the color immediately, and round-trips as
 * <span style="color: ...">...</span> through Parsoid.
 */
( function () {
	'use strict';

	var retryTimer = 0;
	var annotationName = 'textStyle/foregroundColor';
	var palette = [
		'#000000', '#666666', '#999999', '#cccccc', '#ffffff',
		'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db',
		'#9b59b6', '#e84393', '#34b0b0', '#0984e3', '#00b894'
	];

	function registerAnnotation() {
		if ( !ve.dm.modelRegistry.lookup( annotationName ) ) {
			ve.dm.ForegroundColorAnnotation = function VeDmForegroundColorAnnotation() {
				ve.dm.ForegroundColorAnnotation.super.apply( this, arguments );
			};
			OO.inheritClass(
				ve.dm.ForegroundColorAnnotation,
				ve.dm.TextStyleAnnotation
			);
			ve.dm.ForegroundColorAnnotation.static.name = annotationName;
			ve.dm.ForegroundColorAnnotation.static.matchTagNames = [ 'span' ];
			ve.dm.ForegroundColorAnnotation.static.matchFunction = function ( domElement ) {
				return Boolean( domElement.style && domElement.style.color );
			};
			ve.dm.ForegroundColorAnnotation.static.toDataElement = function ( domElements ) {
				return {
					type: annotationName,
					attributes: {
						color: domElements[ 0 ].style.color
					}
				};
			};
			ve.dm.ForegroundColorAnnotation.static.toDomElements = function ( dataElement, doc ) {
				var span = doc.createElement( 'span' );
				span.style.color = dataElement.attributes.color;
				return [ span ];
			};
			ve.dm.ForegroundColorAnnotation.prototype.getComparableObject = function () {
				return {
					type: annotationName,
					color: this.getAttribute( 'color' )
				};
			};
			ve.dm.modelRegistry.register( ve.dm.ForegroundColorAnnotation );
		}

		if ( !ve.ce.annotationFactory.lookup( annotationName ) ) {
			ve.ce.ForegroundColorAnnotation = function VeCeForegroundColorAnnotation() {
				ve.ce.ForegroundColorAnnotation.super.apply( this, arguments );
				this.$element.css( 'color', this.model.getAttribute( 'color' ) );
			};
			OO.inheritClass(
				ve.ce.ForegroundColorAnnotation,
				ve.ce.TextStyleAnnotation
			);
			ve.ce.ForegroundColorAnnotation.static.name = annotationName;
			ve.ce.ForegroundColorAnnotation.static.tagName = 'span';
			ve.ce.annotationFactory.register( ve.ce.ForegroundColorAnnotation );
		}
	}

	function registerTool() {
		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 = 'Text color';
		ForegroundColorTool.static.autoAdd = false;
		ForegroundColorTool.static.supportsPressEvents = true;

		ForegroundColorTool.prototype.applyColor = function ( surface, color ) {
			var surfaceModel = surface.getModel();
			var fragment = surfaceModel.getFragment();
			var selection = fragment.getSelection();

			if ( selection.isCollapsed() ) {
				surface.getView().focus();
				return;
			}

			fragment = fragment.trimLinearSelection();
			fragment.annotateContent( 'clear', annotationName );
			if ( color ) {
				fragment.annotateContent(
					'set',
					new ve.dm.ForegroundColorAnnotation( {
						type: annotationName,
						attributes: { color: color }
					} )
				);
			}
			surface.getView().focus();
		};

		ForegroundColorTool.prototype.onSelect = function () {
			var tool = this;
			var surface = tool.toolbar.getSurface();
			var offset;

			if ( !surface ) {
				return;
			}
			if ( tool.$popup ) {
				tool.$popup.remove();
				tool.$popup = null;
				return;
			}

			tool.$popup = $( '<div>' )
				.addClass( 'techdocs-ve-color-palette' )
				.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)'
				} );

			$( '<button>' )
				.attr( {
					type: 'button',
					title: '默认颜色(跟随网页样式)',
					'aria-label': '默认颜色(跟随网页样式)'
				} )
				.text( '默认颜色' )
				.css( {
					gridColumn: '1 / -1',
					height: '26px',
					padding: '0 8px',
					background: '#fff',
					color: '#202122',
					cursor: 'pointer',
					border: '1px solid #a2a9b1',
					borderRadius: '3px',
					fontSize: '12px',
					lineHeight: '24px',
					textAlign: 'center'
				} )
				.on( 'mousedown', function ( event ) {
					event.preventDefault();
					event.stopPropagation();
					tool.applyColor( surface, null );
					tool.$popup.remove();
					tool.$popup = null;
				} )
				.appendTo( tool.$popup );

			palette.forEach( function ( color ) {
				$( '<button>' )
					.attr( {
						type: 'button',
						title: color,
						'aria-label': color
					} )
					.css( {
						width: '22px',
						height: '22px',
						padding: 0,
						background: color,
						cursor: 'pointer',
						border: '1px solid #ddd',
						borderRadius: '3px'
					} )
					.on( 'mousedown', function ( event ) {
						event.preventDefault();
						event.stopPropagation();
						tool.applyColor( surface, color );
						tool.$popup.remove();
						tool.$popup = null;
					} )
					.appendTo( tool.$popup );
			} );

			offset = tool.$element.offset();
			tool.$popup.css( {
				top: offset.top + tool.$element.outerHeight() + 4,
				left: offset.left
			} );
			$( 'body' ).append( tool.$popup );

			setTimeout( function () {
				$( document ).one( 'mousedown', function ( event ) {
					if (
						tool.$popup &&
						!$( event.target ).closest( tool.$popup ).length
					) {
						tool.$popup.remove();
						tool.$popup = null;
					}
				} );
			}, 50 );
		};

		ForegroundColorTool.prototype.onUpdateState = function () {
			this.setDisabled( false );
		};

		ve.ui.toolFactory.register( ForegroundColorTool );
	}

	function initialize() {
		if (
			typeof ve === 'undefined' ||
			!ve.dm ||
			!ve.dm.TextStyleAnnotation ||
			!ve.ce ||
			!ve.ce.TextStyleAnnotation ||
			!ve.ui ||
			!ve.ui.Tool ||
			typeof OO === 'undefined'
		) {
			if ( !retryTimer ) {
				retryTimer = setTimeout( function () {
					retryTimer = 0;
					initialize();
				}, 200 );
			}
			return;
		}

		registerAnnotation();
		registerTool();
	}

	initialize();
	mw.loader.using( 'ext.veforall.target' ).then( initialize, function () {
		if ( !retryTimer ) {
			retryTimer = setTimeout( initialize, 500 );
		}
	} );
}() );