MediaWiki:ThemeEditor.js

From Support Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
mw.loader.using(['mediawiki.api','mediawiki.util','jquery','oojs-ui-core','oojs-ui-widgets','oojs-ui-windows']).then(function(){ 
	if (!mw.config.get('wgUserGroups').includes('interface-admin')) return;
	function ThemeDialog( config ) {
		ThemeDialog.super.call( this, config );
	}
	OO.inheritClass( ThemeDialog, OO.ui.ProcessDialog );

	ThemeDialog.static.name = 'themeDialog';
	ThemeDialog.static.title = 'Theme Editor';
	ThemeDialog.static.actions = [
		{
			action: 'save',
			label: 'Done',
			flags: 'primary'
		},
		{
			label: 'Cancel',
			flags: 'safe'
		}
	];

	ThemeDialog.static.MapSettingElementProperties = [
		{
			setting: 'body-background-color',
			desc: 'Body background color',
			element: 'body',
			property: 'background-color',
			value: '#ffffff'
		},
		{
			setting: 'body-background-image',
			desc: 'Background image',
			element: 'body',
			property: 'background-image',
			value: 'none'
		},
		{
			setting: 'body-link-color',
			desc: 'Body link color',
			element: '.vector-menu-portal .vector-menu-content li a',
			property: 'color',
			value: '#009cff'
		},
		{
			setting: 'body-visited-link-color',
			desc: 'Body visited link color',
			element: '.vector-menu-portal .vector-menu-content li a:visited,.content-wrapper a:visited',
			property: 'color',
			value: '#038ae0'
		},
		{
			setting: 'body-red-link-color',
			desc: 'Body Red link color',
			element: '.vector-menu-tabs .new a',
			property: 'color',
			value: '#dd3333'
		},
		{
			setting: 'body-text-color',
			desc: 'Body text color',
			element: '.mw-footer li',
			property: 'color',
			value: '#000000'
		},
		{
			setting: 'content-background-color',
			desc: 'Content area background color',
			element: 'div#content',
			property: 'background-color',
			value: '#ffffff'
		},
		{
			setting: 'content-border-color',
			desc: 'Content area border color',
			element: 'div#content',
			property: 'border-color',
			value: '#000000'
		},
		{
			setting: 'content-link-color',
			desc: 'Content area link color',
			element: 'div#content a',
			property: 'color',
			value: '#009cff'
		},
		{
			setting: 'content-visited-link-color',
			desc: 'Content area visited link color',
			element: 'div#content a:visited',
			property: 'color',
			value: '#038ae0'
		},
		{
			setting: 'content-red-link-color',
			desc: 'Content area red link color',
			element: 'div#content a.new',
			property: 'color',
			value: '#dd3333'
		},
		{
			setting: 'content-text-color',
			desc: 'Content area text color',
			element: 'div#content',
			property: 'color',
			value: '#000000'
		}
	];

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

		this.content = new OO.ui.PanelLayout( {
			padded: true,
			expanded: false
		} );
		this.loadCurrentSettings();
		this.content.$element.append('<p style="grid-column-start:1; grid-column-end:4; text-align:center;">Here you can make theming changes to your wiki.</p>')
							 .append('<label for="bgfile">Background image:</label><input type="file" id="bgfile" accept="image/jpeg,image/gif,image/png,image/webp" style="grid-column-start:2; grid-column-end:4;" />');
		for (var e in ThemeDialog.static.MapSettingElementProperties) {
			var eS = ThemeDialog.static.MapSettingElementProperties[e];
			if (eS.setting == 'body-background-image') continue;
			this.content.$element.append('<label for="setting-'+eS.setting+'">'+eS.desc+':</label>')
								 .append('<input type="color" value="'+this.standardizeColor(eS.value).color+'" id="setting-'+eS.setting+'" data-setting="'+eS.setting+'" data-setting-type="color" class="themeSetting" />')
								 .append('<label>Alpha: <input type="range" value="'+this.standardizeColor(eS.value).alpha+'" id="setting-'+eS.setting+'-alpha" data-setting="'+eS.setting+'" data-setting-type="alpha" class="themeSetting" /></label>');
		}
		this.content.$element.wrapInner('<div style="display:grid; grid:50px auto / 200px 80px 200px; justify-content: center;"></div>');
		this.$body.append( this.content.$element );
		$('.oo-ui-windowManager-modal > .oo-ui-dialog').css('background-color','transparent');
		$('.themeSetting').on('input',function() {
			var setting = $(this).data('setting');
			var color = $('#setting-'+setting).val();
			var alpha = parseInt(parseInt($('#setting-'+setting+'-alpha').val())*2.55);
			if (alpha != 255) color += alpha.toString(16);		
			$(':root').css('--'+setting, color);
			ThemeDialog.static.setValue(setting, color);
		});
		$('#bgfile').change( this.previewBackgroundImage );
	};

	ThemeDialog.prototype.getActionProcess = function ( action ) {
		var dialog = this;
		if ( action ) {
			if (action =='save') {
				var promises = [];
				var file = document.getElementById('bgfile').files[0];
				if (file != undefined) {
					var filename = 'Site-background';
					switch (file.type) {
						case 'image/jpeg':
							filename += '.jpg';
							break;
						case 'image/gif':
							filename += '.gif';
							break;
						case 'image/png':
							filename += '.png';
							break;
						case 'image/webp':
							filename += '.webp';
							break;
					}
					var fd = new FormData();
					fd.append('action','upload');
					fd.append('token',mw.user.tokens.get('csrfToken'));
					fd.append('filename',filename);
					fd.append('file',file);
					fd.append('comment','Uploaded by ThemeEditor');
					fd.append('watchlist','preferences');
					fd.append('ignorewarnings',1);
					fd.append('format','json');
					promises.push($.ajax({
						url: mw.config.get('wgScriptPath') + '/api.php',
						method: 'POST',
						data: fd,
						cache: false,
						contentType: false,
						processData: false,
						type: 'POST'
					}));
				}

				Promise.allSettled(promises).then(function(results) {
					if (results.length > 0) {
						if (results[0].value != undefined) {
							if (results[0].value.upload != undefined) {
								if (results[0].value.upload.imageinfo != undefined) {
									if (results[0].value.upload.imageinfo.url != undefined) {
										ThemeDialog.static.setValue('body-background-image','url('+results[0].value.upload.imageinfo.url+')');
									}
								}
							}
						}
					}
					var thisCSS = '/* ### BEGIN THEME SETTINGS ### */\n\n:root {';
					for (var e in ThemeDialog.static.MapSettingElementProperties) {
						var eS = ThemeDialog.static.MapSettingElementProperties[e];
						thisCSS += '\n\t--'+eS.setting+': '+eS.value+';';
					}
					thisCSS += '\n}\n\n';
					promises = [$.getJSON('https://commons.wiki.gg/api.php?action=query&prop=revisions&rvprop=content&titles=MediaWiki:Theme-base.css&format=json&callback=?'),
								(new mw.Api).get({'action':'query','prop':'revisions',rvprop:'content',titles:'MediaWiki:Common.css'})];
					Promise.allSettled(promises).then(function(results) {
						if (results.length == 0) return;
						if (results[0].value == undefined) return;
						if (results[0].value.query == undefined) return;
						var data = Object.entries(results[0].value.query.pages)[0][1].revisions[0]['*'];
						thisCSS += data + '\n\n\n/* ### END THEME SETTINGS ### */';
						var currentCSS = '';
						if (results[1].value != undefined) {
							if (results[1].value.query != undefined) {
								if (Object.entries(results[1].value.query.pages)[0][1].missing == undefined) {
									currentCSS = Object.entries(results[1].value.query.pages)[0][1].revisions[0]['*'];
									currentCSS = currentCSS.replace(/\/\* \#\#\# BEGIN THEME SETTINGS \#\#\# \*\/[.|\n]*\/\* \#\#\# END THEME SETTINGS \#\#\# \*\//ig,'');
								}
							}
						}
						currentCSS = thisCSS + '\n\n' + currentCSS;
						(new mw.Api).postWithEditToken({action:'edit',title:'MediaWiki:Common.css',text:currentCSS,summary:'Updating theme'}).done;
					});
				});
			}
			return new OO.ui.Process( function () {
				dialog.close( {
					action: action
				} );
			} );
		}
		return ThemeDialog.super.prototype.getActionProcess.call( this, action );
	};

	ThemeDialog.prototype.getBodyHeight = function () {
		return this.content.$element.outerHeight( true );
	};

	ThemeDialog.prototype.loadCurrentSettings = function () {
		for (var e in ThemeDialog.static.MapSettingElementProperties) {
			var eS = ThemeDialog.static.MapSettingElementProperties[e];
			var value = $(eS.element).first().css(eS.property);
			if (value == undefined) value = $(':root').css('--'+eS.setting);
			if (value != undefined) ThemeDialog.static.MapSettingElementProperties[e].value = value;
		}
	}

	ThemeDialog.prototype.makeDraggable = function() {
		var $frameEl = this.$element.find(".oo-ui-window-frame");
		var $handleEl = this.$element.find(".oo-ui-processDialog-location").css({"cursor":"move"});
		// Position for css translate transformations, relative to initial position
		// (which is centered on viewport when scrolled to top)
		var position = { x: 0, y: 0 };
		const constrain = function(val, minVal, maxVal) {
			if (val < minVal) return minVal;
			if (val > maxVal) return maxVal;
			return val;
		};
		const constrainX = function (val) {
			// Don't too far horizontally (leave at least 100px visible)
			var limit = window.visualViewport.width/2 + $frameEl.outerWidth()/2 - 100;
			return constrain(val, -1*limit, limit);
		};
		const constrainY = function (val) {
			// Can't take title bar off the viewport, since it's the drag handle
			var minLimit = -1*(window.visualViewport.height - $frameEl.outerHeight())/2;
			// Don't go too far down the page: (whole page height) - (initial position)
			var maxLimit = $("body").innerHeight() - window.visualViewport.height/2;
			return constrain(val, minLimit, maxLimit);
		};

		var pointerdown = false;
		var dragFrom = {};

		var onDragStart = function(event) {
			pointerdown = true;
			dragFrom.x = event.clientX;
			dragFrom.y = event.clientY;
		};
		var onDragMove = function(event) {
			if (!pointerdown || dragFrom.x == null || dragFrom.y === null) {
				return;
			}
			const dx = event.clientX - dragFrom.x;
			const dy = event.clientY - dragFrom.y;
			dragFrom.x = event.clientX;
			dragFrom.y = event.clientY;
			position.x = constrainX(position.x + dx);
			position.y = constrainY(position.y + dy);
			$frameEl.css("transform", 'translate('+position.x+'px, '+position.y+'px)');
		};
		var onDragEnd = function () {
			pointerdown = false;
			delete dragFrom.x;
			delete dragFrom.y;
			// Make sure final positions are whole numbers
			position.x = Math.round(position.x);
			position.y = Math.round(position.y);
			$frameEl.css("transform",'translate('+position.x+'px, '+position.y+'px)');
		};

		const pointer = ("PointerEvent" in window) ? "pointer" : "mouse";
		$handleEl.on(pointer+"down.ThemeDialog", onDragStart);
		$("body").on(pointer+"move.ThemeDialog", onDragMove);
		$("body").on(pointer+"up.ThemeDialog", onDragEnd);
	}

	ThemeDialog.prototype.getSetupProcess = function ( data ) {
		data = data || {};
		return ThemeDialog.super.prototype.getSetupProcess.call( this, data )
			.next( function() {
				this.makeDraggable();
			}, this);
	}

	ThemeDialog.prototype.previewBackgroundImage = function () {
		$('body').css({'background-image':'url('+URL.createObjectURL(document.getElementById('bgfile').files[0])+')',
  					   'background-attachment':'fixed',
  					   'background-repeat':'no-repeat',
  					   'background-size':'cover'});
	}

	ThemeDialog.static.setValue = function ( setting, value ) {
		for (var e in ThemeDialog.static.MapSettingElementProperties) {
			if (ThemeDialog.static.MapSettingElementProperties[e].setting == setting) ThemeDialog.static.MapSettingElementProperties[e].value = value;
		}
	}

	ThemeDialog.prototype.standardizeColor = function ( str ) {
		var ctx = document.createElement('canvas').getContext('2d');
		ctx.fillStyle = str.replace(/rgba\((\d+), (\d+), (\d+), (.+)\)/,'rgb($1,$2,$3)');
		var alpha = 100;
		var alphaMatch = str.match(/rgba\(\d+, \d+, \d+, (.+)\)/);
		if (alphaMatch) { 
			alpha = parseInt(parseFloat(alphaMatch[1])*100);
		}
		return { color: ctx.fillStyle, alpha: alpha };
	}

	var windowManager = new OO.ui.WindowManager({forceTrapFocus:false});
	$( document.body ).append( windowManager.$element );

	var themeDialog = new ThemeDialog({
		size: 'large'
	});
	windowManager.addWindows( [ themeDialog ] );

	var node = mw.util.addPortletLink('p-tb','javascript:void','Theme Editor');
	$( node ).on('click', function(e) {
		windowManager.openWindow( themeDialog );
		e.preventDefault();
	});
});