MediaWiki:Gadget-countdown-timer.js: Difference between revisions

From IdleOn MMO Wiki
(Created page with "→‎jshint esversion: 6: $(() => { const timers = [] class CountdownTimer { constructor($elem) { this.$elem = $elem; // TODO Query the element for the expiration time. this.expiration = Date.now() + (90 * 1000); } tick() { const time = Math.floor((this.expiration - Date.now()) / 1000); const s = time % 60; const m = Math.floor(time / 60) % 60; const h = Math.floor(time / 3600) % 24; const d = Math.floor(time / 86400); l...")
 
No edit summary
 
(14 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* jshint esversion: 6 */
/**
* This gadget automatically tracks and updates any timer elements detected on
* the page. A valid timer element looks like this:
*  <time class="countdown-timer"></time>
*
* Additionally, timer elements can be configured with the following properties:
*  data-timestamp:
*    The Unix timestamp to count down towards.
*  data-mode:
*    The format mode, can be either `clock` (DD:HH:MM:SS) or `labeled` (DDd HHh MMm SSs)
*  data-pad-zeroes:
*    Whether to pad time units with leading zeroes (e.g. 3 seconds -> 03 seconds)
*  data-max-unit:
*    The largest time unit to display (`d` = days, `h` = hours, `m` = minutes, `s` = seconds)
*  data-min-unit:
*    The smallest time unit to display (`d` = days, `h` = hours, `m` = minutes, `s` = seconds)
*  data-truncate:
*    Whether to omit time units larger than the largest non-zero time unit (true/false).
*  data-should-tick:
*    Whether to continue ticking the timer after the page is initially loaded (true/false).
*/
$(function () {
/**
* Names of all valid format modes.
*/
var FORMAT_MODES = ['clock', 'labeled'];


$(() => {
/**
* The names of each unit.
const timers = []
*/
var UNIT_NAMES = ['s', 'm', 'h', 'd'];
class CountdownTimer {
 
constructor($elem) {
/**
this.$elem = $elem;
* Metadata for each unit.
// TODO Query the element for the expiration time.
*/
this.expiration = Date.now() + (90 * 1000);
var UNITS = [
{
divisor: 1,
mod: 60,
},
{
divisor: 60,
mod: 60,
},
{
divisor: 3600,
mod: 24,
},
{
divisor: 86400,
mod: Number.MAX_SAFE_INTEGER,
}
}
];
tick() {
 
const time = Math.floor((this.expiration - Date.now()) / 1000);
/**
* The list of active timers.
const s = time % 60;
*/
const m = Math.floor(time / 60) % 60;
var timers = [];
const h = Math.floor(time / 3600) % 24;
 
const d = Math.floor(time / 86400);
/**
* The list of active formatters.
let text = ''
*/
var formatters = [];
let showUnit = d > 0;
 
if (showUnit) {
/**
text += d + 'd';
* 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;
 
/**
* Extracts and formats a part from the timer.
* @param duration The duration of the timer.
* @param index The index of the target part.
* @param canTruncate Whether this part can be truncated.
* @returns The formatted part.
*/
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 metadata for the unit.
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;
}
}
 
showUnit |= h > 0;
// Discard parts that can be truncated.
if (showUnit) {
if (canTruncate && value == 0) {
text += h + 'h'
return undefined;
}
}
 
showUnit |= m > 0;
// Apply padding if necessary.
if (showUnit) {
var prefix = (self.padZeroes && value < 10) ? '0' : '';
text += m + 'm';
return prefix + value.toString();
};
 
/**
* Format the specified `duration`.
* @param duration The duration to format.
* @returns The formatted duration.
*/
this.format = function (duration) {
// Can't format negative durations.
duration = Math.max(duration, 0);
 
// Track whether the next formatted part can be truncated.
var canTruncate = self.truncate;
 
var parts = [];
for (var index = self.maxUnit; index >= self.minUnit; index--) {
// Smallest unit can never be truncated.
canTruncate &= (index != self.minUnit);
 
// Format the timer parts.
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);
}
}
 
showUnit |= s > 0;
// Format each of the parts.
if (showUnit) {
var output;
text += s + 's';
switch (self.mode) {
case 'clock':
output = self._formatClock(parts);
break;
case 'labeled':
output = self._formatLabeled(parts);
break;
}
}
 
// Prepend the prefix to the output.
return output;
};
 
/**
* Applies the `clock` formatting to the time parts.
* @param parts The parts to format.
* @returns The formatted duration.
*/
this._formatClock = function (parts) {
return parts.map(function (part) {
return part.value;
}).join(':');
};
 
/**
* Applies the `labeled` formatting to the time parts.
* @param parts The parts to format.
* @returns The formatted duration.
*/
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 The timer element to register.
*/
function CountdownTimer($elem) {
var self = this;
this.$elem = $elem;
this.formatter = createFormatter($elem);
this.shouldTick = findOrElse($elem.data('should-tick'), [true, false], true);
this.expiration = $elem.data('timestamp');
if (this.expiration === undefined) {
this.expiration = 0;
}
// Timestamps are specified in seconds, but JS uses milliseconds.
this.expiration *= 1000;
 
/**
* 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);
$elem.text(text);
return time > 0;
// Timers should automatically unregister once duration reaches 0,
}
// or if they're marked as non-ticking.
return self.shouldTick && duration > 0;
};
 
}
}
 
/**
* Ticks all managed timers.
*/
function tick() {
function tick() {
// Tick and automatically remove expired timers.
// Tick and automatically remove expired timers.
timers = timers.filter(t => t.tick())
timers = timers.filter(function (timer) {
return timer.tick();
});
 
// Stop ticking if there are no timers remaining.
if (timers.length != 0) {
if (timers.length != 0) {
setTimeout(tick, 1000)
setTimeout(tick, 1000);
}
}
}
}
 
mw.hook('wikipage.content').add($content => {
mw.hook('wikipage.content').add(function ($content) {
const $targets = $content.find('time.countdown-timer:not(.managed)');
var $targets = $content.find('time.countdown-timer:not(.managed)');
if ($targets.length == 0) {
if ($targets.length == 0) {
return;
return;
}
}
 
$targets
$targets
.addClass('managed')
.addClass('managed')
.each(() => {
.each(function () {
const timer = new CountdownTimer($(this))
var timer = new CountdownTimer($(this));
timers.push(timer)
timers.push(timer);
});
});
 
tick();
tick();
});
});
});
});

Latest revision as of 05:05, 27 May 2024

/**
 * This gadget automatically tracks and updates any timer elements detected on
 * the page. A valid timer element looks like this:
 *   <time class="countdown-timer"></time>
 * 
 * Additionally, timer elements can be configured with the following properties:
 *   data-timestamp:
 *     The Unix timestamp to count down towards.
 *   data-mode:
 *     The format mode, can be either `clock` (DD:HH:MM:SS) or `labeled` (DDd HHh MMm SSs)
 *   data-pad-zeroes:
 *     Whether to pad time units with leading zeroes (e.g. 3 seconds -> 03 seconds)
 *   data-max-unit:
 *     The largest time unit to display (`d` = days, `h` = hours, `m` = minutes, `s` = seconds)
 *   data-min-unit:
 *     The smallest time unit to display (`d` = days, `h` = hours, `m` = minutes, `s` = seconds)
 *   data-truncate:
 *     Whether to omit time units larger than the largest non-zero time unit (true/false).
 *   data-should-tick:
 *     Whether to continue ticking the timer after the page is initially loaded (true/false).
 */
$(function () {
	/**
	 * Names of all valid format modes.
	 */
	var FORMAT_MODES = ['clock', 'labeled'];

	/**
	 * The names of each unit.
	 */
	var UNIT_NAMES = ['s', 'm', 'h', 'd'];

	/**
	 * Metadata for each unit.
	 */
	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;

		/**
		 * Extracts and formats a part from the timer.
		 * @param duration The duration of the timer.
		 * @param index The index of the target part.
		 * @param canTruncate Whether this part can be truncated. 
		 * @returns The formatted part.
		 */
		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 metadata for the unit.
			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;
			}

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

			// Apply padding 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 duration.
		 */
		this.format = function (duration) {
			// Can't format negative durations.
			duration = Math.max(duration, 0);

			// Track whether the next formatted part can be truncated.
			var canTruncate = self.truncate;

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

				// Format the timer parts.
				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);
			}

			// Format each of the parts.
			var output;
			switch (self.mode) {
				case 'clock':
					output = self._formatClock(parts);
					break;
				case 'labeled':
					output = self._formatLabeled(parts);
					break;
			}

			// Prepend the prefix to the output.
			return output;
		};

		/**
		 * Applies the `clock` formatting to the time parts.
		 * @param parts The parts to format.
		 * @returns The formatted duration.
		 */
		this._formatClock = function (parts) {
			return parts.map(function (part) {
				return part.value;
			}).join(':');
		};

		/**
		 * Applies the `labeled` formatting to the time parts.
		 * @param parts The parts to format.
		 * @returns The formatted duration.
		 */
		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 The timer element to register.
	 */
	function CountdownTimer($elem) {
		var self = this;
		this.$elem = $elem;
		this.formatter = createFormatter($elem);
		this.shouldTick = findOrElse($elem.data('should-tick'), [true, false], true);
		this.expiration = $elem.data('timestamp');
		if (this.expiration === undefined) {
			this.expiration = 0;
		}
		
		// Timestamps are specified in seconds, but JS uses milliseconds.
		this.expiration *= 1000;

		/**
		 * 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);
			
			// Timers should automatically unregister once duration reaches 0,
			// or if they're marked as non-ticking.
			return self.shouldTick && duration > 0;
		};

	}

	/**
	 * Ticks all managed timers.
	 */
	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();
	});
});