User:KokoroSenshi/common.js: Difference between revisions

From Zelda Wiki, the Zelda encyclopedia
Jump to navigation Jump to search
m (Attempt to enable a parameter dependent header (or later, footer) on the dropdown list)
m (Tidied somewhat)
Line 1: Line 1:
/**
/**
  * Autocomplete/Dropdown list
  * WikitextAutocompleter
* Provides Autocomplete (a dropdown list) for wiki editing such as for
* completing templates
  * Uses https://yuku-t.com/textcomplete/
  * Uses https://yuku-t.com/textcomplete/
  *
  *
Line 6: Line 8:
  * - Won't work with CodeEditor since it doesn't use a textarea but that's fine
  * - Won't work with CodeEditor since it doesn't use a textarea but that's fine
  *  since CodeEditor won't be used for wikitext
  *  since CodeEditor won't be used for wikitext
* - $.getScript() is used since mw.loader.load() doesn't wait for the code to load
*  mw.loader.load('https://unpkg.com/textcomplete@0.13.1/dist/textcomplete.min.js');
  * Bugs:
  * Bugs:
  *  
  * -
  */
  */


Line 19: Line 19:
autoComplete = {}; // Contains supporting variables, e.g. initialismsArray, createTemplateStrategy()
autoComplete = {}; // Contains supporting variables, e.g. initialismsArray, createTemplateStrategy()
/** */
/** Supporting definitions */
/* Array of Standard Initialisms */
autoComplete.initialismsArray = [];
var readInitialisms = function(callback) {
$.get( "https://zelda.gamepedia.com/Template:Zelda?action=raw", function( data ) {
var initialisms = [];
data.split("<noinclude>")[1]
.match(/\|\|[a-zA-Z0-9-+& ]*\n/g)
.forEach(function(value, index){
var name = value.slice(2).trim();
initialisms.push(name);
});
autoComplete.initialismsArray = initialisms; // Leave autoComplete.initialismsArray empty until fully filled
if (typeof callback === "function") callback();
});
};
/* Call it, optionally with a callback if needed */
readInitialisms(); // No specific need for the callback yet?
/** Template parameter definitions */
/* Template:Color */
templates.Color = {};
templates.Color = {};
$.get( "https://zelda.gamepedia.com/Template:Color?action=raw", function( data ) {
$.get( "https://zelda.gamepedia.com/Template:Color?action=raw", function( data ) {
Line 36: Line 58:
});
});
/* Template:KSTest */
templates.KSTest = {
templates.KSTest = {
"1": ["testparam1value1","testparam1value2","testparam1value3"],
"1": ["testparam1value1","testparam1value2","testparam1value3"],
Line 42: Line 65:
};
};
/* Template:Icon */
templates.Icon = {};
templates.Icon = {};
$.get( "https://zelda.gamepedia.com/Template:Icon?action=raw", function( data ) {
$.get( "https://zelda.gamepedia.com/Template:Icon?action=raw", function( data ) {
Line 57: Line 81:
});
});
autoComplete.initialismsArray = [];
/* Template:Exp Game */
var readInitialisms = function(callback) {
$.get( "https://zelda.gamepedia.com/Template:Zelda?action=raw", function( data ) {
var initialisms = [];
data.split("<noinclude>")[1]
.match(/\|\|[a-zA-Z0-9-+& ]*\n/g)
.forEach(function(value, index){
var name = value.slice(2).trim();
initialisms.push(name);
});
autoComplete.initialismsArray = initialisms; // Leave autoComplete.initialismsArray empty until fully filled
if (typeof callback === "function") callback();
});
};
/** Initialisms-dependent templates: */
readInitialisms();
/* // Won't really need this callback version of the function call?
readInitialisms(function() {
// Exp Game uses a function, which will get the latest definition of initialismsArray, so isn't needed in this callback
});
*/
templates["Exp Game"] = {};
templates["Exp Game"] = {};
templates["Exp Game"].getParams = function(paramNum) {
templates["Exp Game"].getParams = function(paramNum) {
if (paramNum > 0) // No need to check paramNum, really...
return autoComplete.initialismsArray; // Assumes an integer paramNum > 0
return autoComplete.initialismsArray;
else
return [];
};
};
// Getting a list of every template in the wiki is non-trivial...
// Getting a list of every template in the wiki is non-trivial... too many for a single api call
/** Registering strategies */
autoComplete.currentHeader = ""; // An attempt to generate a dropdown menu header to describe the parameter in some way, to accompany choices, or when there are no preset choices.
autoComplete.currentHeader = ""; // An attempt to generate a dropdown menu header to describe the parameter in some way, to accompany choices, or when there are no preset choices.
Line 94: Line 96:
console.log( "Loading textcomplete..." );
console.log( "Loading textcomplete..." );
$.getScript( "https://unpkg.com/textcomplete/dist/textcomplete.min.js", function( data, textStatus, jqxhr ) {
$.getScript( "https://unpkg.com/textcomplete/dist/textcomplete.min.js", function( data, textStatus, jqxhr ) {
console.log( [ data, textStatus, jqxhr.status ] ); // Data returned, Success, 200
//console.log( [ data, textStatus, jqxhr.status ] ); // Data returned, Success, 200
console.log( "Loaded textcomplete. (Warning: May not be executed yet)" );
console.log( "Loaded textcomplete. (Warning: May not be executed yet)" );
// Textarea object: https://github.com/yuku-t/textcomplete/issues/114#issuecomment-318352383
// Textarea object: https://github.com/yuku-t/textcomplete/issues/114#issuecomment-318352383
Textarea = Textcomplete.editors.Textarea; // Global variable
Textarea = Textcomplete.editors.Textarea; // Global Variable
autoComplete.registerStrategies();
autoComplete.registerStrategies();
});
});
};
};
/* Note: The param arrays need ont exist before strategy is registered */
/* Note: The param arrays need not exist before strategy is registered */
autoComplete.registerStrategies = function() {
autoComplete.registerStrategies = function() {
var editor = new Textarea(document.getElementById("wpTextbox1"))
var editor = new Textarea(document.getElementById("wpTextbox1"))
Line 107: Line 110:
dropdown: {
dropdown: {
maxCount: 500,
maxCount: 500,
header: function() { return autoComplete.currentHeader },
header: function() { return autoComplete.currentHeader; },
style: { "margin-top": (-parseFloat($("body").css("margin-top")))+"px" }
style: { "margin-top": (-parseFloat($("body").css("margin-top")))+"px" }
}
}
Line 115: Line 118:
/** Register strategies */
/** Register strategies */
strategies.push(autoComplete.createTemplateStrategy());
strategies.push(autoComplete.createTemplateStrategy());
textcomplete.register(strategies);
textcomplete.register(strategies);
Line 121: Line 123:
autoComplete.createTemplateStrategy = function() {
autoComplete.createTemplateStrategy = function() {
console.log("createTemplateStrategy start");
console.log("/* createTemplateStrategy - start */");
var getBeforeCursor = function(/**textarea*/) { // Like https://github.com/yuku-t/textcomplete/blob/6f07195c47ace5e787cf7b46604b37a8bd5c6d30/src/textarea.js#L82
// Get text preceding text cursor,
// like https://github.com/yuku-t/textcomplete/blob/6f07195c47ace5e787cf7b46604b37a8bd5c6d30/src/textarea.js#L82
var getBeforeCursor = function(/**textarea*/) {
textarea = document.getElementById("wpTextbox1");
textarea = document.getElementById("wpTextbox1");
return textarea.selectionStart !== textarea.selectionEnd ? null : textarea.value.substring(0, textarea.selectionEnd);
return textarea.selectionStart !== textarea.selectionEnd ? null : textarea.value.substring(0, textarea.selectionEnd);
Line 129: Line 133:
var getLastTemplateOpenBracketPos = function(text) { // Assumes no "{{{" and "}}}" (perhaps ok even with these, anyway?), nor <nowiki> tags that would render "{{" or "}}" escaped/inactive,
var getLastTemplateOpenBracketPos = function(text) { // Assumes no "{{{" and "}}}" (perhaps ok even with these, anyway?), nor <nowiki> tags that would render "{{" or "}}" escaped/inactive,
var count = 0;
var count = 0
var index = text.length-2;
  , index = text.length - 2
  , foundBracketPair
  , chars;
while (index >= 0) {
while (index >= 0) {
var chars = [text.charAt(index),text.charAt(index+1)];
chars = [text.charAt(index), text.charAt(index+1)];
console.log(chars);
///console.log(chars);
var foundBracketPair = false;
foundBracketPair = false;
if (chars[0] === '}' && chars[1] === '}') {
if (chars[0] === '}' && chars[1] === '}') {
count++;
count++;
Line 143: Line 149:
}
}
if (count < 0) return index;
if (count < 0) return index;
foundBracketPair ? index -= 2 : index -= 1;
if (foundBracketPair) index -= 2; else index -= 1;
foundBracketPair = false;
}
}
return -1;
return -1;
Line 154: Line 159:
match: /(\|)([^\|]*)$/,
match: /(\|)([^\|]*)$/,
search: function (term, callback) {
search: function (term, callback) {
console.log("Start identification of parameter choices");
console.log("/* Template parameter choice search - start */");
var text = getBeforeCursor();
var text = getBeforeCursor();
console.log("Text before cursor:: " + text);
console.log(" * Text before cursor: " + text);
text = text.slice(0, text.length - term.length); //stuff before the term but including the "|"
text = text.slice(0, text.length - term.length);
console.log("Text before term:: " + text);
console.log(" * Text before term: " + text);
var templateStartPos = getLastTemplateOpenBracketPos(text);
var templateStartPos = getLastTemplateOpenBracketPos(text);
var templateBody = text.slice(templateStartPos + "{{".length);
var templateBody = text.slice(templateStartPos + "{{".length);
console.log("templateBody:: " + templateBody);
console.log(" * templateBody: " + templateBody);
var templateName = templateBody.slice(0,templateBody.indexOf("|")); // Assumes stuff like {{Test|, not {{Test{{aTemplate}}|
var templateName = templateBody.slice(0,templateBody.indexOf("|")); // Assumes stuff like {{Test|, not {{Test{{aTemplate}}|
console.log("templateName:: " + templateName);
console.log(" * templateName: " + templateName);
// Loop regex to remove the templates [TODO: and tables] (there should be no unmatched braces?) (OR could assume none)
/*
// Not sure that this always works, and maybe also inefficient
* Remove the templates [TODO: and tables] (there are no unmatched template brackets within this string?)
* - Loop a regex removal of {{..}} pairs, inside out
* - There should be no unmatched pairs due to having called
*  getLastTemplateOpenBracketPos(text)
* - Use "?" in the regex to "lazy" match, or else it matches
*  the biggest {{...}}, which renders the following method
*  non-functional
* - The match will only have one "}}" at the end, due to ltr
*  lazy regex search?
* - Then remove the rightmost/innermost {{...}} that includes
this "}}", by reversing so the pair is on the left side of
*  the string, lazy matching, then reversing back
*/
var templateBodyTrimmed = templateBody;
var templateBodyTrimmed = templateBody;
while (templateBodyTrimmed != (templateBodyTrimmed = templateBodyTrimmed.replace(/\{\{[^]*?\}\}/g, function (match) { //use "?" in the regex to lazy match, or else it matches the biggest {{...}}, which is unhelpful to the following method
while (templateBodyTrimmed != (templateBodyTrimmed = templateBodyTrimmed.replace(/\{\{[^]*?\}\}/g, function (match) {
var matchInner = match.slice(2, match.length-2);
var matchInner = match.slice(2, match.length - 2); // Remove outer braces
if (matchInner.includes("{{") /*|| matchInner.includes("}}")*/) { // The match will only have one "}}" at the end, since regex searches ltr?
if (matchInner.includes("{{")) {  
// Remove the last pair of "{{" and "}}"
// Remove the last (inner) pair of "{{" and "}}"
return match.split("").reverse().join("")
return match.split("").reverse().join("") // Reverse
.replace(/\}\}[^]*?\{\{/, "") // Pairs become "}}...{{" when the string is reversed
.replace(/\}\}[^]*?\{\{/, "") // Pairs become "}}...{{" when the string is reversed
.split("").reverse().join("");  
.split("").reverse().join(""); //Reverse back
} else {
} else {
return "";
return "";
Line 180: Line 197:
));
));
// Count the number of "|" in text to determine which parameter (since it's e.g. "{{templateName|param1|param2|currentparam" )
// Count the number of "|" in text to determine which number
// parameter (e.g. "{{templateName|param1|param2|currentparam" )
var paramNum = 0;
var paramNum = 0;
for (var i=0; i<templateBodyTrimmed.length; i++) if (templateBodyTrimmed.charAt(i) == '|') paramNum++;
for (var i=0; i<templateBodyTrimmed.length; i++) if (templateBodyTrimmed.charAt(i) == '|') paramNum++;
console.log(paramNum);
console.log(" * paramNum: " + paramNum);
var paramArray = [];
var paramArray = [];
if (templates[templateName] !== undefined) {
if (templates[templateName] !== undefined) {
Line 192: Line 210:
}
}
}
}
console.log(paramArray);
console.log(" * paramArray: " + paramArray);
autoComplete.currentHeader = templateName + " " + paramNum; // testing out header
autoComplete.currentHeader = templateName + " " + paramNum; // testing out header
var nameArray = paramArray.filter(function(currentValue) { return currentValue.startsWith(term); });
var nameArray = paramArray.filter(function(currentValue) { return currentValue.startsWith(term); });
console.log(" * nameArray: " + paramArray);
console.log("/* Template parameter choice search - end */");
callback(nameArray); // List of possible completions ("names")
callback(nameArray); // List of possible completions ("names")
},
},
Line 207: Line 227:
};
};
console.log("createTemplateStrategy end");
console.log("/* createTemplateStrategy - end */");
return templateStrategy;
return templateStrategy;

Revision as of 02:55, 7 May 2018

/**
 * WikitextAutocompleter
 * Provides Autocomplete (a dropdown list) for wiki editing such as for 
 * completing templates
 * Uses https://yuku-t.com/textcomplete/
 *
 * Notes:
 * - Won't work with CodeEditor since it doesn't use a textarea but that's fine
 *   since CodeEditor won't be used for wikitext
 * Bugs:
 * -
 */

if (mw.config.get("wgAction") == "edit") {
	
	/** Global Variables */
	strategies = []; // Array of strategies
	templates = {}; // Contains all the parameter arrays/definitions
	autoComplete = {}; // Contains supporting variables, e.g. initialismsArray, createTemplateStrategy()
	
	/** Supporting definitions */
	
	/* Array of Standard Initialisms */
	autoComplete.initialismsArray = [];
	var readInitialisms = function(callback) {
		$.get( "https://zelda.gamepedia.com/Template:Zelda?action=raw", function( data ) {
			var initialisms = [];
			data.split("<noinclude>")[1]
				.match(/\|\|[a-zA-Z0-9-+& ]*\n/g)
				.forEach(function(value, index){
				var name = value.slice(2).trim();
				initialisms.push(name);
			});
			autoComplete.initialismsArray = initialisms; // Leave autoComplete.initialismsArray empty until fully filled
			if (typeof callback === "function") callback();
		});
	};
	/* Call it, optionally with a callback if needed */
	readInitialisms(); // No specific need for the callback yet?
	
	/** Template parameter definitions */
	
	/* Template:Color */
	templates.Color = {};
	$.get( "https://zelda.gamepedia.com/Template:Color?action=raw", function( data ) {
		var colorsArray = [];
		data.split("</includeonly>")[0]
			.split("#switch:{{{1\|}}}")[1]
			.split("\|#default")[0]
			.match(/\|[a-zA-Z0-9 ]*/g)
			.forEach(function(value, index){
				var name = value.slice(1).trim();
				colorsArray.push(name);
			});
		templates.Color = {
			"1": colorsArray
		};
	});
	
	/* Template:KSTest */
	templates.KSTest = {
		"1": ["testparam1value1","testparam1value2","testparam1value3"],
		"2": ["testparam2value1","testparam2value2","testparam2value3"],
		"3": ["testparam3value1","testparam3value2","testparam3value3"]
	};
	
	/* Template:Icon */
	templates.Icon = {};
	$.get( "https://zelda.gamepedia.com/Template:Icon?action=raw", function( data ) {
		var iconsArray = [];
		data.split("</includeonly>")[0]
			.split("\|#default")[0]
			.match(/\n\|[a-zA-Z0-9-+ ]*/g)
			.forEach(function(value, index){
				var name = value.slice(2).trim();
				iconsArray.push(name);
			});
		templates.Icon = {
			"1": iconsArray
		};
	});
	
	/* Template:Exp Game */
	templates["Exp Game"] = {};
	templates["Exp Game"].getParams = function(paramNum) {
		return autoComplete.initialismsArray; // Assumes an integer paramNum > 0
	};
	
	// Getting a list of every template in the wiki is non-trivial... too many for a single api call
	
	/** Registering strategies */
	
	autoComplete.currentHeader = ""; // An attempt to generate a dropdown menu header to describe the parameter in some way, to accompany choices, or when there are no preset choices.
	
	autoComplete.getTextcompleteScript = function() {
		console.log( "Loading textcomplete..." );
		$.getScript( "https://unpkg.com/textcomplete/dist/textcomplete.min.js", function( data, textStatus, jqxhr ) {
			//console.log( [ data, textStatus, jqxhr.status ] ); // Data returned, Success, 200
			console.log( "Loaded textcomplete. (Warning: May not be executed yet)" );
			// Textarea object: https://github.com/yuku-t/textcomplete/issues/114#issuecomment-318352383
			Textarea = Textcomplete.editors.Textarea; // Global Variable
			autoComplete.registerStrategies();
		});
	};
	
	/* Note: The param arrays need not exist before strategy is registered */
	autoComplete.registerStrategies = function() {
		var editor = new Textarea(document.getElementById("wpTextbox1"))
		  , options = {
				dropdown: {
					maxCount: 500,
					header: function() { return autoComplete.currentHeader; },
					style: { "margin-top": (-parseFloat($("body").css("margin-top")))+"px" }
				}
			}
		  , textcomplete = new Textcomplete(editor, options);
		
		/** Register strategies */
		strategies.push(autoComplete.createTemplateStrategy());
		
		textcomplete.register(strategies);
	};
	
	autoComplete.createTemplateStrategy = function() {
		console.log("/* createTemplateStrategy - start */");
		
		// Get text preceding text cursor, 
		// like https://github.com/yuku-t/textcomplete/blob/6f07195c47ace5e787cf7b46604b37a8bd5c6d30/src/textarea.js#L82
		var getBeforeCursor = function(/**textarea*/) { 
			textarea = document.getElementById("wpTextbox1");
			return textarea.selectionStart !== textarea.selectionEnd ? null : textarea.value.substring(0, textarea.selectionEnd);
		};
		
		var getLastTemplateOpenBracketPos = function(text) { // Assumes no "{{{" and "}}}" (perhaps ok even with these, anyway?), nor <nowiki> tags that would render "{{" or "}}" escaped/inactive,
			var count = 0
			  , index = text.length - 2
			  , foundBracketPair
			  , chars;
			while (index >= 0) {
				chars = [text.charAt(index), text.charAt(index+1)];
				///console.log(chars);
				foundBracketPair = false;
				if (chars[0] === '}' && chars[1] === '}') {
					count++;
					foundBracketPair = true;
				} else if (chars[0] === '{' && chars[1] === '{') {
					count--;
					foundBracketPair = true;
				}
				if (count < 0) return index;
				if (foundBracketPair) index -= 2; else index -= 1;
			}
			return -1;
		};
		
		var templateStrategy = 
		{
			id: "Templates",
			match: /(\|)([^\|]*)$/,
			search: function (term, callback) {
				console.log("/* Template parameter choice search - start */");
				var text = getBeforeCursor();
				console.log(" * Text before cursor: " + text);
				text = text.slice(0, text.length - term.length);
				console.log(" * Text before term: " + text);
				var templateStartPos = getLastTemplateOpenBracketPos(text);
				var templateBody = text.slice(templateStartPos + "{{".length);
				console.log(" * templateBody: " + templateBody);
				var templateName = templateBody.slice(0,templateBody.indexOf("|")); // Assumes stuff like {{Test|, not {{Test{{aTemplate}}|
				console.log(" * templateName: " + templateName);
				/* 
				 * Remove the templates [TODO: and tables] (there are no unmatched template brackets within this string?)
				 * - Loop a regex removal of {{..}} pairs, inside out
				 * - There should be no unmatched pairs due to having called 
				 *   getLastTemplateOpenBracketPos(text)
				 * - Use "?" in the regex to "lazy" match, or else it matches 
				 *   the biggest {{...}}, which renders the following method 
				 *   non-functional
				 * - The match will only have one "}}" at the end, due to ltr 
				 *   lazy regex search?
				 * - Then remove the rightmost/innermost {{...}} that includes 
				 *   this "}}", by reversing so the pair is on the left side of 
				 *   the string, lazy matching, then reversing back
				 */
				var templateBodyTrimmed = templateBody;
				while (templateBodyTrimmed != (templateBodyTrimmed = templateBodyTrimmed.replace(/\{\{[^]*?\}\}/g, function (match) {
						var matchInner = match.slice(2, match.length - 2); // Remove outer braces
						if (matchInner.includes("{{")) { 
							// Remove the last (inner) pair of "{{" and "}}"
							return match.split("").reverse().join("") // Reverse
									.replace(/\}\}[^]*?\{\{/, "") // Pairs become "}}...{{" when the string is reversed
									.split("").reverse().join(""); //Reverse back
						} else {
							return "";
						}
					})
				));
				
				// Count the number of "|" in text to determine which number 
				// parameter (e.g. "{{templateName|param1|param2|currentparam" )
				var paramNum = 0;
				for (var i=0; i<templateBodyTrimmed.length; i++) if (templateBodyTrimmed.charAt(i) == '|') paramNum++;
				console.log(" * paramNum: " + paramNum);
				var paramArray = [];
				if (templates[templateName] !== undefined) {
					if (typeof templates[templateName].getParams === "function") {
						paramArray = templates[templateName].getParams(paramNum);
					} else {
						paramArray = templates[templateName][paramNum];
					}
				}
				console.log(" * paramArray: " + paramArray);
				autoComplete.currentHeader = templateName + " " + paramNum; // testing out header
				var nameArray = paramArray.filter(function(currentValue) { return currentValue.startsWith(term); });
				console.log(" * nameArray: " + paramArray);
				console.log("/* Template parameter choice search - end */");
				callback(nameArray); // List of possible completions ("names")
			},
			template: function (name) {
				var displayName = name;
				return displayName; // What to display in the list
			},
			replace: function (name) {
				var replacementString = "$1" + name;
				return replacementString; // What to replace the matched typed text with
			}
		};
		
		console.log("/* createTemplateStrategy - end */");
		
		return templateStrategy;
		
	};
	
	/* Load Textcomplete then register the strategies */
	$(document).ready(autoComplete.getTextcompleteScript);
	
}