MediaWiki:Common.js

Revision as of 18:58, 27 November 2019 by Jason (talk | contribs)

Note: After saving, 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)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/* 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);
	if (classes !== undefined) {
		d.classList.add(...classes);
	}
	if(id) {
		d.id = id;
	}
	d.textContent = (text || "");
	return d;
};
// Function to streamline getElementById
var $i = function (id, top) {
	if(top === undefined) {
		top = document;
	}
	return top.getElementById(id);
};
// Function to streamline querySelector
var $q = function (query, top) {
	if(top === undefined) {
		top = document;
	}
	return top.querySelector(query);
};
// Function to streamline querySelectorAll
var $a = function (query, top) {
	if(top === undefined) {
		top = document;
	}
	return top.querySelectorAll(query);
};
// Function to look up a CSS variable (not needed)
/*var $v = function (variable) {
	return window.getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
};*/
// Function to set CSS variables
var $sv = function (variable, value) {
	document.documentElement.style.setProperty(variable, value);
};
// Look for a Div that indicates we're a lexicon page
var lexiconInfo = $i("lexiconInfo");
// Swappers have these two attributes.
var 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
		var inverter = $i("swap-inverter");  // A checkbox to invert ALL animations (check to animate)
		var ds = nex.dataset;
		var ident = ds.swapper;     // Name
		var 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(" ");
					var 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
	var lexicon = JSON.parse(lexiconInfo.textContent);
	// Raw info, Array
	var lexiconWords = lexicon.map((x) => x[0]);
	// Array of lexicon keys (derivation-sort-keys)
	var longestWord = Math.max(...lexicon.map(function(x) {
		var z = (x[1].word || x[0]);
		return z.length;
	}));
	// Length of the longest word in the lexicon
	var lexx = new Map(lexicon);
	// Map with the lexicon info
	var lexiMain = $i("lexicon");
	// Div holding the lexicon words, etc, including padding.
	var lexiDiv = $q(".lexi", lexiMain);
	// Div holding only the lexicon words, etc.
	var lexiDivDataset = lexiDiv.dataset;
	// Simplifying an oft-used property
	var sortMain;
	var sortWord;
	var sortCat;
	var sortDef;
	var checkPrevSorts;
	var doSort;
	// Functions for sorting the lexicon
	var less = -1;
	var more = 1;
	var flagShowDerivs = true;
	// Used by the sorting functions
	var prevSorts = new Set();
	// Holds previously-used sorts
	var Em;
	var maxRowsPx;
	var numMaxRows;
	var allRowsPx;
	// Various variables used in determining the lexicon paddings' heights
	var doRewrite;
	var heartbeat;
	// Functions for rewriting info to page depending on (sort and) scroll position
	var tooBig = (lexiconWords.length > 500);
	// Flag that notes if we should limit info displayed by scroll position
	var undef = 0;
	var uncr = 0;
	var deriv = 0;
	var uncat = 0;
	var cats = new Map();
	// Used to fill out information in the lexicon infobox
	var tempo;
	var tempy;

	// Set CSS variables for lexicon grids
	tempo = lexx.size + 2;
	$sv("--rows-num", "repeat(auto, " + tempo.toString() + ")");
	tempo = longestWord + 2;
	tempy = tempo + 5;
	$sv("--cols-num", tempo.toString() + "em 5em calc(100% - " + tempy.toString() + "em)");
	$sv("--header-cols-num", tempo.toString() + "em 5em calc(100% - " + tempy.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;
		var y = lexx.get(b).sort;
		if(x < y) {
			return less;
		}
		if (x > y) {
			return more;
		}
		if(prevSorts.size) {
			return checkPrevSorts(a, b);
		}
		return 0;
	};
	// Function to sort by word, ignoring if its a derivation or not
	sortWord = function (a, b) {
		var x = lexx.get(a);
		var y = lexx.get(b);
		x = x.selfSort || x.sort;
		y = y.selfSort || y.sort;
		if(x < y) {
			return less;
		}
		if (x > y) {
			return more;
		}
		if(prevSorts.size) {
			return checkPrevSorts(a, b);
		}
		return 0;
	};
	// Function to sort by category
	sortCat = function (a, b) {
		var x = lexx.get(a).cat;
		var y = lexx.get(b).cat;
		if(x < y) {
			return less;
		}
		if (x > y) {
			return more;
		}
		if(prevSorts.size) {
			return checkPrevSorts(a, b);
		}
		return 0;
	};
	// Function to sort by definition
	sortDef = function (a, b) {
		var x = lexx.get(a);
		var y = lexx.get(b);
		// Change these to uppercase to eliminate case-based sorting errors
		x = (x.defSort || x.def);
		x = x.toUpperCase();
		y = (y.defSort || y.def);
		y = y.toUpperCase();
		if(x < y) {
			return less;
		}
		if (x > y) {
			return more;
		}
		if(prevSorts.size) {
			return checkPrevSorts(a, b);
		}
		return 0;
	};
	// When search terms are equal, use previous sort criteria (if any).
	checkPrevSorts = function(a, b) {
		var val = 0;
		var test;
		var rv;
		// save prevSorts
		var prev = prevSorts;
		// put all previous sorts in an array format
		var saved = [...prevSorts.values()];
		// 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) {
			test = saved.pop();
			// 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;
		var id;
		var info;
		var special;
		// Make sure we got a number to work with
		if (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;
			var 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;
					break;
			}
			// Set the text
			node.textContent = txt;
			// Remove any classes that may exist, aside from the first (word, cat or def)
			node = [...cL];
			node.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);
		// 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
		var 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;
		var what = $q("select[name=\"sort\"]").value;
		var lexiMainClasses = lexiMain.classList;
		var 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:
				window.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
		if (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);
			var w = info.word || id;
			var extra = (
				info.special ? [info.special] : []
			);
			//window.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
		tooBig = allRowsPx - maxRowsPx;
		$sv("--flex-bottom", tooBig.toString() + "px");

		// Create a function to periodically check the scroll state
		heartbeat = function() {
			var changed = lexiDivDataset.changed;
			var scrollTop;
			var clientHeightTwice;
			var top;
			var 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);
			var extra = (
				info.special ? [info.special] : []
			);
			//window.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");
			var ncat = (cats.get(cat) || 0);
			var def = info.def;
			var word = info.word;
			var 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();
		});
	}

}
}() );