MediaWiki:Gadget-countdown-timer.js

From IdleOn MMO Wiki
Revision as of 22:19, 22 May 2024 by Nads (talk | contribs)

Note: After publishing, 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 / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
$(function() {

	/**
	 * Names of all valid format modes.
	 */
	var FORMAT_MODES = [ 'clock', 'labeled' ];

	var UNIT_NAMES = [ 's', 'm', 'h', 'd' ];

	var UNITS = [
		{
			divisor: 1,
			mod: 60,
		},
		{
			divisor: 60,
			mod: 60,
		},
		{
			divisor: 3600,
			mod: 24,
		},
		{
			divisor: 86400,
			mod: Number.MAX_SAFE_INTEGER,
		}
	];

	/**
	 * The list of active timers.
	 */
	var timers = [];

	/**
	 * The list of active formatters.
	 */
	var formatters = [];

	/**
	 * Searches for a value in `container`.
	 * @param value The value to search for.
	 * @param container The container to search.
	 * @param defaultValue The value to default to if `value` could not be found.
	 * @returns `value` if it exists in `container`, otherwise `defaultValue`.
	 */
	function findOrElse(value, container, defaultValue) {
		return container.includes(value) ? value : defaultValue;
	}

	/**
	 * Creates a formatter for `$elem`. Formatters will be reused for elements
	 * with the same configuration.
	 * @param $elem The element to format.
	 * @returns The formatter.
	 */
	function createFormatter($elem) {
		var mode = findOrElse($elem.data('mode'), FORMAT_MODES, 'clock');
		var padZeroes = findOrElse($elem.data('pad-zeroes'), [true, false], mode == 'clock');
		var maxUnit = UNIT_NAMES.indexOf(findOrElse($elem.data('max-unit'), UNIT_NAMES, 'd'));
		var minUnit = UNIT_NAMES.indexOf(findOrElse($elem.data('min-unit'), UNIT_NAMES, 's'));
		var truncate = findOrElse($elem.data('truncate'), [true, false], false);

		// Search for a formatter with matching configuration.
		var formatter = formatters.find(function (fmt) {
			return fmt.mode == mode
				&& fmt.padZeroes == padZeroes
				&& fmt.maxUnit == maxUnit
				&& fmt.minUnit == minUnit
				&& fmt.truncate == truncate;
		});

		// No suitable formatter was found so create and register a new one.
		if (!formatter) {
			formatter = new CountdownFormatter(mode, padZeroes, maxUnit, minUnit, truncate);
			formatters.push(formatter);
		}

		return formatter;
	}

	/**
	 * The `CountdownFormatter` class is responsible for formatting a duration.
	 * @param mode The format mode, can be either `clock` or `labeled`.
	 * @param padZeroes Whether units should be padded with zeroes.
	 * @param maxUnit The largest unit to display.
	 * @param minUnit The smallest unit to display.
	 * @param truncate Whether units larger than the largest non-zero unit should be omitted.
	 */
	function CountdownFormatter(mode, padZeroes, maxUnit, minUnit, truncate) {
		var self = this;
		this.mode = mode;
		this.padZeroes = padZeroes;
		this.maxUnit = maxUnit;
		this.minUnit = minUnit;
		this.truncate = truncate;

		/**
		 * 
		 * @param duration 
		 * @param index 
		 * @param canTruncate 
		 * @returns 
		 */
		this.extractPart = function (duration, index, canTruncate) {
			// Don't bother extracting parts outside of the visible range.
			if (index < self.minUnit || index > self.maxUnit) {
				return undefined;
			}

			// Get the unit metadata.
			var unit = UNITS[index];

			// Get the actual value for the part.
			var value = Math.floor(duration / unit.divisor);
			if (index == self.maxUnit) {
				value %= unit.mod;
			}

			console.log(duration + " => " + value + "(" + UNIT_NAMES[index] + ")");

			// Discard values if they can be truncated.
			if (value == 0 && canTruncate) {
				return undefined;
			}

			// Pad with zeroes if necessary.
			var prefix = (self.padZeroes && value < 10) ? '0' : '';
			return prefix + value.toString();
		}

		/**
		 * Format the specified `duration`.
		 * @param duration The duration to format.
		 * @returns The formatted string.
		 */
		this.format = function (duration) {
			if (duration < 0) {
				duration = 0;
			}

			var canTruncate = self.truncate;

			var parts = [];
			for (var index = self.maxUnit; index >= self.minUnit; index--) {
				// The smallest unit can never be truncated.
				canTruncate &= (index != self.minUnit);

				var part = self.extractPart(duration, index, canTruncate);
				if (part !== undefined) {
					parts.push({
						label: UNIT_NAMES[index],
						value: part
					})
				}

				// Can't truncate if a larger time unit is valid.
				canTruncate &= (part === undefined);
			}

			switch (self.mode) {
				case 'clock':
					return self._formatClock(parts);
				case 'labeled':
					return self._formatLabeled(parts);
			}
		};

		this._formatClock = function (parts) {
			return parts.map(function(part) {
				return part.value
			}).join(':');
		};

		this._formatLabeled = function (parts) {
			return parts.map(function(part) {
				return part.value + part.label
			}).join(' ');
		};

	}

	/**
	 * The `CountdownTimer` class is responsible for applying formatting to an element.
	 * @param $elem 
	 */
	function CountdownTimer($elem) {
		var self = this;
		this.$elem = $elem;
		this.formatter = createFormatter($elem);

		this.expiration = $elem.data('timestamp')
		if (this.expiration === undefined) {
			this.expiration = Date.now() + 90000;
		}

		/**
		 * Updates the timer's value.
		 * @returns true if the timer has not expired, otherwise false.
		 */
		this.tick = function() {
			var duration = Math.floor((self.expiration - Date.now()) / 1000);
			
			var text = self.formatter.format(duration);
			$elem.text(text);
			
			return duration > 0;
		};
		
	}
	
	function tick() {
		// Tick and automatically remove expired timers.
		timers = timers.filter(function(timer) {
			return timer.tick();
		});
		
		// Stop ticking if there are no timers remaining.
		if (timers.length != 0) {
			setTimeout(tick, 1000);
		}
	}
	
	mw.hook('wikipage.content').add(function($content) {
		var $targets = $content.find('time.countdown-timer:not(.managed)');
		if ($targets.length == 0) {
			return;
		}
		
		$targets
			.addClass('managed')
			.each(function() {
				var timer = new CountdownTimer($(this));
				timers.push(timer);
			});
			
		tick();
	});
});