Difference between revisions of "MediaWiki:Common.js"

Line 1: Line 1:
 
/* Any JavaScript here will be loaded for all users on every page load. */
 
/* Any JavaScript here will be loaded for all users on every page load. */
  
 +
// Function to streamline element creation
 
( function() {
 
( function() {
// Function to streamline element creation
 
 
var $e = function (tag, text, classes, id) {
 
var $e = function (tag, text, classes, id) {
 
var d = document.createElement(tag);
 
var d = document.createElement(tag);
classes !== undefined && d.classList.add(...classes);
+
(classes !== undefined) && d.classList.add(...classes);
d.textContent = text || "";
+
d.textContent = (text || "");
if(id) {
+
id && (d.id = id);
d.id = id;
 
}
 
 
return d;
 
return d;
 
},
 
},
 
// Function to streamline getElementById
 
// Function to streamline getElementById
$i = function (id, top = document) {
+
$i = function (id, top) {
return top.getElementById(id);
+
return (top === undefined ? document : top).getElementById(id);
 
},
 
},
 
// Function to streamline querySelector
 
// Function to streamline querySelector
$q = function (query, top = document) {
+
$q = function (query, top) {
return top.querySelector(query);
+
return (top === undefined ? document : top).querySelector(query);
 
},
 
},
 
// Function to streamline querySelectorAll
 
// Function to streamline querySelectorAll
$a = function (query, top = document) {
+
$a = function (query, top) {
return top.querySelectorAll(query);
+
return (top === undefined ? document : top).querySelectorAll(query);
 
},
 
},
 
// Function to look up a CSS variable (not needed)
 
// Function to look up a CSS variable (not needed)
Line 194: Line 192:
  
 
// Function that rewrites the page to match the newly sorted content
 
// Function that rewrites the page to match the newly sorted content
doRewrite = function(count = Math.max(0, Math.round(Number(lexiDivDataset.top) / Em))) {
+
doRewrite = function(count) {
 
// Variables used in this function
 
// Variables used in this function
 
var cols = 0, id, info, special;
 
var cols = 0, id, info, special;
 +
// Make sure we got a number to work with
 +
(count === undefined) && (count = Math.max(0, Math.round(Number(lexiDivDataset.top) / Em)));
 
// Go through each Div one at a time
 
// Go through each Div one at a time
 
$a("div", lexiDiv).forEach(function(node) {
 
$a("div", lexiDiv).forEach(function(node) {
Line 259: Line 259:
  
 
// Function that is called by the sort button
 
// Function that is called by the sort button
doSort = function(doNotRewrite = false) {
+
doSort = function(doNotRewrite) {
 
var dir = $q("select[name=\"dir\"]").value,
 
var dir = $q("select[name=\"dir\"]").value,
 
what = $q("select[name=\"sort\"]").value,
 
what = $q("select[name=\"sort\"]").value,

Revision as of 18:05, 27 November 2019

/* Any JavaScript here will be loaded for all users on every page load. */

// Function to streamline element creation
( function() {
var $e = function (tag, text, classes, id) {
		var d = document.createElement(tag);
		(classes !== undefined) && d.classList.add(...classes);
		d.textContent = (text || "");
		id && (d.id = id);
		return d;
	},
	// Function to streamline getElementById
	$i = function (id, top) {
		return (top === undefined ? document : top).getElementById(id);
	},
	// Function to streamline querySelector
	$q = function (query, top) {
		return (top === undefined ? document : top).querySelector(query);
	},
	// Function to streamline querySelectorAll
	$a = function (query, top) {
		return (top === undefined ? document : top).querySelectorAll(query);
	},
	// Function to look up a CSS variable (not needed)
	/*$v = function (variable) {
		return window.getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
	},*/
	// Function to set CSS variables
	$sv = function (variable, value) {
		document.documentElement.style.setProperty(variable, value);
	},
	// Look for a Div that indicates we're a lexicon page
	lexiconInfo = $i("lexiconInfo"),
	// Swappers have these two attributes.
	swapperNexus = $a("input[data-swapper][data-swap-interval]");



//
// TEXT SWAP-OUT ANIMATION
//
if(swapperNexus.length) {
	// Check each swapper separately.
	swapperNexus.forEach(function(nex) {
		// Define variables inside here to hold stuff for the setInterval loop.
		var overlord = $i("swap-override"),  // A checkbox to pause ALL swapping on a page
			inverter = $i("swap-inverter"),  // A checkbox to invert ALL animations (check to animate)
			ds = nex.dataset,
			ident = ds.swapper,     // Name
			rep = parseInt(ds.swapInterval); // Duration (in milliseconds)
		setInterval(function(){
			var x = nex.checked;
			if(!overlord.checked &&	(inverter.checked ? x : !x) ) { // Pause when checked
				$a("[data-swap-nexus=\"" + ident + "\"]").forEach(function(d) {
					// Rotate out the text!
					var swap = d.dataset.swap.split(" "),
						newText = swap.shift();
					swap.push(newText);
					d.textContent = newText;
					d.dataset.swap = swap.join(" ");
				});
			}
		}, rep);
	});
}


//
// OVERRIDE LINK TITLES (in certain circumstances)
//
$a("span[title][data-title-override] a[title]").forEach( x => x.removeAttribute("title") );


//
// HANDLE LEXICONS
//
if(lexiconInfo !== null) {
	// Set variables
	let lexicon = JSON.parse(lexiconInfo.textContent),
		// Raw info, Array
		lexiconWords = lexicon.map(x => x[0]),
		// Array of lexicon keys (derivation-sort-keys)
		longestWord = Math.max(...lexicon.map(x => (x[1].word || x[0]).length)),
		// Length of the longest word in the lexicon
		lexx = new Map(lexicon),
		// Map with the lexicon info
		lexiMain = $i("lexicon"),
		// Div holding the lexicon words, etc, including padding.
		lexiDiv = $q(".lexi", lexiMain),
		// Div holding only the lexicon words, etc.
		lexiDivDataset = lexiDiv.dataset,
		// Simplifying an oft-used property
		sortMain, sortWord, sortCat, sortDef, checkPrevSorts, doSort,
		// Functions for sorting the lexicon
		less = -1, more = 1, flagShowDerivs = true,
		// Used by the sorting functions
		prevSorts = new Set(),
		// Holds previously-used sorts
		Em, maxRowsPx, numMaxRows, allRowsPx,
		// Various variables used in determining the lexicon paddings' heights
		doRewrite, heartbeat,
		// Functions for rewriting info to page depending on (sort and) scroll position
		tooBig = (lexiconWords.length > 500),
		// Flag that notes if we should limit info displayed by scroll position
		undef = 0, uncr = 0, deriv = 0, uncat = 0, cats = new Map();
		// Used to fill out information in the lexicon infobox

	// Set CSS variables for lexicon grids
	$sv("--rows-num", "repeat(auto, " + (lexx.size + 2).toString() + ")");
	$sv("--cols-num", (longestWord + 2).toString() + "em 5em calc(100% - " + (longestWord + 7).toString() + "em)");
	$sv("--header-cols-num", (longestWord + 2).toString() + "em 5em calc(100% - " + (longestWord + 7).toString() + "em)");

	// Create an element to determine how large a lexicon row will be, in pixels
	document.body.append($e("div", "", [], "hiddenEm"));
	Em = $i("hiddenEm").clientHeight * 2.5; // Row-height is 2.5em.
	// Determine the maximum number of rows to keep loaded
	numMaxRows = Math.max(Math.floor(window.innerHeight / Em) * 5, 100);
	// Determine the height of the filled rows
	maxRowsPx = numMaxRows * Em;
	// Determine the height that would be generated IF all rows were shown at once
	allRowsPx = lexiconWords.length * Em;

	// Function to sort by word (and derivation)
	sortMain = function (a, b) {
		var x = lexx.get(a).sort, y = lexx.get(b).sort;
		if(x < y) {
			return less;
		} else if (x > y) {
			return more;
		}
		return prevSorts.size ? checkPrevSorts(a, b) : 0;
	};
	// Function to sort by word, ignoring if its a derivation or not
	sortWord = function (a, b) {
		var x = lexx.get(a), y = lexx.get(b);
		x = x.selfSort || x.sort;
		y = y.selfSort || y.sort;
		if(x < y) {
			return less;
		} else if (x > y) {
			return more;
		}
		return prevSorts.size ? checkPrevSorts(a, b) : 0;
	};
	// Function to sort by category
	sortCat = function (a, b) {
		var x = lexx.get(a).cat, y = lexx.get(b).cat;
		if(x < y) {
			return less;
		} else if (x > y) {
			return more;
		}
		return prevSorts.size ? checkPrevSorts(a, b) : 0;
	};
	// Function to sort by definition
	sortDef = function (a, b) {
		var x = lexx.get(a), y = lexx.get(b);
		// Change these to uppercase to eliminate case-based sorting errors
		x = (x.defSort || x.def).toUpperCase();
		y = (y.defSort || y.def).toUpperCase();
		if(x < y) {
			return less;
		} else if (x > y) {
			return more;
		}
		return prevSorts.size ? checkPrevSorts(a, b) : 0;
	};
	// When search terms are equal, use previous sort criteria (if any).
	checkPrevSorts = function(a, b) {
		var val = 0,
			prev = prevSorts,	// save prevSorts
		saved = [...prevSorts.values()];	// put all previous sorts in an array format
		// Clear prevSorts so we don't loop infinitely.
		prevSorts = new Set();
		// Go through previous sorts in a last-in first-out method.
		while(saved.length > 0) {
			let test = saved.pop(), rv;
			// Test the sort
			rv = test(a, b);
			// Check if we're unequal
			if(rv) {
			// Clear saved (to end the loop)
				saved = [];
			// Save the sort value
				val = rv;
			}
		}
		// Restore prevSorts
		prevSorts = prev;
		return val;
	}

	// Function that rewrites the page to match the newly sorted content
	doRewrite = function(count) {
		// Variables used in this function
		var cols = 0, id, info, special;
		// Make sure we got a number to work with
		(count === undefined) && (count = Math.max(0, Math.round(Number(lexiDivDataset.top) / Em)));
		// Go through each Div one at a time
		$a("div", lexiDiv).forEach(function(node) {
			var txt, cL = node.classList;
			// There are three columns
			switch(cols) {
				// Column 1: the word itself
				case 0:
					// Set up the variables so we don't need to check again for the next two columns
					id = lexiconWords[count];
					info = lexx.get(id);
					special = info.special;
					// Word may be in info.word or it may be the sorting id.
					txt = (info.word || id);
					// Advance to next column next time
					cols++;
					break;
				case 1:
					// Category is straightforward
					txt = info.cat;
					// Advance to next column next time
					cols++;
					break;
				case 2:
					// Category is straightforward
					txt = info.def;
					// Advance to next row next time
					count++;
					// Reset column to 0
					cols = 0;
			}
			// Set the text
			node.textContent = txt;
			// Remove any classes that may exist, aside from the first (word, cat or def)
			[...cL].slice(1).forEach(c => cL.remove(c));
			// If special is set, add it as a class
			if(special) {
				cL.add(special);
			}
		});
		// Mark on the main Div if we're sorting by derivations
		lexiMain.classList.toggle("derivationsMarked", flagShowDerivs);
	};

	// Go through each word and add a new property .sort
	lexiconWords.forEach(function(id) {
		var info = lexx.get(id),
			w = (info.word || id),
			// The spreadsheet works best when Zs are used to shunt something to the end of the line
			// In javascript, Zs actuallly move you above all lowercase letters
			// So, we change Zs to ~s to get the desired effect
			sort = id.replace(/Z/g, "~");
		// Create the sort property
		info.sort = sort;
		// Do the same process for the .selfSort property (used by derviations)
		if(info.selfSort) {
			info.selfSort = info.selfSort.replace(/Z/g, "~");
		}
		// Save the info
		lexx.set(id, info);
	});

	// Function that is called by the sort button
	doSort = function(doNotRewrite) {
		var dir = $q("select[name=\"dir\"]").value,
			what = $q("select[name=\"sort\"]").value,
			//table = $q(".lexi", $i("lexicon")),
			//tcl = table.classList,
			//template = ['"wordHead catHead defHead"'],
			lexiMainClasses = lexiMain.classList,
			sortFunction;
		// Change the value of "less" and "more" and change classes on the lexicon to indicate this
		if(dir === "asc") {
			less = -1;
			more = 1;
			lexiMainClasses.add("asc");
			lexiMainClasses.remove("desc");
		} else {
			less = 1;
			more = -1;
			lexiMainClasses.add("desc");
			lexiMainClasses.remove("asc");
		}
		// "Zero out" this flag
		flagShowDerivs = false;
		// Set sortFunction to the correct sorting function
		switch(what) {
			case "word":
				sortFunction = sortMain;
				// Re-enable the flag because we use it here.
				flagShowDerivs = true;
				break;
			case "noDeriv":
				sortFunction = sortWord;
				break;
			case "cat":
				sortFunction = sortCat;
				break;
			case "def":
				sortFunction = sortDef;
				break;
			default:
				console.log("A strange error occurred while trying to sort: [" + what + "]");
				return;
		}
		// Remove the sort from previous sorts (if it's there)
		prevSorts.delete(sortFunction);
		// Sort words via the chosen function
		lexiconWords.sort(sortFunction);
		// Add the sort to previous sorts
		prevSorts.add(sortFunction);
		// Trigger rewrite
		(doNotRewrite === true) || doRewrite();
	};
	// Add the above to the sort button
	$i("dosort").addEventListener("click", function (e) { doSort(false) });
	// Sort right away.
	doSort(true);

	// Check to see if we need to limit the number of rows onscreen
	if(tooBig) {
		// Yes, we need to limit
		// Save initial position as "at the top"
		lexiDivDataset.top = "0";
		lexiDivDataset.scrollTop = "0";
		// Go through the words up to the predetermined limit
		lexiconWords.slice(0, numMaxRows).forEach(function(id) {
			// Get all the info we need
			var info = lexx.get(id),
				w = info.word || id,
				sort = info.sort,
				extra = info.special ? [info.special] : [];
			//console.log([w, id, sort]);
			// Add the word to the page
			lexiDiv.append(
				$e("div", w, ["word", ...extra]),
				$e("div", info.cat, ["cat", ...extra]),
				$e("div", info.def, ["def", ...extra])
			);
		});
		// Note if we have derivations
		lexiMain.classList.toggle("derivationsMarked", flagShowDerivs);
		// Set CSS variables to have no padding on top
		$sv("--flex-top", "0px");
		// Determine how muxh empty space lies beneath the printed rows and set the CSS variable accordingly
		$sv("--flex-bottom", (allRowsPx - maxRowsPx).toString() + "px");

		// Create a function to periodically check the scroll state
		heartbeat = function() {
			var changed = lexiDivDataset.changed,
				scrollTop, clientHeightTwice,
				top, bottom;
			if(changed !== "true") {
				// Nothing has changed.
				if(changed === "setting") {
					lexiDivDataset.changed = "false";
				}
				return;
			}
			// Where are we scrolled to?
			scrollTop = lexiMain.scrollTop;
			// How far up should the padding end?
			clientHeightTwice = lexiMain.clientHeight * 2;
			// What is the current amount of top-padding?
			top = parseInt(lexiDivDataset.top);
			// Determine if we're scrolled within range of an edge
			if(scrollTop >= top && scrollTop + clientHeightTwice - maxRowsPx <= top) {
				// No need to change padding and words
				return;
			}
			// Note that we're changing stuff.
			lexiDivDataset.changed = "setting";
			// Determine new padding for top
			top = Math.min(allRowsPx - maxRowsPx, Math.max(0, scrollTop - clientHeightTwice));
			// Clip to a row boundary
			top -= top % Em;
			// New padding for bottom, too
			bottom = Math.max(0, allRowsPx - (top + maxRowsPx));
			// Rewrite rows starting at the proper position
			doRewrite(Math.max(0, Math.round(top / Em)));
			// Set the top and bottom paddings by CSS variables
			$sv("--flex-top", top.toString() + "px");
			$sv("--flex-bottom", bottom.toString() + "px");
			// Save the top padding and the current position
			lexiDivDataset.top = top.toString();
			lexiDivDataset.scrollTop = scrollTop.toString();
		}
		// Start the heartbeat
		lexiDivDataset.changed = "false";
		setInterval(heartbeat, 300);
		// Start the listener for the scroll event
		lexiMain.addEventListener("scroll", function(event) {
			// When the visible rows are moved down by changing 
		 	// the height of the optimistic row above them, the browser automatically
		 	// scrolls them back into view, which in turn creates another render
		 	// becuase the onScroll is called, resulting in an infinite loop.
		 	// To solve this, we get the snapshot before the DOM is updated
		 	// and check for a mismatch between the scrollTop before and after. 
		 	// If such a mismatch exists, it means that the scroll 
		 	// was done by the browser, and not the user, and therefore
		 	// we apply the scrollTop from the snapshot.
		 	if (lexiDivDataset.changed !== "setting") {
				lexiDivDataset.changed = "true";
			} else {
				lexiDivDataset.changed = "false";
				// Reset scrollTop
				lexiMain.scrollTop = Number(lexiDivDataset.scrollTop);
			}
		});
	} else {
		// No special padding/row-adjustments necessary, just print the entire lexicon
		lexx.forEach(function(info, id) {
			var w = info.word || id,
				sort = info.sort,
				extra = info.special ? [info.special] : [];
			//console.log([w, id, sort]);
			lexiDiv.append(
				$e("div", w, ["word", ...extra]),
				$e("div", info.cat, ["cat", ...extra]),
				$e("div", info.def, ["def", ...extra])
			);
		});
		// Note if we have derivations
		lexiMain.classList.toggle("derivationsMarked", flagShowDerivs);
	}

	//
	// Lexicon Infobox
	//
	if($q("span.lex_box_all") !== null) {
		// total number of words
		$q("span.lex_box_all").textContent = lexicon.length.toString();
		lexx.forEach(function(info) {
			var cat = info.cat || "uncat",
				ncat = cats.get(cat) || 0,
				def = info.def,
				word = info.word,
				dv = info.special === "deriv";
			// Check definitions
			if(!def) {
				undef++;
			} else if (!word) {
				uncr++;
			}
			// Check categories
			if(cat === "uncat") {
				uncat++;
			} else {
				if(cat === "#") {
					cat = "number";
				}
				ncat++;
				cats.set(cat, ncat);
				if(!word) {
					uncr++;
				}
			}
			// Check derivations
			if(dv) {
				deriv++;
			}
		});
		// undefined words
		$q("span.lex_box_undef").textContent = undef.toString();
		// uncreated words (definition and/or category, though)
		$q("span.lex_box_uncr").textContent = uncr.toString();
		// derivations
		$q("span.lex_box_deriv").textContent = deriv.toString();
		// uncategorized words
		$q("span.lex_box_uncat").textContent = uncat.toString();
		// words in categories
		cats.forEach(function(n, cat) {
			$q("span.lex_box_cat_" + cat).textContent = n.toString();
		});
	}

}
}() );