Difference between revisions of "MediaWiki:Common.js"

Line 129: Line 129:
 
tempo, tempy,
 
tempo, tempy,
 
// Temp variables
 
// Temp variables
maybeSetText = function(what, value) {
+
maybeSetText = function (what, value) {
 
var node = $q(what);
 
var node = $q(what);
 
if(node !== null) {
 
if(node !== null) {
Line 417: Line 417:
 
// Create a function to periodically check the scroll state
 
// Create a function to periodically check the scroll state
 
heartbeat = function() {
 
heartbeat = function() {
var changed = lexiDivDataset.changed;
+
var changed = lexiDivDataset.changed,
 
scrollTop, clientHeightTwice, top, bottom;
 
scrollTop, clientHeightTwice, top, bottom;
 
if(changed !== "true") {
 
if(changed !== "true") {

Revision as of 07:21, 28 November 2019

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

// Function to streamline element creation
var $e = function (tag, text, classes, id) {
		var d = document.createElement(tag),
			cl = d.classList;
		if (classes !== undefined) {
			classes.forEach( function(c) {
				cl.add(c);
			});
		}
		if(id) {
			d.id = id;
		}
		d.textContent = (text || "");
		return d;
	},
	// Function to streamline getElementById
	$i = function (id, top) {
		if(top === undefined) {
			top = document;
		}
		return top.getElementById(id);
	},
	// Function to streamline querySelector
	$q = function (query, top) {
		if(top === undefined) {
			top = document;
		}
		return top.querySelector(query);
	},
	// Function to streamline querySelectorAll
	$a = function (query, top) {
		if(top === undefined) {
			top = document;
		}
		return 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( function(x) {
	x.removeAttribute("title");
});


//
// HANDLE LEXICONS
//
if(lexiconInfo !== null) {
	// Set variables
	var lexicon = JSON.parse(lexiconInfo.textContent),
		// Raw info, Array
		lexiconWords = lexicon.map(function(x) {
			return x[0];
		}),
		// Array of lexicon keys (derivation-sort-keys)
		longestWord = lexicon.map(function(x) {
				var z = (x[1].word || x[0]);
				return z.length;
			}).reduce(function(acc, val) {
				return acc > val ? acc : val;
			}),
		// 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
		tempo, tempy,
		// Temp variables
		maybeSetText = function (what, value) {
			var node = $q(what);
			if(node !== null) {
				node.textContent = value;
			}
		};
		// A function for setting text when you're not sure an element exists

	// 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,
			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),
			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,
			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),
			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,
			test, rv,
			// save prevSorts
			prev = prevSorts,
			// put all previous sorts in an array format
			saved = Array.from(prevSorts);
		// 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,
			id, info, 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,
				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)
			Array.from(cL).slice(1).forEach(function(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
			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,
			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:
				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),
				w = info.word || id,
				extraW = ["word"],
				extraC = ["cat"],
				extraD = ["def"],
				special = info.special;
			if (special) {
				extraW.unshift(special);
				extraC.unshift(special);
				extraD.unshift(special);
			}
			//window.console.log([w, id, sort]);
			// Add the word to the page
			lexiDiv.append(
				$e("div", w, extraW),
				$e("div", info.cat, extraC),
				$e("div", info.def, extraD)
			);
		});
		// 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,
				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) {
			// Get all the info we need
			var w = info.word || id,
				extraW = ["word"],
				extraC = ["cat"],
				extraD = ["def"],
				special = info.special;
			if (special) {
				extraW.unshift(special);
				extraC.unshift(special);
				extraD.unshift(special);
			}
			//window.console.log([w, id, sort]);
			// Add the word to the page
			lexiDiv.append(
				$e("div", w, extraW),
				$e("div", info.cat, extraC),
				$e("div", info.def, extraD)
			);
		});
		// Note if we have derivations
		lexiMain.classList.toggle("derivationsMarked", flagShowDerivs);
	}

	//
	// Lexicon Infobox
	//
	if($q("span.lex_box_all") !== null) {
		// total number of words
		maybeSetText("span.lex_box_all", 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
		maybeSetText("span.lex_box_undef", undef.toString());
		// uncreated words (definition and/or category, though)
		maybeSetText("span.lex_box_uncr", uncr.toString());
		// derivations
		maybeSetText("span.lex_box_deriv", deriv.toString());
		// uncategorized words
		maybeSetText("span.lex_box_uncat", uncat.toString());
		// words in categories
		cats.forEach(function(n, cat) {
			maybeSetText("span.lex_box_cat_" + cat, n.toString());
		});
	}

}