MediaWiki:Common.js:修订间差异

来自尘月维基
跳转到导航 跳转到搜索
Admin​(留言 | 贡献)
无编辑摘要
Admin​(留言 | 贡献)
颜色/字号改用 span 内联样式(修:模板导致可视化编辑器无法加链接)
 
(未显示同一用户的17个中间版本)
第1行: 第1行:
/* 尘月维基:窄屏整页等比缩放 + 左侧栏强制常显 */
/*
* 尘月维基 · 排版工具条
* 在源代码编辑器(WikiEditor)上增加:颜色 / 字号 / 对齐 / 插图 / 调图
* 依赖:ext.wikiEditor、oojs-ui-widgets、oojs-ui-windows
*/
( function () {
( function () {
var BASE = 1120;
'use strict';


/* 1) 窄屏整页等比缩放,保证手机与电脑布局完全一致 */
var COLORS = [
function fit() {
{ label: '红', value: '#d32f2f' },
var root = document.documentElement;
{ label: '橙', value: '#ef6c00' },
var w = ( window.innerWidth || root.clientWidth );
{ label: '黄', value: '#f9a825' },
if ( w > 0 && w < BASE + 40 ) {
{ label: '绿', value: '#2e7d32' },
root.style.zoom = ( w / ( BASE + 64 ) ).toFixed(4);
{ label: '青', value: '#00838f' },
} else {
{ label: '蓝', value: '#1565c0' },
root.style.zoom = '';
{ 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;
 
/**
* 生成"包裹选中文字"的动作,套用站内模板
*
* 只用于「对齐」这类块级排版;颜色和字号改用内联样式(见 spanAction)。
*
* @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: '}}'
}
};
}
 
/**
* 颜色 / 字号用内联样式包裹,而不是模板
*
* 模板在可视化编辑器里是不可分割的块,里面的文字没法再选中加链接;
* span + style 则仍然是普通文字,两个编辑器都能继续编辑。
*
* @param {string} prop CSS 属性(color / font-size)
* @param {string} value 值
* @return {Object} WikiEditor action
*/
function spanAction( prop, value ) {
return {
type: 'encapsulate',
options: {
pre: '<span style="' + prop + ':' + value + '">',
peri: '',
post: '</span>'
}
};
}
 
/**
* 把 [{label, value}] 转成 WikiEditor select 需要的 list
*
* @param {Object[]} items
* @param {Function} actionFactory 由 value 生成 action
* @return {Object}
*/
function makeList( items, actionFactory ) {
var list = {};
items.forEach( function ( item, index ) {
list[ 'opt' + index ] = {
label: item.label,
action: actionFactory( 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 } );
} );
}
}


/* 2) 左侧栏钉在正文左边:兜底手机浏览器 flex 降级时侧栏掉到页尾 */
/**
function pinSidebar() {
* 图片对话框(插入 / 调整共用一个)
var block = document.getElementById( 'mw-content-block' );
*
var nav = document.getElementById( 'mw-site-navigation' );
* @return {Function} 对话框类
var wrap = document.getElementById( 'mw-content-wrapper' );
*/
if ( !block || !nav || !wrap ) { return; }
function getDialogClass() {
if ( DialogClass ) {
return DialogClass;
}
 
/**
* @param {Object} config
*/
function ImageDialog( config ) {
ImageDialog.super.call( this, config );
}
OO.inheritClass( ImageDialog, OO.ui.ProcessDialog );
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;
};


var narrow = ( window.innerWidth || document.documentElement.clientWidth ) < 1200;
ImageDialog.prototype.initialize = function () {
ImageDialog.super.prototype.initialize.apply( this, arguments );


if ( !narrow ) {
var data = this.data;
[ block, nav, wrap ].forEach( function ( el ) {
this.title.setLabel( this.isNew ? '插入图片' : '调整图片' );
[ 'display', 'float', 'width', 'flex', 'order',
 
'position', 'visibility', 'max-width', 'min-width',
this.nameInput = new OO.ui.TextInputWidget( {
'align-items' ].forEach( function ( p ) {
value: data.name,
el.style.removeProperty( p );
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()
} );
} );
// textSelection 没有 insertText 方法,统一用 encapsulateSelection:
// 有选区就替换,没选区就插在光标处
dialog.context.$textarea.textSelection( 'encapsulateSelection', {
pre: text,
peri: '',
post: '',
replace: true,
splitlines: false,
ownline: false
} );
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 );
windowManager.addWindows( [ dialog ] );
// openWindow 返回的 promise 在窗口关闭时才 resolve,用它来销毁实例
var opened = windowManager.openWindow( dialog, {} );
if ( opened && typeof opened.then === 'function' ) {
opened.then( cleanup, cleanup );
} else if ( dialog.closed && typeof dialog.closed.then === 'function' ) {
dialog.closed.then( cleanup, cleanup );
}
/**
* 关闭后从 DOM 中移除,避免旧窗口残留
*/
function cleanup() {
windowManager.removeWindows( [ dialog ] );
if ( dialog.$element && dialog.$element.parent() ) {
dialog.$element.remove();
}
}
} );
}
/**
* 调整选中图片的大小 / 位置
*
* @param {Object} context
*/
function adjustImage( context ) {
var selection = context.$textarea.textSelection( 'getSelection' );
var parsed = parseFileLink( selection );
if ( !parsed ) {
notify( '先选中整段图片代码再点「调图」,例如 [[File:熊猫.png|300px|center]]' );
return;
return;
}
}


/* DOM 顺序兜底:侧栏若排在正文之后,挪到正文前面 */
openImageDialog( context, parsed );
if ( block.contains( nav ) && block.contains( wrap ) &&
}
( nav.compareDocumentPosition( wrap ) & Node.DOCUMENT_POSITION_PRECEDING ) ) {
 
block.insertBefore( nav, wrap );
/**
* 把排版工具条插进 WikiEditor
*/
function addLayoutTools() {
if ( $( '#wikiEditor-ui-toolbar .group-layout' ).length ) {
return;
}
}


block.style.setProperty( 'display', 'flex', 'important' );
$( '#wpTextbox1' ).wikiEditor( 'addToToolbar', {
block.style.setProperty( 'align-items', 'flex-start', 'important' );
section: 'main',
groups: {
layout: {
tools: {
layoutColor: {
type: 'select',
label: '颜色',
list: makeList( COLORS, function ( value ) {
return spanAction( 'color', value );
} )
},
layoutSize: {
type: 'select',
label: '字号',
list: makeList( SIZES.map( function ( size ) {
return { label: size, value: size };
} ), function ( value ) {
return spanAction( 'font-size', value );
} )
},
layoutAlign: {
type: 'select',
label: '对齐',
list: makeList( ALIGN_OPTIONS.filter( function ( option ) {
return option.value !== '';
} ), function ( value ) {
return wrapAction( '对齐', 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 );
}
}
}
}
}
}
} );
}


nav.style.setProperty( 'display', 'block', 'important' );
/**
nav.style.setProperty( 'float', 'none', 'important' );
* 等 WikiEditor 渲染出工具条后再挂工具组
nav.style.setProperty( 'position', 'static', 'important' );
*/
nav.style.setProperty( 'visibility', 'visible', 'important' );
function bootstrap() {
nav.style.setProperty( 'width', '186px', 'important' );
if ( !document.getElementById( 'wpTextbox1' ) ) {
nav.style.setProperty( 'flex', '0 0 186px', 'important' );
return;
nav.style.setProperty( 'order', '0', 'important' );
}
nav.style.setProperty( 'max-width', 'none', 'important' );


wrap.style.setProperty( 'order', '1', 'important' );
mw.loader.using( 'ext.wikiEditor' ).then( function () {
wrap.style.setProperty( 'flex', '1 1 auto', 'important' );
var tries = 0;
wrap.style.setProperty( 'min-width', '0', 'important' );
 
}
function waitForToolbar() {
if ( document.getElementById( 'wikiEditor-ui-toolbar' ) ) {
addLayoutTools();
return;
}
tries += 1;
if ( tries <= 30 ) {
window.setTimeout( waitForToolbar, 100 );
}
}


function run() {
waitForToolbar();
fit();
} );
pinSidebar();
}
}


if ( document.readyState === 'loading' ) {
$( bootstrap );
document.addEventListener( 'DOMContentLoaded', run );
} else {
run();
}
window.addEventListener( 'load', run );
window.addEventListener( 'resize', run );
window.addEventListener( 'orientationchange', run );
}() );
}() );

2026年10月31日 (六) 18:39的最新版本

/*
 * 尘月维基 · 排版工具条
 * 在源代码编辑器(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;

	/**
	 * 生成"包裹选中文字"的动作,套用站内模板
	 *
	 * 只用于「对齐」这类块级排版;颜色和字号改用内联样式(见 spanAction)。
	 *
	 * @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: '}}'
			}
		};
	}

	/**
	 * 颜色 / 字号用内联样式包裹,而不是模板
	 *
	 * 模板在可视化编辑器里是不可分割的块,里面的文字没法再选中加链接;
	 * span + style 则仍然是普通文字,两个编辑器都能继续编辑。
	 *
	 * @param {string} prop CSS 属性(color / font-size)
	 * @param {string} value 值
	 * @return {Object} WikiEditor action
	 */
	function spanAction( prop, value ) {
		return {
			type: 'encapsulate',
			options: {
				pre: '<span style="' + prop + ':' + value + '">',
				peri: '',
				post: '</span>'
			}
		};
	}

	/**
	 * 把 [{label, value}] 转成 WikiEditor select 需要的 list
	 *
	 * @param {Object[]} items
	 * @param {Function} actionFactory 由 value 生成 action
	 * @return {Object}
	 */
	function makeList( items, actionFactory ) {
		var list = {};
		items.forEach( function ( item, index ) {
			list[ 'opt' + index ] = {
				label: item.label,
				action: actionFactory( 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.ProcessDialog );
		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()
				} );

				// textSelection 没有 insertText 方法,统一用 encapsulateSelection:
				// 有选区就替换,没选区就插在光标处
				dialog.context.$textarea.textSelection( 'encapsulateSelection', {
					pre: text,
					peri: '',
					post: '',
					replace: true,
					splitlines: false,
					ownline: false
				} );

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

			windowManager.addWindows( [ dialog ] );

			// openWindow 返回的 promise 在窗口关闭时才 resolve,用它来销毁实例
			var opened = windowManager.openWindow( dialog, {} );
			if ( opened && typeof opened.then === 'function' ) {
				opened.then( cleanup, cleanup );
			} else if ( dialog.closed && typeof dialog.closed.then === 'function' ) {
				dialog.closed.then( cleanup, cleanup );
			}

			/**
			 * 关闭后从 DOM 中移除,避免旧窗口残留
			 */
			function cleanup() {
				windowManager.removeWindows( [ dialog ] );
				if ( dialog.$element && dialog.$element.parent() ) {
					dialog.$element.remove();
				}
			}
		} );
	}

	/**
	 * 调整选中图片的大小 / 位置
	 *
	 * @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, function ( value ) {
								return spanAction( 'color', value );
							} )
						},
						layoutSize: {
							type: 'select',
							label: '字号',
							list: makeList( SIZES.map( function ( size ) {
								return { label: size, value: size };
							} ), function ( value ) {
								return spanAction( 'font-size', value );
							} )
						},
						layoutAlign: {
							type: 'select',
							label: '对齐',
							list: makeList( ALIGN_OPTIONS.filter( function ( option ) {
								return option.value !== '';
							} ), function ( value ) {
								return wrapAction( '对齐', 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 );
}() );