Jump to content
Toggle menu
  • 6 articles
  • 6 users
  • 196 edits
Adventure Builder 2 Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
Fix insert dialog placeholder
Use WikiEditor dialogs for level player image
 
Line 579: Line 579:
return;
return;
}
}
mw.messages.set({
'ab-insert-level-title': 'Insert level',
'ab-insert-player-title': 'Insert player',
'ab-insert-image-title': 'Insert image'
});


var AB_INSERT_LEVEL_FIELDS = [
var AB_INSERT_LEVEL_FIELDS = [
Line 622: Line 628:
function abInsertIsUrl(value) {
function abInsertIsUrl(value) {
return /^https?:\/\//i.test(String(value || '').trim());
return /^https?:\/\//i.test(String(value || '').trim());
}
function abSetInputPlaceholder(widget, text) {
if (!widget) {
return;
}
if (typeof widget.setPlaceholder === 'function') {
widget.setPlaceholder(text);
return;
}
if (widget.$input) {
widget.$input.attr('placeholder', text);
}
}
}


Line 684: Line 677:
}
}


var abInsertManager = null;
function abEscapeAttr(value) {
var AbWikiInsertDialog = null;
return mw.html.escape(String(value || ''));
}


function abEnsureInsertDialog() {
function abOptionsHtml(options) {
if (AbWikiInsertDialog) {
var html = '';
return;
var i;
for (i = 0; i < options.length; i++) {
html +=
'<option value="' +
abEscapeAttr(options[i].data) +
'">' +
abEscapeAttr(options[i].label) +
'</option>';
}
}
return html;
}


AbWikiInsertDialog = function (config) {
function abFieldHtml(id, label, control) {
AbWikiInsertDialog.super.call(this, config);
return (
this.abKind = config.abKind;
'<div class="wikieditor-toolbar-field-wrapper">' +
this.abTextarea = config.abTextarea;
'<label for="' +
};
abEscapeAttr(id) +
OO.inheritClass(AbWikiInsertDialog, OO.ui.ProcessDialog);
'">' +
AbWikiInsertDialog.static.name = 'abWikiInsert';
abEscapeAttr(label) +
AbWikiInsertDialog.static.title = 'Insert';
'</label>' +
AbWikiInsertDialog.static.actions = [
control +
{ action: 'insert', label: 'Insert', flags: ['primary', 'progressive'] },
'</div>'
{ action: 'cancel', label: 'Cancel', flags: 'safe' }
);
];
}


AbWikiInsertDialog.prototype.getSetupProcess = function (data) {
function abInputHtml(id, placeholder) {
data = data || {};
return (
this.abKind = data.abKind || this.abKind;
'<input type="text" id="' +
this.abTextarea = data.abTextarea || this.abTextarea;
abEscapeAttr(id) +
var dialog = this;
'"' +
return AbWikiInsertDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
(placeholder ? ' placeholder="' + abEscapeAttr(placeholder) + '"' : '') +
var titles = {
'/>'
level: 'Insert level',
);
player: 'Insert player',
}
image: 'Insert image'
};
if (dialog.title && dialog.title.setLabel) {
dialog.title.setLabel(titles[dialog.abKind] || 'Insert');
}
dialog.setSize(dialog.abKind === 'image' ? 'large' : 'medium');
dialog.abResetFromSelection();
dialog.abUpdatePreview();
dialog.updateSize();
});
};


AbWikiInsertDialog.prototype.initialize = function () {
function abSelectHtml(id, options) {
AbWikiInsertDialog.super.prototype.initialize.call(this);
return (
this.abSource = new OO.ui.TextInputWidget({
'<select id="' + abEscapeAttr(id) + '">' + abOptionsHtml(options) + '</select>'
placeholder: '123',
);
autocomplete: false
}
});
this.abField = new OO.ui.DropdownInputWidget({
options: AB_INSERT_LEVEL_FIELDS
});
this.abWidth = new OO.ui.TextInputWidget({
placeholder: '300'
});
this.abHeight = new OO.ui.TextInputWidget({
placeholder: 'optional'
});
this.abAlign = new OO.ui.DropdownInputWidget({
options: AB_INSERT_ALIGN
});
this.abCaption = new OO.ui.TextInputWidget();
this.abAlt = new OO.ui.TextInputWidget();
this.abLink = new OO.ui.TextInputWidget();
this.abPreview = new OO.ui.TextInputWidget({
readOnly: true
});


this.abSourceLayout = new OO.ui.FieldLayout(this.abSource, {
function abSetInsertEnabled($dialog, enabled) {
label: 'ID',
var $button = $dialog.closest('.ui-dialog').find('.ui-dialog-buttonpane button').first();
align: 'top'
if ($button.length && $button.button) {
});
$button.button('option', 'disabled', !enabled);
this.abFieldLayout = new OO.ui.FieldLayout(this.abField, {
}
label: 'Insert as',
}
align: 'top'
});
this.abSizeLayout = new OO.ui.HorizontalLayout({
items: [
new OO.ui.FieldLayout(this.abWidth, { label: 'Width (px)', align: 'top' }),
new OO.ui.FieldLayout(this.abHeight, { label: 'Height (px)', align: 'top' })
]
});
this.abAlignLayout = new OO.ui.FieldLayout(this.abAlign, {
label: 'Align',
align: 'top'
});
this.abCaptionLayout = new OO.ui.FieldLayout(this.abCaption, {
label: 'Caption',
align: 'top'
});
this.abAltLayout = new OO.ui.FieldLayout(this.abAlt, {
label: 'Alt text',
align: 'top'
});
this.abLinkLayout = new OO.ui.FieldLayout(this.abLink, {
label: 'Link',
align: 'top'
});
this.abPreviewLayout = new OO.ui.FieldLayout(this.abPreview, {
label: 'Wikitext',
align: 'top'
});


this.abForm = new OO.ui.PanelLayout({
function abBindDialogEnter($dialog) {
padded: true,
if ($dialog.data('dialogkeypressset')) {
expanded: false
return;
});
}
this.abForm.$element.append(
$dialog.data('dialogkeypressset', true);
this.abSourceLayout.$element,
var $ui = $dialog.closest('.ui-dialog');
this.abFieldLayout.$element,
$ui.on('keypress', function (e) {
this.abSizeLayout.$element,
if (e.which === 13) {
this.abAlignLayout.$element,
var dialogAction = $ui.data('dialogaction');
this.abCaptionLayout.$element,
var $button = dialogAction ? $(dialogAction) : $ui.find('button').first();
this.abAltLayout.$element,
if ($button && $button.trigger) {
this.abLinkLayout.$element,
$button.trigger('click');
this.abPreviewLayout.$element
}
);
e.preventDefault();
this.$body.append(this.abForm.$element);
this.$element.addClass('ab-wiki-insert-dialog');
 
var dialog = this;
function onChange() {
dialog.abUpdatePreview();
}
}
this.abSource.connect(this, { change: onChange, enter: 'abTryInsert' });
});
this.abField.connect(this, { change: onChange });
$ui.find('button').on('focus', function (e) {
this.abWidth.connect(this, { change: onChange, enter: 'abTryInsert' });
$ui.data('dialogaction', e.delegateTarget);
this.abHeight.connect(this, { change: onChange, enter: 'abTryInsert' });
});
this.abAlign.connect(this, { change: onChange });
}
this.abCaption.connect(this, { change: onChange, enter: 'abTryInsert' });
this.abAlt.connect(this, { change: onChange, enter: 'abTryInsert' });
this.abLink.connect(this, { change: onChange, enter: 'abTryInsert' });
};


