מדיה ויקי:Gadget-TemplateParamWizard.js

הערה: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.

  • פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
  • גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
  • אינטרנט אקספלורר / אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים Ctrl-F5.
  • אופרה: ללחוץ על Ctrl-F5.
//Template parameters wizard
//Written by [[User:קיפודנחש]]
"use strict";
if(($.inArray(mw.config.get('wgAction'), ['edit', 'submit'])+1) && ( !$('#wpTextbox1').prop( 'readonly' ) ) )
$(function() {
	// template parameter is an object with the following fields:
	// desc: desciption string
	// defval: default value (optional)
	// options: object with optional fields:
	//// multiline: number of lines
	//// depends: another field's name
	//// required: boolean
	//// date: use JS date widget
	//// choices: array of legal values for the field
	//// extended: this field will not show on initial screen, and will appear once the user selects "show all fields"

	var
	// templateParams is keyed by paramName.
		templateParams,
		paramsOrder,
	// which template are we working on
		template,
	// array of pairs - [paramName, inputField]
		dialogFields,
	// table rows keyed by paramName
		rowsBypName,
	// the fields, keyed by paramName
		fieldsBypName,
	// boolean, indicating we did not find "Parameters" page, so the parameters are extracted from template page itself.
		rawTemplate,
		rtl = $('body').is('.rtl'),
	// test to see if a string contains wikiCode and hence needs parsing, or cen be used as is.
		wikiCodeFinder = /[\[\]\{\}\<\>]/,
		globalExplanation = '',
		extendedParamCssRule,
		anyExtended = false,
		localStorageKey = 'templateParamWizard',
		emptiesKey = 'writeEmpties';

	function addParam(name) {
		if ($.inArray(name, paramsOrder) == -1)
			paramsOrder.push(name);
	}

	function paramsFromSelection() {
		var selection = $("#wpTextbox1").textSelection('getSelection').replace(/^\s*\{\{|\}\}\s*$/g, ''); //scrap the first {{ and last }}
		var specials = [];
		while (true) { //extract inner links, inner templates and inner params - we don't want to sptit those.
			var match = selection.match(/(\{\{[^\{\}\]\[]*\}\}|\[\[[^\{\}\]\[]*\]\]|\[[^\{\}\]\[]*\])/);
			if (! match || ! match.length)
				break;
			specials.push(match[0]);
			selection = selection.replace(match[0], "\0" + specials.length + "\0");
		}
		var params = selection.split(/\s*\|\s*/);
		for (var i in params) {
			var param = params[i];
			while (true) {
				var match = param.match(/\0(\d+)\0/);
				if (! match || ! match.length)
					break;
				param = param.replace(match[0], specials[parseInt(match[1], 10)-1]);
			}
			var paramPair = param.split("=");
			var name = $.trim(paramPair.shift());
			if (name && paramPair.length) {
				templateParams[name] = templateParams[name] || {options: {notInParamPage: 1}};
				addParam(name);
				$.extend(templateParams[name].options, {'defval': paramPair.join('=')});
			}
		}
	}

	function buildParamsRaw(data) {
		var
			paramExtractor = /{{3,}(.*?)[<|}]/mg,
			m;
		while (m = paramExtractor.exec(data)) {
			var paramName = $.trim(m[1]);
			templateParams[paramName] = {desc: '', options: {multiline: 5}};
			addParam(paramName);
		}
	}

	function buildParams(data) {
		var
			lines = data.split("\n"),
			line;

		function extractGlobalExplanation() {
			line = line.replace(/[!\|][^\|]*\|/, '');
			if (wikiCodeFinder.test(line))
				$.post(
					mw.util.wikiScript('api'),
					{action: 'parse', text: line, disablepp: 1, format: 'json'},
					function(data) {
						var html = data.parse.text['*'];
						globalExplanation = html;
						$('#tpw_globalExplanation').html(html).find('a').attr({target: '_blank'});
					}
				);
			else
				globalExplanation = line;
		}

		while (lines && lines.length) {
			line = lines.shift();
				if (!(/^\|-/.test(line))) // look for |- this is wikitext for table row.
					continue;
			line = lines.shift();
			if (line.indexOf('globalExplanation') + 1) {
				extractGlobalExplanation();
				continue;
			}
			if (! line || ! (/^\|/.test(line))) //wikitext for column
				continue;
			line = line.substr(1); // get rid of the leading |
			var fields = line.split('||');
			if (fields.length < 2)
				continue;
			var name = $.trim(fields[0]);
			if (! name)
				continue;
			var desc = $.trim(fields[1]);
			var pAttribs = {desc: desc};
			if (fields.length > 2)
				pAttribs.options = analyzeOptions($.trim(fields[2]));

			templateParams[name] = pAttribs;
			addParam(name);

		}
	}

	function analyzeOptions(str) {
		var res = {},
			avail = ['multiline', 'required', 'depends', 'defval', 'choices', 'date', 'extended'], // maybe we'll have more in the future
			tavail = $.map(avail, i18n),
			options = str.split(/\s*;\s*/);
		for (var i in options) {
			var option = options[i].split(/\s*=\s*/);
			var ind = $.inArray(option[0], tavail);
			if (ind + 1)
				res[avail[ind]] = option.length > 1 ? option[1] : true;
		}
		anyExtended = anyExtended || res.extended;
		return res;
	}

	function createWikiCode() {
		var par = [template],
			delim = $('#oneLineTemplate').prop('checked') ? '' : '\n',
			createEmpties = $('#createEmpties').prop('checked');
		for (var i in dialogFields) {
			var
				field = dialogFields[i],
				name = $.trim(field[0]),
				f = field[1],
				hidden = f.parents('.tpw_hidden').length,
				val = $.trim(f.val());
			if (!createEmpties && val === "" && f.attr('type') != 'checkbox') 
				continue;//skip parameters with no value
			if (f.attr('type') == 'checkbox' && ! f.prop('checked'))
				val = "";
			par.push(name + '=' + val);
		}
		return "{{" + par.join(delim + "|") + delim + "}}";
	}

	function showPreview() {
		var temp = createWikiCode();
		$.post(mw.util.wikiScript('api'),
			{action: 'parse',
				title: mw.config.get('wgPageName'),
				prop: 'text',
				text: temp,
				format: 'json'
			},
			function(data) {
				if (data && data.parse && data.parse.text) {
					var buttons = [{text: i18n('close'), click: function() {$(this).dialog('close');}}],
						div = $('<div>').html(data.parse.text['*']);
					$('a', div).attr('target', '_blank'); // we don't want people to click on links in preview - they'll lose their work.
					$('<div>')
						.dialog(
							{title: i18n('preview'),
							modal: true,
							position: [60, 60],
							buttons: buttons})
						.append(div);
					circumventRtlBug();
				}
			});
	}

	function circumventRtlBug() {
		if (rtl)
			$('.ui-dialog-buttonpane button').css({float: 'right'}); // jQuery has problems with rtl dialogs + ie is braindamaged.
	}

	function i18n(key, param) {
		switch (mw.config.get('wgUserLanguage')) {
			case 'ar':
				switch (key) {
					case 'explain': return rawTemplate
						? 'قالب "' + template + '" ليس له صفحة وسائط فرعية، لذلك فما من وصف لمعطياته.'
						: 'الوسائط الضرورية محددة بالأحمر والبقية اختيارية.';
					case 'wizard dialog title': return 'وسائط ' + '<a href="' + mw.util.getUrl('قالب:' + template) + '" target="_blank">' + 'قالب:' + template + '</a>';
					case 'ok': return 'موافقة';
					case 'cancel': return 'إلغاء';
					case 'params subpage': return 'وسائط';
					case 'preview': return 'معاينة';
					case 'options select': return 'اختر معطى';
					case 'multiline': return 'عدد صفوف';
					case 'close': return 'أغلق';
					case 'required': return 'ضروري';
					case 'depends': return 'يلزمه';
					case 'defval': return 'غيابي';
					case 'choices': return 'خيارات';
					case 'date': return 'تاريخ';
					case 'extended': return 'Extended';
					case 'button hint': return 'معالج وسائط القالب';
					case 'able templates category name': return 'قوالب صالحة لمعالج وسائط القالب';
					case 'template selector title': return 'اكتب اسم القالب:';
					case 'notInParamPage': return 'وسيط "' + param + '" ليس من وسائط القالب';
					case 'editParamPage': return 'عدل صفحة الوسائط';
					case 'unknown error': return 'وقع خطأ.\n' + param;
					case 'please select template': return 'اسم القالب';
					case 'oneliner': return 'اجعله في صف واحد';
					case 'createempties': return 'Write empty parameters to page';
					case 'dateFormat': return 'd MM yy';
					case 'extended labels': return 'Show all parameters';
				}
			case 'he':
				switch (key) {
					case 'explain': return rawTemplate
						? 'לתבנית "' + template + '" אין דף פרמטרים, ולכן לשדות אין תיאור.'
						: 'השדות המסומנים באדום הם חובה, השאר אופציונליים.';
					case 'wizard dialog title': return 'מילוי הפרמטרים עבור ' + '<a href="' + mw.util.getUrl('תבנית:' + template) + '" target="_blank">' + 'תבנית:' + template + '</a>';
					case 'ok': return 'אישור';
					case 'cancel': return 'ביטול';
					case 'params subpage': return 'פרמטרים';
					case 'preview': return 'תצוגה מקדימה';
					case 'options select': return 'בחרו ערך מהרשימה';
					case 'multiline': return 'מספר שורות';
					case 'close': return 'סגור';
					case 'required': return 'שדה חובה';
					case 'depends': return 'תלוי';
					case 'defval': return 'ברירת מחדל';
					case 'choices': return 'אפשרויות';
					case 'date': return 'תאריך';
					case 'extended': return 'משני';
					case 'button hint': return 'אשף מילוי תבניות';
					case 'able templates category name': return 'תבניות הנתמכות על ידי אשף התבניות';
					case 'template selector title': return 'אנא הזינו את שם התבנית:';
					case 'notInParamPage': return 'השדה "' + param + '" לא מופיע ברשימת הפרמטרים של התבנית';
					case 'editParamPage': return 'לעריכת דף הפרמטרים';
					case 'unknown error': return 'טעות בהפעלת האשף.\n' + param;
					case 'please select template': return 'שם התבנית';
					case 'oneliner': return 'תבנית בשורה אחת';
					case 'createempties': return 'רשום שדות ריקים';
					case 'dateFormat': return 'd בMM yy';
					case 'extended labels': return 'הראה את כל הפרמטרים';
				}
			default:
				switch (key) {
					case 'explain': return 'fields with red border are required, the rest are optional';
					case 'wizard dialog title': return 'Set up parameters for template: ' + template;
					case 'ok': return 'OK';
					case 'cancel': return 'Cancel'
					case 'params subpage': return 'Parameters';
					case 'preview': return 'Preview';
					case 'options select': return 'Select one:';
					case 'multiline': return 'Multiline';
					case 'close': return 'Close';
					case 'required': return 'Required';
					case 'depends': return 'Depends on';
					case 'defval': return 'Default';
					case 'choices': return 'Choices';
					case 'date': return 'Date';
					case 'extended': return 'Extended';
					case 'button hint': return 'Template parameters wizard';
					case 'able templates category name': throw('Must define category name for wizard-capable templates');
					case 'template selector title': return 'Please enter the template name';
					case 'notInParamPage': return 'field "' + param + '" does not appear in the template\'s parameters list';
					case 'editParamPage': return 'Edit paramters page';
					case 'unknown error': return 'Error occured: \n' + param;
					case 'please select template': return 'Please enter template name';
					case 'oneliner': return 'Single-line template';
					case 'createempties': return 'Write empty parameters to page';
					case 'dateFormat': return 'MM d, yy';
					case 'extended labels': return 'Show all parameters';
				}
		}
		return key;
	}

	function paramPage() {return mw.config.get('wgFormattedNamespaces')[10] + ':' + $.trim(template) + '/' + i18n('params subpage');}

	function templatePage() {return mw.config.get('wgFormattedNamespaces')[10] + ':' + $.trim(template);}

	function updateRawPreview(){
		var canOK = 'enable';
		for (var i in dialogFields) {
			var df = dialogFields[i][1];
			var opts = df.data('options');
			if (opts && opts.required && $.trim(df.val()).length == 0)
				canOK = 'disable';
			if (opts && opts.depends) {
				var dep = fieldsBypName[opts.depends];
				var depEmpty = (dep && dep.val() && $.trim(dep.val())) ? false : true;
				var row = rowsBypName[df.data('paramName')];
				if (row)
					row.toggleClass('tpw_hidden', depEmpty);
			}
		}
		$(".ui-dialog-buttonpane button:contains('" + i18n('ok') + "')").button(canOK);
		$('#tpw_preview').text(createWikiCode());
		localStorage.setItem(localStorageKey + '.' + emptiesKey, $('#createEmpties').prop('checked'))
	}

	function createInputField(paramName) {
		var options = templateParams[paramName].options || {},
			f,
			checkbox = false;

		if (options.choices) {
			var choices = options.choices.split(/\s*,\s*/);
			if (choices.length > 1) {
				f = $('<select>').append($('<option>', {text: i18n('options select'), value: ''}));
				for (var i in choices)
					f.append($('<option>', {text: choices[i], value: choices[i]}));
			}
			else {
				checkbox = true;
				f = $('<input>', {type: 'checkbox', value: choices[0], text: choices[0]})
					.css({float: rtl ? 'right' : 'left'})
				f.prop('checked', options.defval && options.defval == choices[0]);
			}
		}
		else if (options.multiline) {
			var rows = options.multiline;
			f = $('<textarea>', {rows: 1})
				.data({dispRows: isNaN(parseInt(rows)) ? 5 : rows})
				.focus(function(){this.rows = $(this).data('dispRows');})
				.blur(function(){this.rows = 1});
		}
		else
			f = $('<input>', {type: 'text'});

		if (!checkbox && f.autoCompleteWikiText) // teach the controls to autocomplete.
			f.autoCompleteWikiText({positionMy: rtl ? "left top" : "right top"});

		f.css({width: checkbox ? '1em' : '28em'})
			.data({paramName: paramName, options: options})
			.bind('paste cut drop input change', updateRawPreview);

		if (options.defval)
			f.val(options.defval);

		if (options.required)
			f.addClass('tpw_required').css({border: '1px red solid'});

		if (options.date)
			f.datepicker({dateFormat: typeof options.date  == "string" ? options.date : i18n('dateFormat')});
		return f;
	}

	var
		timer = null,
		lastVisited = $('<a>');

	function enterTipsy() {
		clearTimeout(timer);
		$(this).attr('inside', 1);
	}

	function leaveTipsy() {
		var $this = $(this);
		if ($this.attr('master') || $this.attr('inside')) {
			$this.attr('inside', '');
			timer = setTimeout(function(){lastVisited.tipsy('hide');}, 500);
		}
	}

	function tipsyContent() {
		var
			paramName = $(this).text(),
			def = templateParams[paramName],
			desc = def.desc || '';
		if (def.htmlDesc)
			return def.htmlDesc;
		if (def.options.notInParamPage)
			return $('<div>')
				.append(i18n('notInParamPage', paramName) + '<br />')
				.append($('<a>', {href: mw.util.getUrl(paramPage()) + '?action=edit', target: '_blank', text: i18n('editParamPage')}))
				.html();
		if (wikiCodeFinder.test(desc)) // does it need parsing?
			$.ajax({
				url: mw.util.wikiScript('api'),
				async: false,
				type: 'post',
				data: {action: 'parse', text: desc, disablepp: 1, format: 'json'}, // parse it.
				success: function(data) {
					var div = $('<div>').html(data.parse.text['*']);
					$('a', div).attr({target: '_blank'});
					def.htmlDesc = div.html();
				}
			});
		else
			def.htmlDesc = desc;
		return def.htmlDesc;
	}

	function addRow(paramName, table) {
		var
			def = templateParams[paramName],
			inputField = createInputField(paramName),
			nameColor = def.desc ? 'blue' : (def.options.notInParamPage ? 'red' : 'black'),
			tr = $('<tr>')
				.append(
					$('<td>', {width: 120})
					.css({fontWeight: 'bold', color: nameColor})
					.text(paramName)
					.tipsy({html: true, trigger: 'manual', title: tipsyContent})
					.mouseenter(function() {
						clearTimeout(timer);
						$('.tipsy').remove();
						lastVisited = $(this);
						lastVisited.tipsy('show');
					})
					.mouseleave(leaveTipsy)
					.attr('master', 'true')
				)
				.append($('<td>').css({width: '30em'}).append(inputField));
		dialogFields.push([paramName, inputField]);
		if (def.options.extended)
			tr.addClass('tpw_extended');
		table.append(tr);
		rowsBypName[paramName] = tr;
		fieldsBypName[paramName] = inputField;
	}

	function injectResults() {
		$("#wpTextbox1").textSelection('encapsulateSelection', {replace: true, peri: createWikiCode()});
	}

	function createExtendedCheckBox() {
		return $('<p>')
				.text(i18n('extended labels'))
				.append($('<input>', {type: 'checkbox'})
					.change(function() {
						extendedParamCssRule.disabled = $(this).prop('checked');
					})
				);
	}

	function buildDialog(data) {
		$('.tpw_disposable').remove();
		if (rawTemplate)
			buildParamsRaw(data)
		else
			buildParams(data);
		paramsFromSelection();
		var	table = $('<table>');
		var dialog = $('<div>', {'class': 'tpw_disposable'})
			.dialog({height: 'auto',
					title: i18n('wizard dialog title', template),
					width: 'auto',
					overflow: 'auto',
					position: [$('body').width() * 0.2, $('body').height() * 0.1],
					open: function() {$(this).css({'max-height': Math.round($('body').height() * 0.7)});}
			})
			.append($('<div>', {id: 'tpw_globalExplanation'}).html(globalExplanation))
			.append($('<p>').html(i18n('explain')))
			.append(anyExtended ? createExtendedCheckBox() : '')
			.append(table)
			.append($('<p>')
				.append(i18n('oneliner'))
				.append($('<input>', {type:'checkbox', id:'oneLineTemplate'}).change(updateRawPreview)
				)
			)
			.append($('<p>')
				.append(i18n('createempties'))
				.append($('<input>', {type:'checkbox', id:'createEmpties'})
					.change(updateRawPreview)
					.prop('checked', localStorage.getItem(localStorageKey + '.' + emptiesKey) == "true")
				)
			)
			.append($('<pre>', {id: 'tpw_preview'})
				.css({backgroundColor: "lightGreen", maxWidth: '40em', maxHeight: '8em', overflow: 'auto'}));

		while (paramsOrder.length)
			addRow(paramsOrder.shift(), table);

		var buttons = {}; // we need to do it this way, because with literal object, the keys must be literal.
		buttons[i18n('ok')] = function() {injectResults(); dialog.dialog('close'); };
		buttons[i18n('cancel')] = function() {dialog.dialog('close');}
		buttons[i18n('preview')] = showPreview;
		dialog.dialog('option', 'buttons', buttons);
		circumventRtlBug();
		updateRawPreview();
		$('.tipsy').hover(enterTipsy, leaveTipsy);
	}

	function init() {
		template = null;
		templateParams = {};
		paramsOrder = [];
		dialogFields = [];
		rowsBypName = {};
		fieldsBypName = {};
		mw.util.addCSS(".tpw_hidden{display:none;}");
		anyExtended = false;
		extendedParamCssRule = extendedParamCssRule || mw.util.addCSS(".tpw_extended{display:none;}");
	}

	function reportError(a,b,error) {
		var key;
		if (typeof console != 'undefined') {
			for (key in a)
				if (typeof a[key] != 'function')
					console.log(key + '=>' + a[key]);
			console.log(b);
			console.log(error);
		}
		alert(i18n('unknown error', error));
	}

	function pickTemplate(item) {
		function okButtonPressed(e, ui) {
			template = ui ? ui.item.value : selector.val();
			fireDialog();
			templateSelector.dialog("close");
		}
		var selector = $('<input>')
			.css({width: '28em'})
			.autocomplete({
				source: function(request, response) {
					$.getJSON(
						mw.util.wikiScript('api'),
						{action:'opensearch', search: request.term, namespace: 10},
						function(data){
							if(data[1])
								response($(data[1]).map(function(index,item){return item.replace(/.*:/, '');}));
						}
					);
				},
				select: okButtonPressed
			});
		var templateSelector = $('<div>').dialog({
			title: i18n('template selector title'),
			height: 'auto',
			width: 'auto',
			modal: true,
			buttons: [
				{text: i18n('ok'), click: okButtonPressed},
				{text: i18n('cancel'), click: function(){templateSelector.dialog("close")}}
			]
		}).append(selector);
		circumventRtlBug();
		selector.focus();
	}

	function fireDialog() {
		rawTemplate = false;
		$.ajax({
			url: mw.util.wikiScript(),
			data: {title: paramPage(), action: 'raw'},
			dataType: 'text',
			success: buildDialog,
			cache: false,
			error: function() {
				rawTemplate = true;
				$.ajax({
					url: mw.util.wikiScript(),
					data: {title: templatePage(), action: 'raw'},
					dataType: 'text',
					success: buildDialog,
					error: reportError
				});
			}
		});
	}

	function doIt() {
		mw.loader.using(['jquery.ui.widget','jquery.tipsy','jquery.textSelection', 'jquery.ui.autocomplete', 'jquery.ui.dialog', 'jquery.ui.datepicker'], function() {
			init();
			var match = $("#wpTextbox1").textSelection('getSelection').match(/^\{\{([^|}]*)/);
			template = match ? $.trim(match[1]) : null;
			if (template)
				fireDialog();
			else
				pickTemplate();
		});
	}

	if (mw.user.options.get('usebetatoolbar'))
		mw.loader.using(['jquery.wikiEditor','jquery.wikiEditor.toolbar.config','ext.wikiEditor.toolbar'], function() {
			if(typeof $.wikiEditor != 'undefined' && $.wikiEditor.isSupported())
				$('#wpTextbox1').wikiEditor('addToToolbar', {
					section: 'advanced',
					groups: {
						'wizards': {
							tools: {
								'linkTemplatewizard': {
									label: i18n('button hint'),
									type: 'button',
									icon: '//upload.wikimedia.org/wikipedia/commons/d/dd/Vector_toolbar_template_button.png',
									action: {type: 'callback', execute: doIt}
								}
							}
						}
					}
				});
			});
		else
			$('div #toolbar').append( // "old style"
				$('<img>', {src: '//upload.wikimedia.org/wikipedia/commons/e/eb/Button_plantilla.png', title: i18n('button hint'), 'class': 'mw-toolbar-editbutton'})
				.css({cursor: 'pointer'})
				.click(doIt)
			);
});