MediaWiki:ThemeEditor.js
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();
});
});