AbWikiInsertDialog.prototype.abTryInsert = function () {
function abInsertIntoTextarea($dialog, wikitext) {
if (this.abCurrentWikitext()) {
if (!wikitext) {
this.executeAction('insert');
return;
}
}
};
var context = $dialog.data('context');
$dialog.dialog('close');
if (!context || !context.$textarea) {
return;
}
context.$textarea.textSelection('encapsulateSelection', {
pre: '',
peri: wikitext,
post: '',
replace: true,
selectPeri: false
});
}


AbWikiInsertDialog.prototype.getReadyProcess = function (data) {
function abMakeTemplateDialog(kind) {
var dialog = this;
var prefix = 'ab-insert-' + kind;
return AbWikiInsertDialog.super.prototype.getReadyProcess.call(this, data).next(function () {
var idField = prefix + '-id';
dialog.abSource.focus();
var fieldField = prefix + '-field';
});
var templateName = kind === 'player' ? 'player' : 'level';
};
var fields = kind === 'player' ? AB_INSERT_PLAYER_FIELDS : AB_INSERT_LEVEL_FIELDS;
var idLabel = kind === 'player' ? 'Player ID' : 'Level ID';


AbWikiInsertDialog.prototype.abResetFromSelection = function () {
function currentWikitext($root) {
var kind = this.abKind;
return abInsertTemplate(
var selected = '';
templateName,
if (this.abTextarea && this.abTextarea.textSelection) {
$root.find('#' + idField).val(),
selected = String(this.abTextarea.textSelection('getSelection') || '').trim();
$root.find('#' + fieldField).val()
}
);
}


this.abSource.setValue('');
return {
this.abField.setValue('');
title: mw.message(prefix + '-title'),
this.abWidth.setValue('');
id: prefix + '-dialog',
this.abHeight.setValue('');
html:
this.abAlign.setValue('');
'<div class="wikieditor-toolbar-dialog-wrapper"><fieldset>' +
this.abCaption.setValue('');
abFieldHtml(idField, idLabel, abInputHtml(idField, '123')) +
this.abAlt.setValue('');
abFieldHtml(fieldField, 'Insert as', abSelectHtml(fieldField, fields)) +
this.abLink.setValue('');
'</fieldset></div>',
 
init: function () {
if (kind === 'image') {
var $dialog = $(this);
abSetInputPlaceholder(this.abSource, '123 or https://');
$dialog.find('#' + idField + ', #' + fieldField).on('input change', function () {
this.abSourceLayout.setLabel('Asset ID or URL');
abSetInsertEnabled($dialog, !!currentWikitext($dialog));
this.abField.setOptions(AB_INSERT_LEVEL_FIELDS);
});
this.abFieldLayout.toggle(false);
},
this.abSizeLayout.toggle(true);
dialog: {
this.abAlignLayout.toggle(true);
resizable: false,
this.abCaptionLayout.toggle(true);
dialogClass: 'wikiEditor-toolbar-dialog ab-insert-dialog',
this.abAltLayout.toggle(true);
width: 500,
this.abLinkLayout.toggle(true);
buttons: {
if (selected) {
'wikieditor-toolbar-tool-file-insert': function () {
this.abSource.setValue(selected);
abInsertIntoTextarea($(this), currentWikitext($(this)));
}
},
} else {
'wikieditor-toolbar-tool-file-cancel': function () {
abSetInputPlaceholder(this.abSource, '123');
$(this).dialog('close');
this.abSourceLayout.setLabel(kind === 'player' ? 'Player ID' : 'Level ID');
}
this.abField.setOptions(
},
kind === 'player' ? AB_INSERT_PLAYER_FIELDS : AB_INSERT_LEVEL_FIELDS
open: function () {
);
var $dialog = $(this);
this.abFieldLayout.toggle(true);
var context = $dialog.data('context');
this.abSizeLayout.toggle(false);
var selected = '';
this.abAlignLayout.toggle(false);
if (context && context.$textarea) {
this.abCaptionLayout.toggle(false);
selected = String(context.$textarea.textSelection('getSelection') || '').trim();
this.abAltLayout.toggle(false);
}
this.abLinkLayout.toggle(false);
$dialog.find('#' + idField).val(abInsertDigits(selected) || '');
if (abInsertDigits(selected)) {
$dialog.find('#' + fieldField).val('');
this.abSource.setValue(abInsertDigits(selected));
abSetInsertEnabled($dialog, !!currentWikitext($dialog));
$dialog.find('#' + idField).trigger('focus');
abBindDialogEnter($dialog);
}
}
}
}
};
};
}
function abMakeImageDialog() {
function currentWikitext($root) {
return abInsertImageWikitext({
source: $root.find('#ab-insert-image-source').val(),
width: $root.find('#ab-insert-image-width').val(),
height: $root.find('#ab-insert-image-height').val(),
align: $root.find('#ab-insert-image-align').val(),
caption: $root.find('#ab-insert-image-caption').val(),
alt: $root.find('#ab-insert-image-alt').val(),
link: $root.find('#ab-insert-image-link').val()
});
}


AbWikiInsertDialog.prototype.abCurrentWikitext = function () {
return {
if (this.abKind === 'image') {
title: mw.message('ab-insert-image-title'),
return abInsertImageWikitext({
id: 'ab-insert-image-dialog',
source: this.abSource.getValue(),
html:
width: this.abWidth.getValue(),
'<div class="wikieditor-toolbar-dialog-wrapper"><fieldset>' +
height: this.abHeight.getValue(),
abFieldHtml(
align: this.abAlign.getValue(),
'ab-insert-image-source',
caption: this.abCaption.getValue(),
'Asset ID or URL',
alt: this.abAlt.getValue(),
abInputHtml('ab-insert-image-source', '123 or https://')
link: this.abLink.getValue()
) +
'<div class="wikieditor-toolbar-field-wrapper ab-insert-size-row">' +
'<div class="wikieditor-toolbar-floated-field-wrapper">' +
'<label for="ab-insert-image-width">Width (px)</label>' +
abInputHtml('ab-insert-image-width', '300') +
'</div>' +
'<div class="wikieditor-toolbar-floated-field-wrapper">' +
'<label for="ab-insert-image-height">Height (px)</label>' +
abInputHtml('ab-insert-image-height', 'optional') +
'</div>' +
'</div>' +
abFieldHtml(
'ab-insert-image-align',
'Align',
abSelectHtml('ab-insert-image-align', AB_INSERT_ALIGN)
) +
abFieldHtml(
'ab-insert-image-caption',
'Caption',
abInputHtml('ab-insert-image-caption')
) +
abFieldHtml('ab-insert-image-alt', 'Alt text', abInputHtml('ab-insert-image-alt')) +
abFieldHtml('ab-insert-image-link', 'Link', abInputHtml('ab-insert-image-link')) +
'</fieldset></div>',
init: function () {
var $dialog = $(this);
$dialog.find('input, select').on('input change', function () {
abSetInsertEnabled($dialog, !!currentWikitext($dialog));
});
});
}
},
return abInsertTemplate(
dialog: {
this.abKind === 'player' ? 'player' : 'level',
resizable: false,
this.abSource.getValue(),
dialogClass: 'wikiEditor-toolbar-dialog ab-insert-dialog',
this.abField.getValue()
width: 590,
);
buttons: {
};
'wikieditor-toolbar-tool-file-insert': function () {
 
abInsertIntoTextarea($(this), currentWikitext($(this)));
AbWikiInsertDialog.prototype.abUpdatePreview = function () {
},
var wikitext = this.abCurrentWikitext();
'wikieditor-toolbar-tool-file-cancel': function () {
this.abPreview.setValue(wikitext);
$(this).dialog('close');
this.actions.setAbilities({ insert: !!wikitext });
}
};
},
 
open: function () {
AbWikiInsertDialog.prototype.getBodyHeight = function () {
var $dialog = $(this);
return Math.max(this.$body[0].scrollHeight, this.abKind === 'image' ? 380 : 200);
var context = $dialog.data('context');
};
var selected = '';
 
if (context && context.$textarea) {
AbWikiInsertDialog.prototype.getActionProcess = function (action) {
selected = String(context.$textarea.textSelection('getSelection') || '').trim();
var dialog = this;
}
if (action === 'insert') {
$dialog.find('input').val('');
return new OO.ui.Process(function () {
$dialog.find('#ab-insert-image-align').val('');
var wikitext = dialog.abCurrentWikitext();
if (selected) {
if (wikitext && dialog.abTextarea) {
$dialog.find('#ab-insert-image-source').val(selected);
dialog.abTextarea.textSelection('encapsulateSelection', {
pre: '',
peri: wikitext,
post: '',
replace: true,
selectPeri: false
});
}
}
dialog.close({ action: action });
abSetInsertEnabled($dialog, !!currentWikitext($dialog));
});
$dialog.find('#ab-insert-image-source').trigger('focus');
abBindDialogEnter($dialog);
}
}
}
if (action === 'cancel') {
return new OO.ui.Process(function () {
dialog.close({ action: action });
});
}
return AbWikiInsertDialog.super.prototype.getActionProcess.call(this, action);
};
};
}
}


