Jump to content

User:NguoiDungKhongDinhDanh/QuickFunction.js

From Meta, a Wikimedia project coordination wiki

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)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * Run Javascript functions quickly from your tab.
 * Not supposed to be a replacement for browser console.
 * Basic knowledge of JS is required to use this.
**/

mw.loader.using(['mediawiki.util', 'ext.pygments', 'jquery', 'jquery.ui']).then(function() {
	if (mw.config.get('wgCodeEditorCurrentLanguage')) {
		return; // We don't mess with already-exist codeEditor(s).
	}
	
	mw.util.addPortletLink('p-tb', 'javascript:void(0)', 'QuickFunction', 't-qfn');
	
	$('#t-qfn').click(function(e) {
		e.preventDefault();
		if ($('#qfn-dialog').length) {
			$('#qfn-dialog').dialog('open');
			return;
		}
		
		// Styles.
		var url = 'https://upload.wikimedia.org/wikipedia/commons/thumb';
		var css = [
			`.qfn-logline::before {
				margin-right: 0.3em;
			}`,
			`[dir="rtl"] .qfn-logline::before {
				margin-right: unset;
				margin-left: 0.3em;
			}`,
			`.qfn-logline-info::before {
				content: url(${url}/2/2b/OOjs_UI_icon_information.svg/10px-OOjs_UI_icon_information.svg.png)
			}`,
			`.qfn-logline-warning::before {
				content: url(${url}/3/3b/OOjs_UI_icon_alert-warning.svg/10px-OOjs_UI_icon_alert-warning.svg.png)
			}`,
			`.qfn-logline-error::before {
				content: url(${url}/3/33/OOjs_UI_icon_clear-destructive.svg/10px-OOjs_UI_icon_clear-destructive.svg.png)
			}`,
			`#qfn-dialog input,
			#qfn-dialog select {
				-webkit-transition: outline 250ms;
				transition: outline 250ms;
			}`,
			`#qfn-dialog input:not([disabled]):focus,
			#qfn-dialog select:not([disabled]):focus,
			#qfn-dialog input:not([disabled]):active,
			#qfn-dialog select:not([disabled]):active {
				outline: 1px solid #3366CC;
			}`
		];
		mw.loader.addStyleTag(css.join('\n').replace(/^\t{2,}(?=(?:\t\w|\}|#))/gm, ''));
		
		// Other variables.
		var advert = ' ([[:m:User:NguoiDungKhongDinhDanh/QuickFunction.js|QFN]])';
		var jsonwp = 'User:' + mw.config.get('wgUserName') + '/QuickFunction.json';
		
		// Global general functions.
		var QFN = {};
		window.QFN = QFN;
		
		// Logging.
		QFN.log = function(content, header, type = 'info') {
			console[type === 'info' ? 'log' : type](content);
			var border, background, text;
			
			switch (type) { // Inspired by/Stolen from Chrome's DevTools.
				case 'info':
					border = '#C8CCD1';
					background = 'transparent';
					text = '#202124';
				break;
				case 'warn':
					border = '#FFF5C2';
					background = '#FFFBE5';
					text = '#5C3C00';
				break;
				case 'error':
					border = '#FFD6D6';
					background = '#FFF0F0';
					text = '#FF0000';
				break;
			}
			$('#qfn-log').append(
				$('<div>').attr({
					'class': 'qfn-logline qfn-logline-' + (type === 'warn' ? 'warning' : type)
				}).css({
					'margin': '0.3em 0',
					'border': '1px solid ' + border,
					'background': background,
					'padding': '0.5em',
					'line-height': '',
					'font-size': 'small'
				}).append(
					$('<p>').css({
						'display': 'inline-block'
					}).html(
						$('<strong>').css('color', text).text(header || 'QuickFunction:')
					),
					$('<div>').css({
						'padding': '0 calc(10px + 0.3em)',
						'color': text
					}).html(content && content.hasOwnProperty('toString') ? content.toString() : content)
				)
			);
		};
		['info', 'warn', 'error'].forEach(function(type) {
			QFN[type] = function(content, header = null) {
				return QFN.log(content, header, type);
			};
		});
		
		// QFN functions.
		QFN.apierror = function(error, response) {
			return QFN.warn(response.error.info, 'API error: ' + error);
		};
		QFN.buttontoggle = function() {
			$('#qfn-dialog').parent().find('button').not('#qfn-reload').each(function() {
				if ($(this).button('option', 'disabled')) {
					$(this).button('enable');
				} else {
					$(this).button('disable');
				}
			});
		};
		
		// API callers.
		QFN.call = function(method, data, callback) {
			$.ajax({
				url: mw.config.get('wgScriptPath') + '/api.php',
				data: data,
				dataType: 'json',
				method: method,
				success: callback,
				error: QFN.apierror
			});
		};
		QFN.fapi = function(url, method, data, callback) {
			(new mw.ForeignApi(url))[method](data).done(callback).fail(QFN.apierror);
		};
		
		// JSON formatters.
		QFN.fmt = function(object, replacer = null, space = '\t') {
			if (typeof object === 'string') {
				try {
					return JSON.stringify(JSON.parse(object), replacer, space);
				} catch (error) {
					QFN.warn('Invalid JSON.');
					return '';
				}
			} else if (typeof object === 'object') {
				return JSON.stringify(object, replacer, space);
			} else {
				QFN.warn('Invalid JSON.');
				return '';
			}
		};
		QFN.parse = function(object, callback) {
			var content = QFN.fmt(object);
			if (content === '') {
				QFN.warn('Invalid JSON.');
				return;
			}
			
			QFN.call('GET', {
				action: 'parse',
				text: '<syntaxhighlight lang="json">\n' + content + '\n</syntaxhighlight>',
				prop: ['text'],
				disablelimitreport: true,
				disableeditsection: true,
				disabletoc: true,
				contentmodel: 'wikitext',
				format: 'json',
				formatversion: 2
			}, callback || function(response) {
				QFN.info(response.parse.text);
			});
		};
		
		// Storing & retrieving.
		QFN.storage = function() {
			var storage;
			try {
				storage = JSON.parse(localStorage.getItem('QuickFunction-templates') || '{}');
			} catch (e) {
				storage = {};
			}
			return storage;
		};
		QFN.merge = function(silent, callback) {
			QFN.fapi('https://meta.wikimedia.org/w/api.php', 'get', {
				action: 'query',
				prop: ['revisions'],
				titles: [jsonwp],
				rvprop: ['content'],
				rvslots: 'main',
				rvlimit: 1,
				format: 'json',
				formatversion: 2
			}, function(response) {
				var content = {};
				
				if (!response.query.pages[0].missing) {
					try {
						content = JSON.parse(response.query.pages[0].revisions[0].slots.main.content);
					} catch (e) {
						QFN.warn('Invalid JSON.');
						return;
					}
				}
				content = Object.assign(QFN.storage(), content);
				QFN.store(content, silent = true);
				
				if (callback) callback();
			});
		};
		QFN.content = function() {
			var storage = QFN.storage();
			storage[$('#qfn-template-name').val()] = $('#qfn-code').textSelection('getContents');
			return storage;
		};
		QFN.store = function(content, silent = false) {
			localStorage.setItem('QuickFunction-templates', QFN.fmt(content || QFN.content(), null, null));
			if (!silent) {
				QFN.parse(localStorage.getItem('QuickFunction-templates'), function(response) {
					QFN.info(response.parse.text, 'Stored successfully.');
				});
			}
		};
		QFN.save = function() {
			var url = 'https://meta.wikimedia.org/w/api.php';
			QFN.fapi(url, 'get', {
				action: 'query',
				meta: 'tokens',
				type: 'csrf',
				format: 'json',
				formatversion: 2
			}, function(response) {
				var token = response.query.tokens.csrftoken;
				QFN.fapi(url, 'post', {
					action: 'edit',
					title: jsonwp,
					text: QFN.fmt(QFN.content()),
					summary: '/* Automatic edit */ Saving personal functions' + advert,
					token: token,
					format: 'json',
					formatversion: 2
				}, function(response) {
					QFN.parse(response, function(parsed) {
						QFN.info(parsed.parse.text, 'Edited successfully:');
						QFN.update();
					});
				});
			});
		};
		QFN.update = function() {
			$('#qfn-template-names').html(function() {
				var r = [];
				var list = QFN.storage();
				
				r.push(
					$('<option>')
						.data('code', '')
						.text('Blank')
						.prop('selected', true)
				);
				for (let i in list) {
					r.push(
						$('<option>')
							.data('code', list[i])
							.attr('value', list[i])
							.text(i)
					);
				}
			
				return r;
			});
		};
		QFN.delete = function() {
			var storage = QFN.storage();
			var name = $('#qfn-template-names').find('option:selected').text();
			if (name !== 'Blank') {
				delete storage[name];
			} else {
				QFN.warn('"Blank" cannot be deleted.');
				return;
			}
			QFN.store(QFN.fmt(storage, null, null), false);
		};
		
		// Dialog content.
		$('<div>').attr({
			id: 'qfn-dialog'
		}).append(
			$('<div>').attr({
				'id': 'qfn-wrapper'
			}).css({
				'display': 'flex',
				'flex-direction': 'column'
			}).append(
				$('<input>').attr({
					type: 'hidden'
				}),
				$('<div>').attr({
					'id': 'qfn-log'
				}).css({
					'margin': '0.3em',
					'border': '1px solid #C8CCD1',
					'resize': 'vertical',
					'height': '7.5em',
					'min-height': '7.5em',
					'overflow-y': 'auto',
					'padding': '1em'
				}),
				$('<div>').css({
					'display': 'flex'
				}).append(
					$('<button>').attr({
						id: 'qfn-log-clear'
					}).css({
						'margin': '0.3em'
					}).text(
						'Clear'
					).button(),
					$('<button>').attr({
						id: 'qfn-log-toggle'
					}).css({
						'margin': '0.3em'
					}).text(
						'Hide log'
					).button(),
					$('<label>').css({
						'display': 'flex',
						'align-items': 'center',
						'gap': '0.3em',
						'flex-grow': '4',
						'margin': '0.3em'
					}).append(
						$('<span>').text('Name:'),
						$('<input>').attr({
							type: 'text',
							autocomplete: 'off',
							id: 'qfn-template-name'
						}).css({
							'flex-grow': '4',
							'padding': '0.3em'
						})
					),
					$('<button>').css({
						'margin': '0.3em'
					}).attr({
						'id': 'qfn-template-save',
						'title': 'Save content to localStorage'
					}).text(
						'Save'
					).button(),
					$('<select>').attr({
						id: 'qfn-template-names'
					}).css({
						'margin': '0.3em',
						'padding': '0.3em'
					}).append(
						$('<option>').text(
							'Loading...'
						).prop({
							'selected': true,
							'hidden': true
						})
					),
					$('<button>').css({
						'margin': '0.3em'
					}).attr({
						'id': 'qfn-template-delete',
						'title': 'Delete this template from localStorage'
					}).text(
						'Delete'
					).button()
				),
				$('<div>').attr({
					'class': 'qfn-pseudocode'
				}).css({
					'margin': '0.5em 0.3em',
					'font-family': '"Consolas", monospace'
				}).append(
					$('<span>').append(
						'(',
						$('<strong>').css('color', 'green').text('function'),
						'() {'
					)
				),
				$('<div>').attr('id', 'qfn-code-wrapper'),
				$('<div>').attr({
					'class': 'qfn-pseudocode'
				}).css({
					'margin': '0.5em 0.3em',
					'font-family': '"Consolas", monospace'
				}).append(
					$('<span>').append(
						'}',
						')();'
					)
				)
			)
		).appendTo(document.body);
		
		// Start dialog.
		$('#qfn-dialog').dialog({
			autoOpen: true,
			width: '80%',
			'min-width': '60%',
			height: 600,
			'min-height': 400,
			title: 'QuickFunction',
			buttons: [
				// {
				// 	text: 'Stop',
				// 	id: 'qfn-stop',
				// 	disabled: true,
				// 	click: function() {
				// 		window.QuickFunctionRunning.resolve();
				// 		delete window.QuickFunctionRunning;
				// 		QFN.buttontoggle();
				// 	}
				// },
				{
					text: 'Reload this dialog',
					id: 'qfn-reload',
					click: function() {
						if (!confirm('Do you really want to reload?')) return;
						$(this).parent().remove();
						$(this).remove();
						$('#t-qfn').click();
					}
				},
				{
					text: 'Run!',
					id: 'qfn-run',
					click: function() {
						QFN.buttontoggle();
			
						if (mw.loader.getState('jquery.textSelection') === 'ready') {
							mw.loader.state({
								'jquery.textSelection': 'loaded'
							});
						}
						mw.loader.load('jquery.textSelection');
						
						if (!$('#qfn-code').textSelection('getContents')) {
							QFN.error('No content found.');
							return;
						}
						
						// window.QuickFunctionRunning = new $.Deferred();
						var code = $('#qfn-code').textSelection('getContents');
						
						try {
							$.when.apply($, window.QuickFunctionRunning, (new Function(code))()).then(function() {
								QFN.buttontoggle();
							});
						} catch (e) {
							QFN.error(e);
							QFN.buttontoggle();
						}
					}
				}
			]
		});
		
		// Get storage.
		QFN.merge(true, QFN.update);
		
		// Coding area. For attribution: [[:en:User:BrandonXLF/SVGEditor.js]].
		mw.loader.using(['oojs-ui', 'ext.wikiEditor']).then(function() {
			var code = new OO.ui.MultilineTextInputWidget({
				rows: 17,
				name: 'wpTextbox1', // qfn-code', // Go ask codeEditor for reason.
				id: 'qfn-code-wrapper'
			});
			$('#wpTextbox1', '#content').attr({
				name: 'wpTextbox1-temp',
				id: 'wpTextbox1-temp'
			});
			var $container = $('#qfn-code-wrapper');
			code.$element.css('max-width', 'unset');
			// code.$input.attr('id', 'qfn-code');
			code.$input.attr('id', 'wpTextbox1');

			mw.config.set('wgCodeEditorCurrentLanguage', 'javascript');
			$container.replaceWith(code.$element);
			
			mw.addWikiEditor(code.$input);
			
			if (mw.loader.getState('ext.codeEditor') === 'ready') {
				mw.loader.state({'ext.codeEditor': 'loaded'});
			}
			
			// From /w/load.php?modules=ext.codeEditor
			// mw.loader.implement(['ext.codeEditor@v0khv'], function($, jQuery, require, module) {
			// 	$('#qfn-code').parent().prop('dir', 'ltr');
			// 	$('#qfn-code').wikiEditor('addModule', 'codeEditor');
			// 	$('#qfn-code').on('wikiEditor-toolbar-doneInitialSections', function () {
			// 		$('#qfn-code').data('wikiEditor-context').fn.codeEditorMonitorFragment();
			// 	});
			// });
			mw.loader.using(['ext.codeEditor'], function() {
				window.QFNcodeEditorloaded = true;
				// Loaded. Now return my id.
				$('body').one('mouseover', function() {
					if (window.QFNcodeEditorloaded) {
						$('#wpTextbox1', '#qfn-dialog').attr('id', 'qfn-code').removeAttr('name');
						$('#wpTextbox1-temp[name="wpTextbox1-temp"]').attr({
							name: 'wpTextbox1',
							id: 'wpTextbox1'
						});
						delete window.QFNcodeEditorloaded;
					}
				});
			});
		});
		
		// Event handlers.
		$('#qfn-log-clear').click(function() {
			$('#qfn-log').empty();
		});
		$('#qfn-log-toggle').click(function() {
			var b = $('#qfn-log').is(':visible');
			if (b) {
				$('#qfn-log').css({
					'min-height': b ? 'initial' : '7.5em'
				}).slideToggle(500, 'swing');
			} else {
				$('#qfn-log').slideToggle(500, 'swing', function() {
					$(this).css({
						'min-height': b ? 'initial' : '7.5em'
					});
				});
			}
			$(this).children('span').text(
				b ? 'Show log' : 'Hide log'
			);
		});
		
		// For attribution: //stackoverflow.com/a/469362
		[
			'input', 'keydown', 'keyup',
			'mousedown', 'mouseup', 'select',
			'contextmenu', 'drop', 'focusout'
		].forEach(function(event) {
			$('#qfn-template-name').on(event, function(e) {
				if ((function(v) {
					return !/^Blank$/.test(v.trim());
				})(this.value)) {
					if (['keydown', 'mousedown', 'focusout'].includes(e.type)) {
						this.setCustomValidity('');
					}
					this.oldValue = this.value;
					this.oldSelectionStart = this.selectionStart;
					this.oldSelectionEnd = this.selectionEnd;
				} else if (this.hasOwnProperty('oldValue')) {
					this.setCustomValidity('Template names cannot be "Blank".');
					this.reportValidity();
					this.value = this.oldValue;
					this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
				} else {
					this.value = '';
				}
			})
		});
		
		$('#qfn-template-names').change(function() {
			$('#qfn-template-name').val(
				$(this).find('option:selected').text() !== 'Blank' && 
				$(this).find('option:selected').text() ||
				''
			);
			$('#qfn-code').textSelection('setContents', $(this).find('option:selected').data('code'));
		});
		$('#qfn-template-delete').click(function() {
			QFN.delete();
			QFN.save();
		});
		$('#qfn-template-save').click(function() {
			QFN.store(QFN.content(), true);
			QFN.save();
		});
	});
});