MediaWiki:Common.js

来自尘月维基
Admin​(留言 | 贡献)2026年10月31日 (六) 17:56的版本 (排版:修复弹窗关闭回调)
跳转到导航 跳转到搜索

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
  • Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/*
 * 尘月维基 · 排版工具条
 * 在源代码编辑器(WikiEditor)上增加:颜色 / 字号 / 对齐 / 插图 / 调图
 * 依赖:ext.wikiEditor、oojs-ui-widgets、oojs-ui-windows
 */
( function () {
	'use strict';

	var COLORS = [
		{ label: '红', value: '#d32f2f' },
		{ label: '橙', value: '#ef6c00' },
		{ label: '黄', value: '#f9a825' },
		{ label: '绿', value: '#2e7d32' },
		{ label: '青', value: '#00838f' },
		{ label: '蓝', value: '#1565c0' },
		{ label: '紫', value: '#6b3fa0' },
		{ label: '粉', value: '#d81b60' },
		{ label: '灰', value: '#666666' },
		{ label: '黑', value: '#000000' }
	];
	var SIZES = [ '80%', '100%', '120%', '150%', '200%', '250%' ];
	var WIDTHS = [ '', '100', '150', '200', '250', '300', '400', '500', '600' ];
	var ALIGN_OPTIONS = [
		{ label: '默认', value: '' },
		{ label: '居中', value: 'center' },
		{ label: '靠左', value: 'left' },
		{ label: '靠右', value: 'right' }
	];
	var ALIGN_MAP = { '居中': 'center', '居左': 'left', '居右': 'right', '无': '' };

	var FILE_RE = /^\s*\[\[\s*(?:File|Image|文件|图像)\s*:\s*([^|\]]+)((?:\|[^[\]]*)*)\s*\]\]\s*$/i;
	var ALIGN_RE = /^(?:center|left|right|none|居中|居左|居右|无)$/i;

	var DialogClass = null;
	var windowManager = null;

	/**
	 * 生成"包裹选中文字"的动作,套用站内模板
	 *
	 * @param {string} template 模板名
	 * @param {string} arg 第一个参数(颜色 / 字号 / 对齐方式)
	 * @return {Object} WikiEditor action
	 */
	function wrapAction( template, arg ) {
		return {
			type: 'encapsulate',
			options: {
				pre: '{{' + template + '|1=' + arg + '|2=',
				peri: '',
				post: '}}'
			}
		};
	}

	/**
	 * 把 [{label, value}] 转成 WikiEditor select 需要的 list
	 *
	 * @param {Object[]} items
	 * @param {string} template
	 * @return {Object}
	 */
	function makeList( items, template ) {
		var list = {};
		items.forEach( function ( item, index ) {
			list[ 'opt' + index ] = {
				label: item.label,
				action: wrapAction( template, item.value )
			};
		} );
		return list;
	}

	/**
	 * 从 [[File:...]] 代码里解析文件名、宽度、位置、说明
	 *
	 * @param {string} text
	 * @return {Object|null}
	 */
	function parseFileLink( text ) {
		var match = FILE_RE.exec( text || '' );
		if ( !match ) {
			return null;
		}
		var parsed = { name: match[ 1 ].trim(), width: '', align: '', caption: '' };
		var params = match[ 2 ] ? match[ 2 ].split( '|' ).slice( 1 ) : [];
		params.forEach( function ( raw ) {
			var param = raw.trim();
			if ( !param ) {
				return;
			}
			if ( /^\d+\s*px$/i.test( param ) ) {
				parsed.width = String( parseInt( param, 10 ) );
			} else if ( ALIGN_RE.test( param ) ) {
				parsed.align = Object.prototype.hasOwnProperty.call( ALIGN_MAP, param ) ?
					ALIGN_MAP[ param ] : param.toLowerCase();
			} else if ( !parsed.caption ) {
				parsed.caption = param;
			}
		} );
		return parsed;
	}

	/**
	 * 拼出 [[File:名字|宽度px|位置|说明]]
	 *
	 * @param {Object} data
	 * @return {string}
	 */
	function buildFileLink( data ) {
		var parts = [ '[[File:' + data.name ];
		if ( data.width ) {
			parts.push( data.width + 'px' );
		}
		if ( data.align ) {
			parts.push( data.align );
		}
		if ( data.caption ) {
			parts.push( data.caption );
		}
		return parts.join( '|' ) + ']]';
	}

	/**
	 * 上传页地址
	 *
	 * @return {string}
	 */
	function uploadUrl() {
		if ( mw.util && typeof mw.util.getUrl === 'function' ) {
			return mw.util.getUrl( 'Special:Upload' );
		}
		return '/Special:Upload';
	}

	/**
	 * 提示条
	 *
	 * @param {string} text
	 */
	function notify( text ) {
		mw.loader.using( 'mediawiki.notification' ).then( function () {
			mw.notify( text, { type: 'warn', autoHideSeconds: 5 } );
		} );
	}

	/**
	 * 图片对话框(插入 / 调整共用一个)
	 *
	 * @return {Function} 对话框类
	 */
	function getDialogClass() {
		if ( DialogClass ) {
			return DialogClass;
		}

		/**
		 * @param {Object} config
		 */
		function ImageDialog( config ) {
			ImageDialog.super.call( this, config );
		}
		OO.inheritClass( ImageDialog, OO.ui.Dialog );
		ImageDialog.static.name = 'chenyue-image';
		ImageDialog.static.size = 'medium';
		ImageDialog.static.title = '图片';
		ImageDialog.static.actions = [
			{ action: 'apply', label: '确定', flags: [ 'primary', 'progressive' ] },
			{ action: 'cancel', label: '取消', flags: 'safe' }
		];

		/**
		 * @param {Object} context
		 * @param {Object} data
		 * @param {boolean} isNew
		 * @return {ImageDialog}
		 */
		ImageDialog.prototype.setData = function ( context, data, isNew ) {
			this.context = context;
			this.data = data;
			this.isNew = isNew;
			return this;
		};

		ImageDialog.prototype.initialize = function () {
			ImageDialog.super.prototype.initialize.apply( this, arguments );

			var data = this.data;
			this.title.setLabel( this.isNew ? '插入图片' : '调整图片' );

			this.nameInput = new OO.ui.TextInputWidget( {
				value: data.name,
				placeholder: '图片文件名,例如 熊猫.png',
				icon: 'image'
			} );
			this.widthInput = new OO.ui.DropdownInputWidget( {
				options: WIDTHS.map( function ( width ) {
					return { data: width, label: width ? width + ' 像素宽' : '按原始大小' };
				} ),
				value: data.width || ''
			} );
			this.alignInput = new OO.ui.DropdownInputWidget( {
				options: ALIGN_OPTIONS.map( function ( option ) {
					return { data: option.value, label: option.label };
				} ),
				value: data.align || ''
			} );
			this.captionInput = new OO.ui.TextInputWidget( {
				value: data.caption,
				placeholder: '图片说明,可留空'
			} );

			var fieldset = new OO.ui.FieldsetLayout();
			fieldset.addItems( [
				new OO.ui.FieldLayout( this.nameInput, { label: '文件名', align: 'top' } ),
				new OO.ui.FieldLayout( this.widthInput, { label: '大小', align: 'top' } ),
				new OO.ui.FieldLayout( this.alignInput, { label: '位置', align: 'top' } ),
				new OO.ui.FieldLayout( this.captionInput, { label: '说明', align: 'top' } )
			] );

			var hint = new OO.ui.HtmlSnippet(
				'<p class="chenyue-img-hint">还没上传?' +
				'<a href="' + uploadUrl() + '" target="_blank" rel="noopener">去上传图片 →</a></p>'
			);

			var panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
			panel.$element.append( fieldset.$element, hint.$element );
			this.$body.append( panel.$element );
		};

		ImageDialog.prototype.getBodyHeight = function () {
			return 380;
		};

		ImageDialog.prototype.getActionProcess = function ( action ) {
			var dialog = this;

			if ( action !== 'apply' ) {
				return new OO.ui.Process( function () {
					dialog.close( { action: action || 'cancel' } );
				} );
			}

			return new OO.ui.Process( function () {
				var name = dialog.nameInput.getValue().trim()
					.replace( /^(?:File|Image|文件|图像)\s*:\s*/i, '' );
				if ( !name ) {
					dialog.nameInput.setValidityFlag( false );
					dialog.nameInput.focus();
					return;
				}

				var text = buildFileLink( {
					name: name,
					width: dialog.widthInput.getValue(),
					align: dialog.alignInput.getValue(),
					caption: dialog.captionInput.getValue().trim()
				} );

				if ( dialog.isNew ) {
					dialog.context.$textarea.textSelection( 'insertText', text );
				} else {
					dialog.context.$textarea.textSelection( 'encapsulateSelection', {
						pre: '', peri: text, post: '', replace: true
					} );
				}

				dialog.close( { action: 'apply' } );
			} );
		};

		DialogClass = ImageDialog;
		return DialogClass;
	}

	/**
	 * 打开图片对话框
	 *
	 * @param {Object} context WikiEditor context
	 * @param {Object|null} parsed 已有图片信息(调整时传入)
	 */
	function openImageDialog( context, parsed ) {
		var isNew = !parsed;
		mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ] ).then( function () {
			if ( !windowManager ) {
				windowManager = new OO.ui.WindowManager();
				$( document.body ).append( windowManager.$element );
			}

			var ImageDialog = getDialogClass();
			var dialog = new ImageDialog();
			dialog.setData( context, parsed || { name: '', width: '', align: '', caption: '' }, isNew );

			/**
			 * 窗口关闭后销毁实例,避免堆积
			 */
			function cleanup() {
				windowManager.removeWindows( [ dialog ] );
			}

			windowManager.addWindows( [ dialog ] );

			var opened = windowManager.openWindow( dialog, {} );
			if ( opened && typeof opened.then === 'function' ) {
				opened.then( function () {
					if ( dialog.closed && typeof dialog.closed.then === 'function' ) {
						dialog.closed.then( cleanup, cleanup );
					}
				} );
			}
		} );
	}

	/**
	 * 调整选中图片的大小 / 位置
	 *
	 * @param {Object} context
	 */
	function adjustImage( context ) {
		var selection = context.$textarea.textSelection( 'getSelection' );
		var parsed = parseFileLink( selection );

		if ( !parsed ) {
			notify( '先选中整段图片代码再点「调图」,例如 [[File:熊猫.png|300px|center]]' );
			return;
		}

		openImageDialog( context, parsed );
	}

	/**
	 * 把排版工具条插进 WikiEditor
	 */
	function addLayoutTools() {
		if ( $( '#wikiEditor-ui-toolbar .group-layout' ).length ) {
			return;
		}

		$( '#wpTextbox1' ).wikiEditor( 'addToToolbar', {
			section: 'main',
			groups: {
				layout: {
					tools: {
						layoutColor: {
							type: 'select',
							label: '颜色',
							list: makeList( COLORS, '颜色' )
						},
						layoutSize: {
							type: 'select',
							label: '字号',
							list: makeList( SIZES.map( function ( size ) {
								return { label: size, value: size };
							} ), '字号' )
						},
						layoutAlign: {
							type: 'select',
							label: '对齐',
							list: makeList( ALIGN_OPTIONS.filter( function ( option ) {
								return option.value !== '';
							} ), '对齐' )
						},
						layoutImage: {
							type: 'button',
							label: '插图',
							action: {
								type: 'callback',
								execute: function ( context ) {
									openImageDialog( context, null );
								}
							}
						},
						layoutAdjust: {
							type: 'button',
							label: '调图',
							action: {
								type: 'callback',
								execute: function ( context ) {
									adjustImage( context );
								}
							}
						}
					}
				}
			}
		} );
	}

	/**
	 * 等 WikiEditor 渲染出工具条后再挂工具组
	 */
	function bootstrap() {
		if ( !document.getElementById( 'wpTextbox1' ) ) {
			return;
		}

		mw.loader.using( 'ext.wikiEditor' ).then( function () {
			var tries = 0;

			function waitForToolbar() {
				if ( document.getElementById( 'wikiEditor-ui-toolbar' ) ) {
					addLayoutTools();
					return;
				}
				tries += 1;
				if ( tries <= 30 ) {
					window.setTimeout( waitForToolbar, 100 );
				}
			}

			waitForToolbar();
		} );
	}

	$( bootstrap );
}() );