function abOpenInsertDialog($textarea, kind) {
function abRegisterInsertDialogs($textarea) {
mw.loader.using(['oojs-ui-windows', 'oojs-ui-widgets']).then(function () {
var context = $textarea.data('wikiEditor-context');
abEnsureInsertDialog();
if (!context || !$.wikiEditor || !$.wikiEditor.modules.dialogs) {
if (!abInsertManager) {
return;
abInsertManager = new OO.ui.WindowManager();
}
$(document.body).append(abInsertManager.$element);
$.wikiEditor.modules.dialogs.fn.create(context, {
abInsertManager.addWindows([new AbWikiInsertDialog({ size: 'medium' })]);
'ab-insert-level': abMakeTemplateDialog('level'),
}
'ab-insert-player': abMakeTemplateDialog('player'),
abInsertManager.openWindow('abWikiInsert', {
'ab-insert-image': abMakeImageDialog()
abKind: kind,
abTextarea: $textarea
});
});
});
}
}
Line 951: Line 929:
return;
return;
}
}
abRegisterInsertDialogs($textarea);
$textarea.wikiEditor('removeFromToolbar', {
$textarea.wikiEditor('removeFromToolbar', {
section: 'main',
section: 'main',
Line 966: Line 945:
oouiIcon: 'play',
oouiIcon: 'play',
action: {
action: {
type: 'callback',
type: 'dialog',
execute: function () {
module: 'ab-insert-level'
abOpenInsertDialog($textarea, 'level');
}
}
}
},
},
Line 977: Line 954:
oouiIcon: 'userAvatar',
oouiIcon: 'userAvatar',
action: {
action: {
type: 'callback',
type: 'dialog',
execute: function () {
module: 'ab-insert-player'
abOpenInsertDialog($textarea, 'player');
}
}
}
},
},
Line 988: Line 963:
oouiIcon: 'image',
oouiIcon: 'image',
action: {
action: {
type: 'callback',
type: 'dialog',
execute: function () {
module: 'ab-insert-image'
abOpenInsertDialog($textarea, 'image');
}
}
}
}
}

