User:Boyconga278/SVGedit.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * Allow editing SVG file's source code without having to save them locally (aka "download") them.
 *
 * @rev 1 (2014-03-22)
 * @rev 2 (2015-05-29)
 * @author Rillke, 2014-2015
 */
// List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/
// Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting]
/*global jQuery:false, mediaWiki:false, MwJSBot:false, CodeMirror:false*/
 
// Set jsHint-options. You should not set forin or undef to false if your script does not validate.
/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true*/
 
( function ( $, mw ) {
"use strict";
 
var svgEdit,
	NS_FILE = 6,
	MYSELF = 'SVGEdit',
	conf = mw.config.get([
		'wgPageName',
		'wgNamespaceNumber',
		'wgRevisionId',
		'wgTitle'
	]);
 
svgEdit = {
	version: '0.0.7.2',
	init: function() {
		var $activationLinks = $();

		// File namespace?
		if (conf.wgNamespaceNumber !== NS_FILE || !/\.svg$/.test(conf.wgPageName)) return svgEdit.log('Not a SVG-file. Aborting initialization.');
		if (mw.user.isAnon()) return svgEdit.log('Anonymous users cannot upload files. Aborting initialization.');
		if (conf.wgRevisionId < 1 || $('.filehistory').find('td.filehistory-selected').length === 0) return svgEdit.log('Page or file does not exist.');

		$activationLinks = $activationLinks.add( mw.libs.commons.ui.addEditLink('#SVGedit', "edit SVG", 'e-edit-raw-SVG', "Edit SVG source code") );

		$activationLinks.click(function(e) {
			e.preventDefault();
			svgEdit.run();
			$activationLinks.addClass('ui-state-disabled');
		});
		if (mw.util.getParamValue('svgrawedit')) svgEdit.run();
	},
	registerModules: function() {
		// Register custom modules
		if (null === mw.loader.getState('mediawiki.commons.MwJSBot')) mw.loader.implement('mediawiki.commons.MwJSBot', ["//commons.wikimedia.org/w/index.php?action=raw&ctype=text/javascript&title=User:Rillke/MwJSBot.js"], { /*no styles*/
		}, { /*no messages*/
		});
	},
	run: function() {
		// Create GUI
		svgEdit.registerModules();

		mw.loader.using(['mediawiki.commons.MwJSBot', 'user.options'], function() {
			svgEdit.gui();
		});
	},
	gui: function() {
		var $gui = $('<div>'),
			$preview = $('<div>')
				.appendTo($gui),
			$imgPreviewContainer = $('<div>')
				.css({
					'position': 'relative',
					'overflow': 'hidden',
					'display': 'inline-block'
				})
				.html('<a href="https://en.wikipedia.org/wiki/Librsvg" target="_blank">RSVG</a> rendering:<br />')
				.hide()
				.appendTo($preview),
			$imgPreview = $('<img>')
				.attr({
					'title': "rsvg preview"
				})
				.css({
					'vertical-align': 'top'
				})
				.appendTo($imgPreviewContainer),
			$imgPreview2Container = $('<div>')
				.css({
					'position': 'relative',
					'overflow': 'hidden',
					'display': 'inline-block'
				})
				.html('Browser rendering (iframe):<br />')
				.hide()
				.appendTo($preview),
			$imgPreview2Overlay = $('<div>')
				.css({
					'position': 'absolute',
					'left': 0,
					'top': 0,
					'bottom': 0,
					'right': 0,
					'z-index': 1
				})
				.appendTo($imgPreview2Container),
			$imgPreview2 = $('<iframe>')
				.attr({
					'sandbox': 'sandbox',
					'title': "browser preview"
				})
				.css({
					'border': '1px solid #eee',
					'width': 0,
					'height': 0,
					'resizable': 'both',
					'vertical-align': 'top'
				})
				.appendTo($imgPreview2Container),
			$taWrap = $('<div>')
				.appendTo($gui),
			$ta = $('<textarea>').attr({
					rows: mw.user.options.get('rows'),
					cols: mw.user.options.get('cols'),
					disabled: 'disabled'
				}).css({
					width: '99%'
				}).appendTo($taWrap),
			$sum = $('<input type="text" style="width:99%" maxlength="200" placeholder="upload summary (changes, techniques)"/>').appendTo($gui),
			$saveBtn = $('<button>').attr({
					type: 'button',
					role: 'button',
					disabled: 'disabled'
				}).text("Save SVG").appendTo($gui),
			$loadCodeEditorBtn = $('<button>').attr({
					type: 'button',
					role: 'button',
					disabled: 'disabled',
					title: 'Loads a code editor (XML mode)'
				}).text("Load CodeMirror").appendTo($gui),
			$previewBtn = $('<button>').attr({
					type: 'button',
					role: 'button',
					disabled: 'disabled',
					title: 'Render a preview'
				}).text("Preview").appendTo($gui);
				
		$ta.val("Loading SVG");
		svgEdit.$fetch().done(function(r) {
			$ta.val(r);
			$saveBtn
				.add($ta)
				.add($loadCodeEditorBtn)
				.add($previewBtn)
				.removeAttr('disabled');
		});
		var allowCloseWindow, timeout = setTimeout(function() {
			mw.loader.using('mediawiki.confirmCloseWindow', function() {
				allowCloseWindow = mw.confirmCloseWindow();
			});
		}, 5000);
		
		$saveBtn.click(function(e) {
			e.preventDefault();
			$saveBtn.add($sum).attr('disabled', 'disabled');
			svgEdit.save(
				svgEdit.CodeMirror
				? svgEdit.CodeMirror.getValue()
				: $ta.val(),
				$sum.val()
			).done(function() {
				clearTimeout(timeout);
				if (allowCloseWindow) allowCloseWindow.release();
				svgEdit.reload();
			}).fail(function() {
				alert("Something went wrong");
				$saveBtn.add($sum).removeAttr('disabled');
				$taWrap.attr('noblock', 1).unblock();
			});
			svgEdit.block($taWrap);
		});

		$loadCodeEditorBtn.click(function() {
			$(this).attr('disabled', 'disabled');
			svgEdit.loadCodeEditor($ta);
		});

		$previewBtn.click(function() {
			var val = svgEdit.CodeMirror
					? svgEdit.CodeMirror.getValue()
					: $ta.val();

			var blob, URL, dataUrl, typedArray, v, w, h, m;
			URL = window.URL || window.webkitURL;

			blob = new Blob( [ val ], { type: 'image/svg+xml' } );
			dataUrl = URL.createObjectURL( blob );
			// Naive RegExp matching (avoids parsing the whole document)
			// and possible security or malformed SVG troubles
			v = val.slice(4, 5000);
			m = v.match( /height\s*=\s*["']([\d\.]+)["']/ );
			if ( !( m && ( h = m[1] ) && ( h = Number( h ) ) && h > 15 ) ) {
				h = 500;
			}
			m = v.match( /width\s*=\s*["']([\d\.]+)["']/ );
			if ( !( m && ( w = m[1] ) && ( w = Number( w ) ) && w > 15 ) ) {
				w = 500;
			}
			$previewBtn.attr('disabled', 'disabled');

			$imgPreview2Container.show();
			$imgPreviewContainer.css({
				'height': 500,
				'width': 500
			}).show();
			svgEdit.block( $imgPreviewContainer );
			svgEdit.block( $imgPreview2Container );

			$imgPreview2.one('load', function() {
				$imgPreview2Container.unblock();
			}).attr( 'src', dataUrl ).css({
				'width': w,
				'height': h
			});

			svgEdit
				.fetchPreview(val)
				.done(function(statusText, response) {
					typedArray = new Uint8Array( response );
					blob = new Blob( [ typedArray ], { type: 'image/jpeg' } );
					dataUrl = URL.createObjectURL( blob );
					$imgPreviewContainer.css({
						'height': 'auto',
						'width': 'auto'
					});
					$imgPreview.attr( 'src', dataUrl );
					setTimeout(function() {
						$imgPreview2.css({
							'width': $imgPreview.width(),
							'height': $imgPreview.height()
						});
					}, 1000);
				})
				.fail(function(r) {
					$imgPreview.attr( 'src', '//upload.wikimedia.org/wikipedia/commons/thumb/5/55/Bug_blank.svg/200px-Bug_blank.svg.png' );
				})
				.always(function() {
					$previewBtn.removeAttr('disabled');
					$imgPreviewContainer.add($imgPreview2Container).unblock();
				});
		});
		$gui.prependTo('#mw-content-text');
	},
	block: function( $el ) {
		mw.loader.using('ext.gadget.jquery.blockUI', function() {
			if ($el.attr('noblock')) return;
			$el.block({
				message: '<img src="//upload.wikimedia.org/wikipedia/commons/1/10/Loading-special.gif" height="15" width="128"/>',
				css: {
					border: 'none',
					background: 'none'
				} 
			});
		});
	},
	$fetch: function() {
		// Fetch SVG source code
		svgEdit.bot = new MwJSBot();
		svgEdit.fileUrl = '';
		
		$('#file').find('a').each(function(i, el) {
			var href = $(el).attr('href'),
				fileDomain = 'upload.wikimedia.org',
				fileDomainPos = href.indexOf('upload.wikimedia.org');
				
			if (fileDomainPos < 10 && fileDomainPos !== -1 && /\.svg$/.test(href)) {
				svgEdit.fileUrl = href;
				return false;
			}
		});
		if (!svgEdit.fileUrl) {
			svgEdit.log("Unable to extract file URL.");
			throw new Error("Unable to extract file URL.");
		}
		
		// Assuming the SVG is UTF-8-encoded
		return $.ajax({
				url: svgEdit.fileUrl,
				cache: false,
				beforeSend: function (xhr) {
					xhr.overrideMimeType('text/plain; charset=UTF-8');
				}
			});
	},
	loadCodeEditor: function( $textArea, $parent ) {
		// Just in case someone complains about the license ...
		var mirrors = [
				'//commons.wikimedia.org/w/index.php?',
				'//tools-static.wmflabs.org/rillke/CodeMirror/',
				'//mol-static.wmflabs.org/CodeMirror/'
			],
			scripts = ['lib/codemirror.js', 'mode/xml/xml.js'],
			styles = ['lib/codemirror.css'],
			params = { 'action': 'raw', 'ctype': 'text/javascript', 'title': '?' };

		var rlScripts = $.map(scripts, function(el) {
			params.title = 'User:Rillke/CodeMirror/' + el;
			return mirrors[0] + $.param(params);
		});
		params.ctype = 'text/css';
		var rlStyles = $.map(styles, function(el) {
			params.title = 'User:Rillke/CodeMirror/' + el;
			return mirrors[0] + $.param(params);	
		});

		if (null === mw.loader.getState('mediawiki.commons.CodeMirror')) {
			mw.loader.implement('mediawiki.commons.CodeMirror',
				rlScripts,  { url: { screen: rlStyles } },
				{ /*no messages*/ });
		}

		mw.loader.using('mediawiki.commons.CodeMirror', function() {
			var h = $textArea.parent().height(),
				m = $textArea.val()
					.slice(0, 6000)
					.match(/.+\n([\t ]+)<\S+(?:.|\n)*\n\1</),
				settings = {
					lineNumbers: true,
					mode: 'xml',
					viewportMargin: 120
				},
				l;

			if (m) {
				l = m[1].length;
				if (l > 0 && l < 9) {
					if (/ /.test(m[1])) {
						svgEdit.log('Indention with spaces');
						$.extend(true, settings, {
							extraKeys: {
								Tab: function() {
									svgEdit.CodeMirror.execCommand('insertSoftTab');
								}
							},
							tabSize: l
						});
					} else if (/\t/.test(m[1])) {
						svgEdit.log('Indention with tabs');
						$.extend(true, settings, {
							indentWithTabs: true,
							tabSize: 2
						});
					}
				}
			}
			svgEdit.CodeMirror = CodeMirror.fromTextArea($textArea[0], settings);
			$( svgEdit.CodeMirror.display.scroller ).css( {
				'height': ( h - 5 ) + 'px'
			} );
			$( svgEdit.CodeMirror.display.wrapper ).css( {
				'border': '1px solid #eee',
				'height': 'auto'
			} );
		});
	},
	save: function(text, summary) {
		if (summary) summary += ' // ';

		return svgEdit.bot.multipartMessageForUTF8Files()
			.appendPart('format', 'json')
			.appendPart('action', 'upload')
			.appendPart('filename', conf.wgTitle)
			.appendPart('comment', summary + 'Editing SVG source code using [[User:Rillke/SVGedit.js]]; upload handled by [[User:Rillke/MwJSBot.js]]')
			.appendPart('file', text, conf.wgTitle)
			.appendPart('ignorewarnings', 1)
			.appendPart('token', mw.user.tokens.get('editToken'))
			.$send();
	},
	fetchPreview: function(svg) {
		return svgEdit.bot.multipartMessageForUTF8Files()
			.appendPart('file', svg, 'input.svg')
			.$send('//tools.wmflabs.org/convert/svg2png.php', 'arraybuffer');
	},
	reload: function() {
		window.location.href = mw.util.getUrl(conf.wgPageName);
	},
	log: function() {
		var args = Array.prototype.slice.call(arguments);
		args.unshift(MYSELF);
		mw.log.apply(mw.log, args);
	}
};
 
// Expose globally
window.svgRawEditor = svgEdit;

mw.loader.using(['ext.gadget.editDropdown', 'mediawiki.util', 'mediawiki.user'], svgEdit.init);
 
}( jQuery, mediaWiki ));