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
m Fix color tool v4: TransactionBuilder direct span insertion with style
Fix-VisualEditor-foreground-color-annotation
Line 396: Line 396:
}() );
}() );


/* ===== Custom VE Foreground Color Tool =====
/* ===== VisualEditor foreground color tool =====
  v4: Uses ve.dm.TransactionBuilder to commit inline color directly to
* Register a real VisualEditor annotation instead of trying to insert span
  the VE model as a proper transaction. Splits the selection into ranges
* nodes through TransactionBuilder. This keeps the selection in the VE model,
  per branch and applies the color annotation to each. */
* renders the color immediately, and round-trips as
(function () {
* <span style="color: ...">...</span> through Parsoid.
    function initColorTool() {
*/
        if (typeof ve === 'undefined' || !ve.ui || !ve.ui.Tool || !ve.dm || !OO) {
( function () {
            setTimeout(initColorTool, 200);
'use strict';
            return;
        }
        if (ve.ui.toolFactory.lookup('foreground')) return;


        function ForegroundColorTool() {
var retryTimer = 0;
            ForegroundColorTool.super.apply(this, arguments);
var annotationName = 'textStyle/foregroundColor';
            this.$popup = null;
var palette = [
        }
'#000000', '#666666', '#999999', '#cccccc', '#ffffff',
        OO.inheritClass(ForegroundColorTool, ve.ui.Tool);
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db',
        ForegroundColorTool.static.name = 'foreground';
'#9b59b6', '#e84393', '#34b0b0', '#0984e3', '#00b894'
        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 () {
function registerAnnotation() {
            var tool = this;
if ( !ve.dm.modelRegistry.lookup( annotationName ) ) {
            var surface = tool.toolbar.getSurface();
ve.dm.ForegroundColorAnnotation = function VeDmForegroundColorAnnotation() {
            if (!surface) return;
ve.dm.ForegroundColorAnnotation.super.apply( this, arguments );
            if (tool.$popup) { tool.$popup.remove(); tool.$popup = null; return; }
};
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 );
}


            var colors = [
if ( !ve.ce.annotationFactory.lookup( annotationName ) ) {
                '#000000', '#666666', '#999999', '#cccccc', '#ffffff',
ve.ce.ForegroundColorAnnotation = function VeCeForegroundColorAnnotation() {
                '#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db',
ve.ce.ForegroundColorAnnotation.super.apply( this, arguments );
                '#9b59b6', '#e84393', '#34B0B0', '#0984e3', '#00b894'
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 );
}
}


            var $popup = $('<div>').css({
function registerTool() {
                position: 'absolute', background: '#fff', border: '1px solid #ccc',
if ( ve.ui.toolFactory.lookup( 'foreground' ) ) {
                borderRadius: '6px', padding: '8px', display: 'grid',
return;
                gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px',
}
                zIndex: 10000, boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
            });


            colors.forEach(function (color) {
function ForegroundColorTool() {
                $('<div>').css({
ForegroundColorTool.super.apply( this, arguments );
                    width: '22px', height: '22px', background: color,
this.$popup = null;
                    cursor: 'pointer', border: '1px solid #ddd', borderRadius: '3px'
}
                }).on('mousedown', function (e) {
OO.inheritClass( ForegroundColorTool, ve.ui.Tool );
                    e.preventDefault();
ForegroundColorTool.static.name = 'foreground';
                    tool.applyColor(surface, color);
ForegroundColorTool.static.group = 'textStyle';
                    $popup.remove();
ForegroundColorTool.static.icon = 'highlight';
                    tool.$popup = null;
ForegroundColorTool.static.title = OO.ui.deferMsg(
                }).appendTo($popup);
'visualeditor-foreground-color-tool',
            });
'Text color'
);
ForegroundColorTool.static.autoAdd = false;
ForegroundColorTool.static.supportsPressEvents = true;


            var offset = tool.$element.offset();
ForegroundColorTool.prototype.applyColor = function ( surface, color ) {
            $popup.css({ top: offset.top + tool.$element.outerHeight() + 4, left: offset.left });
var surfaceModel = surface.getModel();
            $('body').append($popup);
var fragment = surfaceModel.getFragment();
            tool.$popup = $popup;
var selection = fragment.getSelection();


            setTimeout(function () {
if ( selection.isCollapsed() ) {
                $(document).one('mousedown', function (e) {
surface.getView().focus();
                    if (!$(e.target).closest($popup).length) { $popup.remove(); tool.$popup = null; }
return;
                });
}
            }, 50);
        };


        ForegroundColorTool.prototype.applyColor = function (surface, color) {
fragment = fragment.trimLinearSelection();
            var model = surface.getModel();
fragment.annotateContent( 'clear', annotationName );
            var doc = model.getDocument();
fragment.annotateContent(
            var selection = model.getSelection();
'set',
            var range = selection.getRange();
new ve.dm.ForegroundColorAnnotation( {
type: annotationName,
attributes: { color: color }
} )
);
surface.getView().focus();
};


            if (range.isCollapsed()) {
ForegroundColorTool.prototype.onSelect = function () {
                surface.getView().focus();
var tool = this;
                return;
var surface = tool.toolbar.getSurface();
            }
var offset;


            // Use the fragment API to apply color to each range in the selection
if ( !surface ) {
            var fragment = model.getFragment(selection, 'editable');
return;
            var ranges = fragment.getRanges();
}
if ( tool.$popup ) {
tool.$popup.remove();
tool.$popup = null;
return;
}


            // Build a transaction that applies the color annotation to each range
tool.$popup = $( '<div>' )
            var txBuilder = new ve.dm.TransactionBuilder();
.addClass( 'techdocs-ve-color-palette' )
            var annotated = 0;
.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)'
} );


            ranges.forEach(function (r) {
palette.forEach( function ( color ) {
                if (r.isCollapsed()) return;
$( '<button>' )
                // Get the content branches within this range
.attr( {
                var branches = r.getCoveringBranches();
type: 'button',
                if (!branches || branches.length === 0) return;
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 );
} );


                branches.forEach(function (branchInfo) {
offset = tool.$element.offset();
                    var branch = branchInfo.branch;
tool.$popup.css( {
                    var start = branchInfo.start;
top: offset.top + tool.$element.outerHeight() + 4,
                    var end = branchInfo.end;
left: offset.left
                    if (branch === undefined || start === end) return;
} );
$( 'body' ).append( tool.$popup );


                    // Get the actual nodes at this position
setTimeout( function () {
                    var textStart = branch.getNodeAtOffset(start);
$( document ).one( 'mousedown', function ( event ) {
                    var textEnd = branch.getNodeAtOffset(end - 1);
if (
                    if (!textStart) return;
tool.$popup &&
!$( event.target ).closest( tool.$popup ).length
) {
tool.$popup.remove();
tool.$popup = null;
}
} );
}, 50 );
};


                    // Find the offset within the start text node
ForegroundColorTool.prototype.onUpdateState = function () {
                    var startOffsetInNode = start - branch.getOffset(textStart, true);
this.setDisabled( false );
                    var endOffsetInNode = end - branch.getOffset(textEnd, true);
};


                    // Create wrapper nodes
ve.ui.toolFactory.register( ForegroundColorTool );
                    var openSpan = ['span', { style: 'color: ' + color + ';' }, [], { 'type': '+style' }];
}
                    var closeSpan = ['span', { }, [], { 'type': 'closeStyle' }];


                    // Apply using internalList manipulation
function initialize() {
                    txBuilder.inserting(branch, start, [{type: 'span', attributes: {style: 'color: ' + color + ';'}}, {type: '/span'}]);
if (
                    annotated++;
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;
}


            if (annotated > 0) {
registerAnnotation();
                try {
registerTool();
                    txBuilder.commit();
}
                } catch (err) {
                    console.log('[ColorTool] TransactionBuilder failed, trying alternative:', err);
                    // Alternative: use fragment.annotateContent with a custom annotation
                    tool.applyColorV2(model, fragment, color);
                }
            } else {
                // Fallback: simpler approach
                tool.applyColorV2(model, fragment, color);
            }


            surface.getView().focus();
initialize();
        };
mw.loader.using( 'ext.veforall.target' ).then( initialize, function () {
 
if ( !retryTimer ) {
        ForegroundColorTool.prototype.applyColorV2 = function (model, fragment, color) {
retryTimer = setTimeout( initialize, 500 );
            // Simpler fallback: use the existing textStyle/span annotation
}
            // with `style` attribute in the data element's content
} );
            // We need to create a custom inline annotation.
}() );
            //
            // Approach: insert <span style="color:..."> markup directly into the
            // content branches using the TransactionBuilder's insertContent method.
            var doc = model.getDocument();
            var ranges = fragment.getRanges();
 
            ranges.forEach(function (r) {
                if (r.isCollapsed()) return;
                var branches = r.getCoveringBranches();
                if (!branches) return;
 
                branches.forEach(function (branchInfo) {
                    var branch = branchInfo.branch;
                    var start = branchInfo.start;
                    var end = branchInfo.end;
                    if (branch === undefined || start === end) return;
 
                    // Use TransactionBuilder to wrap with span
                    var tx = new ve.dm.TransactionBuilder();
                    var insertAt = start;
                    var deleteCount = end - start;
 
                    // Get the current content to preserve it
                    var content = branch.slice(start, end);
                    // Insert: open span + content + close span
                    var newContent = []
                        .concat({type: 'span', attributes: {style: 'color: ' + color + ';'}})
                        .concat(content)
                        .concat({type: '/span'});
 
                    try {
                        tx.remove(branch, insertAt, deleteCount);
                        tx.insert(branch, insertAt, newContent);
                        tx.commit();
                    } catch (err) {
                        console.log('[ColorTool] alt failed:', err);
                    }
                });
            });
        };
 
        ForegroundColorTool.prototype.onUpdateState = function () { this.setDisabled(false); };
 
        ve.ui.toolFactory.register(ForegroundColorTool);
        console.log('[VEForAll] Foreground color tool v4 registered');
    }
 
    initColorTool();
    if (typeof mw !== 'undefined' && mw.loader) {
        mw.loader.using(['ext.veforall.target']).done(initColorTool).fail(function(){ setTimeout(initColorTool, 500); });
    }
})();

Revision as of 11:21, 9 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 = OO.ui.deferMsg(
			'visualeditor-foreground-color-tool',
			'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 );
			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)'
				} );

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