Latest revision as of 21:08, 29 August 2026

$(function () {
	if (!mw.config.get('wgIsMainPage')) {
		return;
	}
	function hideMainPageComments() {
		$('#ext-comments-container, .ext-comments-comments-list, .comment-list-toolbar').hide();
	}
	hideMainPageComments();
	$('.home-card-action a').attr({
		target: '_blank',
		rel: 'noopener noreferrer'
	});
	if (mw.hook) {
		mw.hook('wikipage.content').add(hideMainPageComments);
	}
	setTimeout(hideMainPageComments, 400);
});

var AB_DIFFICULTY = { 1: 'Easy', 2: 'Normal', 3: 'Hard', 4: 'INSANE' };

function abSetDifficulty($els, difficulty) {
	if (!$els || !$els.length) {
		return;
	}
	$els.removeClass('ab-difficulty-1 ab-difficulty-2 ab-difficulty-3 ab-difficulty-4');
	var label = AB_DIFFICULTY[difficulty];
	if (!label) {
		$els.text('-').attr('data-difficulty', '0');
		return;
	}
	$els
		.addClass('ab-difficulty ab-difficulty-' + difficulty)
		.attr('data-difficulty', String(difficulty))
		.text(label);
}

function abCompactCount(n) {
	var num = Number(n) || 0;
	if (num >= 1e9) {
		return (num / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
	}
	if (num >= 1e6) {
		return (num / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
	}
	if (num >= 1e3) {
		return (num / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
	}
	return String(Math.floor(num));
}

var AB_ROLE_NAMES = {
	owner: 'Owner',
	moderator: 'Moderator',
	admin: 'Admin',
	scout: 'Scout'
};

function abFormatRole(role) {
	if (role == null) {
		return '';
	}
	role = String(role).trim();
	if (!role) {
		return '';
	}
	var lower = role.toLowerCase();
	if (lower === 'user') {
		return '';
	}
	if (AB_ROLE_NAMES[lower]) {
		return AB_ROLE_NAMES[lower];
	}
	return lower.replace(/[_-]+/g, ' ').replace(/\b\w/g, function (ch) {
		return ch.toUpperCase();
	});
}

function abSetRole($els, role) {
	if (!$els || !$els.length) {
		return;
	}
	$els.removeClass('ab-role-owner ab-role-moderator ab-role-scout');
	var slug = String(role || '')
		.trim()
		.toLowerCase();
	var label = abFormatRole(role);
	if (!label) {
		$els.text('').removeAttr('data-role').removeClass('ab-role');
		return;
	}
	$els.addClass('ab-role');
	if (slug === 'owner' || slug === 'moderator' || slug === 'scout') {
		$els.addClass('ab-role-' + slug);
	}
	$els.attr('data-role', slug).text(label);
}

$(function () {
	var levelId = mw.config.get('abLevelId');
	var $box = $('.ab-level-infobox');
	if (!levelId || !$box.length) {
		return;
	}

	var defaultThumbs = ['82374078680299', '105249369413891', '104825146904235'];

	function thumbAssetId(level) {
		if (!level || level.locked) {
			return defaultThumbs[0];
		}
		var raw = String(level.thumbnail || '').replace(/^rbxassetid:\/\//i, '').replace(/[^0-9]/g, '');
		if (raw && raw !== '0') {
			return raw;
		}
		return defaultThumbs[Math.abs(Number(level.id) || Number(levelId) || 0) % defaultThumbs.length];
	}

	function setThumb(assetId) {
		var $thumb = $box.find('.ab-level-thumb');
		if (!$thumb.length || !assetId) {
			return;
		}
		fetch(
			'https://adventure-builder.net/api/roblox-proxy?service=thumbnails&assetIds=' +
				encodeURIComponent(assetId) +
				'&size=' +
				encodeURIComponent('420x420')
		)
			.then(function (res) {
				return res.ok ? res.json() : null;
			})
			.then(function (data) {
				var item = data && data.data && data.data[0];
				if (!item || item.state !== 'Completed' || !item.imageUrl) {
					return;
				}
				$thumb.attr('src', item.imageUrl).removeClass('ab-level-thumb--placeholder');
			})
			.catch(function () {});
	}

	function atUsername(name) {
		name = String(name || '')
			.replace(/^@+/, '')
			.trim();
		return name ? '@' + name : '';
	}

	function setCreator(level) {
		var $el = $box.find('.ab-level-creator');
		if (!$el.length || !level) {
			return;
		}
		var creatorId = String(level.creator || '').replace(/[^0-9]/g, '');

		function apply(name) {
			var label = atUsername(name);
			if (!label) {
				return;
			}
			var $link = $el.find('a');
			if ($link.length) {
				$link.text(label);
				return;
			}
			if (!creatorId) {
				$el.text(label);
				return;
			}
			$('<a>')
				.attr('href', 'https://adventure-builder.net/players/' + encodeURIComponent(creatorId))
				.attr('target', '_blank')
				.attr('rel', 'noopener noreferrer')
				.text(label)
				.appendTo($el.empty());
		}

		var seeded = level.creator_name || level.creatorName;
		if (seeded) {
			apply(seeded);
			return;
		}
		if (!creatorId) {
			return;
		}
		fetch(
			'https://adventure-builder.net/api/roblox-proxy?service=username&userId=' +
				encodeURIComponent(creatorId)
		)
			.then(function (res) {
				return res.ok ? res.json() : null;
			})
			.then(function (data) {
				apply(data && (data.name || data.displayName));
			})
			.catch(function () {});
	}

	fetch('https://adventure-builder.net/api/v1/levels/' + encodeURIComponent(levelId))
		.then(function (res) {
			return res.ok ? res.json() : null;
		})
		.then(function (data) {
			var level = data && (data.level || data);
			if (!level || level.locked || !level.id) {
				setThumb(defaultThumbs[0]);
				return;
			}
			if (level.title) {
				$box.find('.pi-title').text(level.title);
			}
			if (level.id) {
				$box.find('.ab-level-stat[data-stat="id"]').text(String(level.id));
			}
			setCreator(level);
			[
				['plays', level.plays],
				['clears', level.clears],
				['favorites', level.favorites]
			].forEach(function (pair) {
				$box.find('.ab-level-stat[data-stat="' + pair[0] + '"]').text(
					abCompactCount(pair[1])
				);
			});
			$box.find('.ab-level-stat[data-stat="featured"]').text(
				level.featured ? 'Yes' : 'No'
			);
			abSetDifficulty($box.find('.ab-level-stat[data-stat="difficulty"]'), level.difficulty);
			setThumb(thumbAssetId(level));
		})
		.catch(function () {
			if ($box.find('.ab-level-thumb--placeholder').length) {
				setThumb(defaultThumbs[Math.abs(Number(levelId) || 0) % defaultThumbs.length]);
			}
		});
});

$(function () {
	var $bits = $('.ab-inline-level');
	if (!$bits.length) {
		return;
	}

	function atUsername(name) {
		name = String(name || '')
			.replace(/^@+/, '')
			.trim();
		return name ? '@' + name : '';
	}

	function applyLevel(id, level) {
		var $els = $('.ab-inline-level[data-level-id="' + id + '"]');
		if (!level || level.locked || !level.id) {
			return;
		}
		$els.filter('[data-stat="plays"]').text(abCompactCount(level.plays));
		$els.filter('[data-stat="clears"]').text(abCompactCount(level.clears));
		$els.filter('[data-stat="favorites"]').text(abCompactCount(level.favorites));
		if (level.title) {
			$els.filter('[data-stat="title"]').text(level.title);
		}
		if (Object.prototype.hasOwnProperty.call(level, 'description')) {
			var desc = String(level.description || '')
				.replace(/\s+/g, ' ')
				.trim();
			$els.filter('[data-stat="description"]').text(desc || '-');
		}
		$els.filter('[data-stat="featured"]').text(level.featured ? 'Yes' : 'No');
		abSetDifficulty($els.filter('[data-stat="difficulty"]'), level.difficulty);
		var $creator = $els.filter('[data-stat="creator"]');
		if ($creator.length) {
			var creatorId = String(level.creator || $creator.attr('data-creator-id') || '').replace(
				/[^0-9]/g,
				''
			);
			function setCreatorName(name) {
				var label = atUsername(name);
				if (!label) {
					return;
				}
				$creator.text(label);
				if (creatorId) {
					$creator.attr(
						'href',
						'https://adventure-builder.net/players/' + encodeURIComponent(creatorId)
					);
				}
			}
			if (level.creator_name || level.creatorName) {
				setCreatorName(level.creator_name || level.creatorName);
				return;
			}
			if (!creatorId) {
				return;
			}
			fetch(
				'https://adventure-builder.net/api/roblox-proxy?service=username&userId=' +
					encodeURIComponent(creatorId)
			)
				.then(function (res) {
					return res.ok ? res.json() : null;
				})
				.then(function (data) {
					setCreatorName(data && (data.name || data.displayName));
				})
				.catch(function () {});
		}
	}

	var ids = [];
	$bits.each(function () {
		var id = String($(this).attr('data-level-id') || '').replace(/[^0-9]/g, '');
		if (id && ids.indexOf(id) === -1) {
			ids.push(id);
		}
	});
	ids.forEach(function (id) {
		fetch('https://adventure-builder.net/api/v1/levels/' + encodeURIComponent(id))
			.then(function (res) {
				return res.ok ? res.json() : null;
			})
			.then(function (data) {
				applyLevel(id, data && (data.level || data));
			})
			.catch(function () {});
	});
});

$(function () {
	var $bits = $('.ab-inline-player');
	if (!$bits.length) {
		return;
	}

	function atUsername(name) {
		name = String(name || '')
			.replace(/^@+/, '')
			.trim();
		return name ? '@' + name : '';
	}

	function applyPlayer(id, profile, name) {
		var $els = $('.ab-inline-player[data-player-id="' + id + '"]');
		if (name) {
			$els.filter('[data-stat="name"]').text(atUsername(name));
		}
		if (!profile) {
			return;
		}
		$els.filter('[data-stat="levels"]').text(abCompactCount(profile.levels_count));
		$els.filter('[data-stat="clears"]').text(abCompactCount(profile.clears));
		$els.filter('[data-stat="followers"]').text(abCompactCount(profile.followers));
		abSetRole($els.filter('[data-stat="role"]'), profile.role);
	}

	var ids = [];
	$bits.each(function () {
		var id = String($(this).attr('data-player-id') || '').replace(/[^0-9]/g, '');
		if (id && ids.indexOf(id) === -1) {
			ids.push(id);
		}
	});
	ids.forEach(function (id) {
		fetch(
			'https://adventure-builder.net/api/roblox-proxy?service=username&userId=' +
				encodeURIComponent(id)
		)
			.then(function (res) {
				return res.ok ? res.json() : null;
			})
			.then(function (data) {
				var name = data && (data.name || data.displayName);
				if (name) {
					applyPlayer(id, null, name);
				}
			})
			.catch(function () {});

		fetch('https://adventure-builder.net/api/v1/profiles/' + encodeURIComponent(id))
			.then(function (res) {
				return res.ok ? res.json() : null;
			})
			.then(function (data) {
				applyPlayer(id, data && (data.profile || data), null);
			})
			.catch(function () {});
	});
});

$(function () {
	var userId = mw.config.get('abRobloxUserId');
	if (!userId) {
		return;
	}

	var $img = $('.profile-avatar-image');
	if ($img.length) {
		var fallback = $img.attr('src');
		$img.on('error', function () {
			$img.attr('src', fallback);
		});
		$img.attr(
			'src',
			'https://adventure-builder.net/api/roblox-proxy?service=profile-picture&userId=' +
				encodeURIComponent(userId)
		);
	}

	var $stats = $('.profile-header-statistics');
	if (!$stats.length) {
		return;
	}

	fetch(
		'https://adventure-builder.net/api/v1/profiles/' + encodeURIComponent(userId)
	)
		.then(function (res) {
			return res.ok ? res.json() : null;
		})
		.then(function (profile) {
			if (!profile) {
				return;
			}
			[
				[profile.levels_count, 'Levels'],
				[profile.clears, 'Clears'],
				[profile.followers, 'Followers']
			].forEach(function (pair) {
				$('<li>')
					.addClass('ab-central-stat')
					.text(abCompactCount(pair[0]) + ' ' + pair[1])
					.appendTo($stats);
			});
		})
		.catch(function () {});
});

$(function () {
	var playerId = mw.config.get('abPlayerId');
	var $box = $('.ab-player-infobox');
	if (!playerId || !$box.length) {
		return;
	}

	function atUsername(name) {
		name = String(name || '')
			.replace(/^@+/, '')
			.trim();
		return name ? '@' + name : '';
	}

	function setPfp() {
		var $thumb = $box.find('.ab-player-thumb');
		if (!$thumb.length) {
			return;
		}
		$thumb
			.on('error', function () {
				$thumb.css('visibility', 'hidden');
			})
			.attr(
				'src',
				'https://adventure-builder.net/api/roblox-proxy?service=profile-picture&userId=' +
					encodeURIComponent(playerId)
			)
			.removeClass('ab-player-thumb--placeholder');
	}

	fetch(
		'https://adventure-builder.net/api/roblox-proxy?service=username&userId=' +
			encodeURIComponent(playerId)
	)
		.then(function (res) {
			return res.ok ? res.json() : null;
		})
		.then(function (data) {
			var label = atUsername(data && (data.name || data.displayName));
			if (label) {
				$box.find('.ab-player-title').text(label);
				$box.find('.ab-player-thumb').attr('alt', label);
			}
		})
		.catch(function () {});

	fetch('https://adventure-builder.net/api/v1/profiles/' + encodeURIComponent(playerId))
		.then(function (res) {
			return res.ok ? res.json() : null;
		})
		.then(function (data) {
			var profile = data && (data.profile || data);
			if (!profile) {
				return;
			}
			[
				['levels', profile.levels_count],
				['clears', profile.clears],
				['followers', profile.followers]
			].forEach(function (pair) {
				$box.find('.ab-level-stat[data-stat="' + pair[0] + '"]').text(
					abCompactCount(pair[1])
				);
			});
			setRole(profile.role);
		})
		.catch(function () {});

	function setRole(role) {
		var label = abFormatRole(role);
		var $stat = $box.find('.ab-level-stat[data-stat="role"]');
		var $row = $stat.closest('.pi-data');
		if (!label) {
			$row.remove();
			return;
		}
		if (!$row.length) {
			$row = $(
				'<div class="pi-item pi-data pi-item-spacing pi-border-color">' +
					'<div class="pi-data-label">Role</div>' +
					'<div class="pi-data-value ab-level-stat" data-stat="role"></div>' +
					'</div>'
			);
			$stat = $row.find('[data-stat="role"]');
		}
		abSetRole($stat, role);
		var $levels = $box.find('.ab-level-stat[data-stat="levels"]').closest('.pi-data');
		if ($levels.length) {
			$levels.before($row);
		} else {
			var $nav = $box.find('.pi-navigation');
			if ($nav.length) {
				$nav.before($row);
			} else {
				$box.append($row);
			}
		}
	}

	setPfp();
});

$(function () {
	var $imgs = $('.roblox-asset img[data-asset-id].roblox-asset-img--pending');
	if (!$imgs.length) {
		return;
	}
	$imgs.each(function () {
		var $img = $(this);
		var assetId = String($img.attr('data-asset-id') || '').replace(/[^0-9]/g, '');
		var size = String($img.attr('data-thumb-size') || '420x420');
		if (!assetId) {
			return;
		}
		fetch(
			'https://adventure-builder.net/api/roblox-proxy?service=thumbnails&assetIds=' +
				encodeURIComponent(assetId) +
				'&size=' +
				encodeURIComponent(size)
		)
			.then(function (res) {
				return res.ok ? res.json() : null;
			})
			.then(function (data) {
				var item = data && data.data && data.data[0];
				if (!item || item.state !== 'Completed' || !item.imageUrl) {
					return;
				}
				$img.attr('src', item.imageUrl).removeClass('roblox-asset-img--pending');
			})
			.catch(function () {});
	});
});

$(function () {
	var action = mw.config.get('wgAction');
	if (action !== 'edit' && action !== 'submit') {
		return;
	}
	if (!mw.hook) {
		return;
	}

	mw.messages.set({
		'ab-insert-level-title': 'Insert level',
		'ab-insert-player-title': 'Insert player',
		'ab-insert-image-title': 'Insert image'
	});

	var AB_INSERT_LEVEL_FIELDS = [
		{ data: '', label: 'Infobox' },
		{ data: 'plays', label: 'Plays' },
		{ data: 'creator', label: 'Creator' },
		{ data: 'title', label: 'Title' },
		{ data: 'description', label: 'Description' },
		{ data: 'clears', label: 'Clears' },
		{ data: 'favorites', label: 'Favorites' },
		{ data: 'difficulty', label: 'Difficulty' }
	];
	var AB_INSERT_PLAYER_FIELDS = [
		{ data: '', label: 'Infobox' },
		{ data: 'name', label: 'Name' },
		{ data: 'role', label: 'Role' },
		{ data: 'levels', label: 'Levels' },
		{ data: 'clears', label: 'Clears' },
		{ data: 'followers', label: 'Followers' }
	];
	var AB_INSERT_ALIGN = [
		{ data: '', label: 'None' },
		{ data: 'left', label: 'Left' },
		{ data: 'right', label: 'Right' },
		{ data: 'center', label: 'Center' },
		{ data: 'inline', label: 'Inline' }
	];

	function abInsertSanitize(value) {
		return String(value || '')
			.replace(/\|/g, '')
			.replace(/\{\{/g, '')
			.replace(/\}\}/g, '')
			.trim();
	}

	function abInsertDigits(value) {
		return String(value || '')
			.replace(/^rbxassetid:\/\//i, '')
			.replace(/[^0-9]/g, '');
	}

	function abInsertIsUrl(value) {
		return /^https?:\/\//i.test(String(value || '').trim());
	}

	function abInsertTemplate(name, id, field) {
		var clean = abInsertDigits(id);
		if (!clean) {
			return '';
		}
		if (field) {
			return '{{' + name + '|' + clean + '|' + field + '}}';
		}
		return '{{' + name + '|' + clean + '}}';
	}

	function abInsertImageWikitext(values) {
		var source = abInsertSanitize(values.source);
		if (!source) {
			return '';
		}
		if (!abInsertIsUrl(source)) {
			source = abInsertDigits(source);
			if (!source) {
				return '';
			}
		}
		var bits = ['{{image', source];
		var width = abInsertDigits(values.width);
		var height = abInsertDigits(values.height);
		if (width && height) {
			bits.push(width + 'x' + height + 'px');
		} else if (width) {
			bits.push(width + 'px');
		} else if (height) {
			bits.push('x' + height + 'px');
		}
		if (values.align) {
			bits.push(values.align);
		}
		if (values.caption) {
			bits.push(abInsertSanitize(values.caption));
		}
		if (values.alt) {
			bits.push('alt=' + abInsertSanitize(values.alt));
		}
		if (values.link) {
			bits.push('link=' + abInsertSanitize(values.link));
		}
		return bits[0] + '|' + bits.slice(1).join('|') + '}}';
	}

	function abEscapeAttr(value) {
		return mw.html.escape(String(value || ''));
	}

	function abOptionsHtml(options) {
		var html = '';
		var i;
		for (i = 0; i < options.length; i++) {
			html +=
				'<option value="' +
				abEscapeAttr(options[i].data) +
				'">' +
				abEscapeAttr(options[i].label) +
				'</option>';
		}
		return html;
	}

	function abFieldHtml(id, label, control) {
		return (
			'<div class="wikieditor-toolbar-field-wrapper">' +
			'<label for="' +
			abEscapeAttr(id) +
			'">' +
			abEscapeAttr(label) +
			'</label>' +
			control +
			'</div>'
		);
	}

	function abInputHtml(id, placeholder) {
		return (
			'<input type="text" id="' +
			abEscapeAttr(id) +
			'"' +
			(placeholder ? ' placeholder="' + abEscapeAttr(placeholder) + '"' : '') +
			'/>'
		);
	}

	function abSelectHtml(id, options) {
		return (
			'<select id="' + abEscapeAttr(id) + '">' + abOptionsHtml(options) + '</select>'
		);
	}

	function abSetInsertEnabled($dialog, enabled) {
		var $button = $dialog.closest('.ui-dialog').find('.ui-dialog-buttonpane button').first();
		if ($button.length && $button.button) {
			$button.button('option', 'disabled', !enabled);
		}
	}

	function abBindDialogEnter($dialog) {
		if ($dialog.data('dialogkeypressset')) {
			return;
		}
		$dialog.data('dialogkeypressset', true);
		var $ui = $dialog.closest('.ui-dialog');
		$ui.on('keypress', function (e) {
			if (e.which === 13) {
				var dialogAction = $ui.data('dialogaction');
				var $button = dialogAction ? $(dialogAction) : $ui.find('button').first();
				if ($button && $button.trigger) {
					$button.trigger('click');
				}
				e.preventDefault();
			}
		});
		$ui.find('button').on('focus', function (e) {
			$ui.data('dialogaction', e.delegateTarget);
		});
	}

	function abInsertIntoTextarea($dialog, wikitext) {
		if (!wikitext) {
			return;
		}
		var context = $dialog.data('context');
		$dialog.dialog('close');
		if (!context || !context.$textarea) {
			return;
		}
		context.$textarea.textSelection('encapsulateSelection', {
			pre: '',
			peri: wikitext,
			post: '',
			replace: true,
			selectPeri: false
		});
	}

	function abMakeTemplateDialog(kind) {
		var prefix = 'ab-insert-' + kind;
		var idField = prefix + '-id';
		var fieldField = prefix + '-field';
		var templateName = kind === 'player' ? 'player' : 'level';
		var fields = kind === 'player' ? AB_INSERT_PLAYER_FIELDS : AB_INSERT_LEVEL_FIELDS;
		var idLabel = kind === 'player' ? 'Player ID' : 'Level ID';

		function currentWikitext($root) {
			return abInsertTemplate(
				templateName,
				$root.find('#' + idField).val(),
				$root.find('#' + fieldField).val()
			);
		}

		return {
			title: mw.message(prefix + '-title'),
			id: prefix + '-dialog',
			html:
				'<div class="wikieditor-toolbar-dialog-wrapper"><fieldset>' +
				abFieldHtml(idField, idLabel, abInputHtml(idField, '123')) +
				abFieldHtml(fieldField, 'Insert as', abSelectHtml(fieldField, fields)) +
				'</fieldset></div>',
			init: function () {
				var $dialog = $(this);
				$dialog.find('#' + idField + ', #' + fieldField).on('input change', function () {
					abSetInsertEnabled($dialog, !!currentWikitext($dialog));
				});
			},
			dialog: {
				resizable: false,
				dialogClass: 'wikiEditor-toolbar-dialog ab-insert-dialog',
				width: 500,
				buttons: {
					'wikieditor-toolbar-tool-file-insert': function () {
						abInsertIntoTextarea($(this), currentWikitext($(this)));
					},
					'wikieditor-toolbar-tool-file-cancel': function () {
						$(this).dialog('close');
					}
				},
				open: function () {
					var $dialog = $(this);
					var context = $dialog.data('context');
					var selected = '';
					if (context && context.$textarea) {
						selected = String(context.$textarea.textSelection('getSelection') || '').trim();
					}
					$dialog.find('#' + idField).val(abInsertDigits(selected) || '');
					$dialog.find('#' + fieldField).val('');
					abSetInsertEnabled($dialog, !!currentWikitext($dialog));
					$dialog.find('#' + idField).trigger('focus');
					abBindDialogEnter($dialog);
				}
			}
		};
	}

	function abMakeImageDialog() {
		function currentWikitext($root) {
			return abInsertImageWikitext({
				source: $root.find('#ab-insert-image-source').val(),
				width: $root.find('#ab-insert-image-width').val(),
				height: $root.find('#ab-insert-image-height').val(),
				align: $root.find('#ab-insert-image-align').val(),
				caption: $root.find('#ab-insert-image-caption').val(),
				alt: $root.find('#ab-insert-image-alt').val(),
				link: $root.find('#ab-insert-image-link').val()
			});
		}

		return {
			title: mw.message('ab-insert-image-title'),
			id: 'ab-insert-image-dialog',
			html:
				'<div class="wikieditor-toolbar-dialog-wrapper"><fieldset>' +
				abFieldHtml(
					'ab-insert-image-source',
					'Asset ID or URL',
					abInputHtml('ab-insert-image-source', '123 or https://')
				) +
				'<div class="wikieditor-toolbar-field-wrapper ab-insert-size-row">' +
				'<div class="wikieditor-toolbar-floated-field-wrapper">' +
				'<label for="ab-insert-image-width">Width (px)</label>' +
				abInputHtml('ab-insert-image-width', '300') +
				'</div>' +
				'<div class="wikieditor-toolbar-floated-field-wrapper">' +
				'<label for="ab-insert-image-height">Height (px)</label>' +
				abInputHtml('ab-insert-image-height', 'optional') +
				'</div>' +
				'</div>' +
				abFieldHtml(
					'ab-insert-image-align',
					'Align',
					abSelectHtml('ab-insert-image-align', AB_INSERT_ALIGN)
				) +
				abFieldHtml(
					'ab-insert-image-caption',
					'Caption',
					abInputHtml('ab-insert-image-caption')
				) +
				abFieldHtml('ab-insert-image-alt', 'Alt text', abInputHtml('ab-insert-image-alt')) +
				abFieldHtml('ab-insert-image-link', 'Link', abInputHtml('ab-insert-image-link')) +
				'</fieldset></div>',
			init: function () {
				var $dialog = $(this);
				$dialog.find('input, select').on('input change', function () {
					abSetInsertEnabled($dialog, !!currentWikitext($dialog));
				});
			},
			dialog: {
				resizable: false,
				dialogClass: 'wikiEditor-toolbar-dialog ab-insert-dialog',
				width: 590,
				buttons: {
					'wikieditor-toolbar-tool-file-insert': function () {
						abInsertIntoTextarea($(this), currentWikitext($(this)));
					},
					'wikieditor-toolbar-tool-file-cancel': function () {
						$(this).dialog('close');
					}
				},
				open: function () {
					var $dialog = $(this);
					var context = $dialog.data('context');
					var selected = '';
					if (context && context.$textarea) {
						selected = String(context.$textarea.textSelection('getSelection') || '').trim();
					}
					$dialog.find('input').val('');
					$dialog.find('#ab-insert-image-align').val('');
					if (selected) {
						$dialog.find('#ab-insert-image-source').val(selected);
					}
					abSetInsertEnabled($dialog, !!currentWikitext($dialog));
					$dialog.find('#ab-insert-image-source').trigger('focus');
					abBindDialogEnter($dialog);
				}
			}
		};
	}

	function abRegisterInsertDialogs($textarea) {
		var context = $textarea.data('wikiEditor-context');
		if (!context || !$.wikiEditor || !$.wikiEditor.modules.dialogs) {
			return;
		}
		$.wikiEditor.modules.dialogs.fn.create(context, {
			'ab-insert-level': abMakeTemplateDialog('level'),
			'ab-insert-player': abMakeTemplateDialog('player'),
			'ab-insert-image': abMakeImageDialog()
		});
	}

	mw.hook('wikiEditor.toolbarReady').add(function ($textarea) {
		if (!$textarea || !$textarea.wikiEditor) {
			return;
		}
		abRegisterInsertDialogs($textarea);
		$textarea.wikiEditor('removeFromToolbar', {
			section: 'main',
			group: 'insert',
			tool: 'file'
		});
		mw.loader.using('oojs-ui.styles.icons-user').then(function () {
			$textarea.wikiEditor('addToToolbar', {
				section: 'main',
				group: 'insert',
				tools: {
					ablevel: {
						label: 'Level',
						type: 'button',
						oouiIcon: 'play',
						action: {
							type: 'dialog',
							module: 'ab-insert-level'
						}
					},
					abplayer: {
						label: 'Player',
						type: 'button',
						oouiIcon: 'userAvatar',
						action: {
							type: 'dialog',
							module: 'ab-insert-player'
						}
					},
					abimage: {
						label: 'Image',
						type: 'button',
						oouiIcon: 'image',
						action: {
							type: 'dialog',
							module: 'ab-insert-image'
						}
					}
				}
			});
		});
	});
});