MediaWiki:Gadget-countdown-timer.js: Difference between revisions
From IdleOn MMO Wiki
No edit summary |
No edit summary |
||
(12 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
/* | /** | ||
* 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 = []; | |||
if ( | /** | ||
* 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 ( | if (canTruncate && value == 0) { | ||
return undefined; | |||
} | } | ||
// Apply padding if necessary. | |||
if ( | 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); | $elem.text(text); | ||
return | // 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( | 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) { | ||
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 () { | ||
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();
});
});