You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
305 lines
9.6 KiB
305 lines
9.6 KiB
/** |
|
* @file |
|
* Dynamic time difference formatting. |
|
*/ |
|
|
|
((Drupal, once) => { |
|
/** |
|
* @typedef {object} timeDiffValue |
|
* |
|
* @prop {number} [year] |
|
* Years count. |
|
* @prop {number} [month] |
|
* Months count. |
|
* @prop {number} [week] |
|
* Weeks count. |
|
* @prop {number} [day] |
|
* Days count. |
|
* @prop {number} [hour] |
|
* Hours count. |
|
* @prop {number} [minute] |
|
* Minutes count. |
|
* @prop {number} [second] |
|
* Seconds count. |
|
*/ |
|
|
|
/** |
|
* @typedef {object} timeDiff |
|
* |
|
* @prop {string} formatted |
|
* A translated string representation of the interval. |
|
* @prop {timeDiffValue} value |
|
* The elements composing the time difference interval. Example: { day: 2, |
|
* hour: 2, minute: 32, second: 15 }. |
|
*/ |
|
|
|
/** |
|
* List of time intervals. |
|
* |
|
* @type {object} |
|
* |
|
* @prop {number} year |
|
* Year duration in seconds. |
|
* @prop {number} month |
|
* Month duration in seconds. |
|
* @prop {number} week |
|
* Week duration in seconds. |
|
* @prop {number} day |
|
* Day duration in seconds. |
|
* @prop {number} hour |
|
* Hour duration in seconds. |
|
* @prop {number} minute |
|
* Minute duration in seconds. |
|
* @prop {number} second |
|
* One second. |
|
*/ |
|
const intervals = { |
|
year: 31536000, |
|
month: 2592000, |
|
week: 604800, |
|
day: 86400, |
|
hour: 3600, |
|
minute: 60, |
|
second: 1, |
|
}; |
|
|
|
/** |
|
* List of available time intervals names. |
|
* |
|
* @type {string[]} |
|
*/ |
|
const intervalsNames = Object.keys(intervals); |
|
|
|
/** |
|
* |
|
* @type {WeakMap<HTMLElement, number>} |
|
*/ |
|
const timers = new WeakMap(); |
|
|
|
/** |
|
* @namespace |
|
*/ |
|
Drupal.timeDiff = { |
|
/** |
|
* Fills a HTML5 time element text with a computed time difference string. |
|
* |
|
* @param {Element} timeElement |
|
* The time DOM element. |
|
*/ |
|
show(timeElement) { |
|
const timestamp = new Date( |
|
timeElement.getAttribute('datetime'), |
|
).getTime(); |
|
const timeDiffSettings = JSON.parse( |
|
timeElement.getAttribute('data-drupal-time-diff'), |
|
); |
|
|
|
const now = Date.now(); |
|
const diff = Math.round((timestamp - now) / 1000); |
|
const options = { granularity: timeDiffSettings.granularity }; |
|
const timeDiff = Drupal.timeDiff.format(diff, options); |
|
const format = diff > 0 ? 'future' : 'past'; |
|
timeElement.textContent = Drupal.formatString( |
|
timeDiffSettings.format[format], |
|
{ |
|
'@interval': timeDiff.formatted, |
|
}, |
|
); |
|
|
|
if (timeDiffSettings.refresh > 0) { |
|
const refreshInterval = Drupal.timeDiff.refreshInterval( |
|
timeDiff.value, |
|
timeDiffSettings.refresh, |
|
timeDiffSettings.granularity, |
|
); |
|
clearTimeout(timers.get(timeElement)); |
|
timers.set( |
|
timeElement, |
|
setTimeout(Drupal.timeDiff.show, refreshInterval * 1000, timeElement), |
|
); |
|
} |
|
}, |
|
|
|
/** |
|
* Computes the refresh interval. |
|
* |
|
* There are cases when the refresh occurs even when it is not needed. For |
|
* example if the refresh interval is '10 seconds', the granularity is 2 and |
|
* the time difference is '1 hour 32 minutes', there's no need to refresh |
|
* every 10 seconds but every 1 minute. This function optimizes the refresh |
|
* interval to higher values, if the structure of the time difference |
|
* doesn't require refreshing more often. |
|
* |
|
* @param {timeDiffValue} value |
|
* The time difference object. |
|
* @param {number} refresh |
|
* The configured refresh interval in seconds. |
|
* @param {number} granularity |
|
* The time difference granularity. |
|
* |
|
* @return {number} |
|
* The computed refresh interval in seconds. |
|
*/ |
|
refreshInterval(value, refresh, granularity) { |
|
const units = Object.keys(value); |
|
const unitsCount = units.length; |
|
const lastUnit = units.pop(); |
|
|
|
// If the lowest unit of time difference is 'minute' or greater but the |
|
// refresh interval is lower, do not refresh often than the duration of |
|
// the lowest unit of time difference. |
|
if (lastUnit !== 'second') { |
|
// If the time difference value parts count equals the granularity and |
|
// lowest unit duration is bigger than the refresh interval, use the |
|
// interval duration. For example, if the refresh interval is |
|
// '10 seconds', the granularity is 2 and the time difference is |
|
// '1 hour 32 minutes', do not refresh every 10 seconds but every one |
|
// minute (60 seconds). |
|
if (unitsCount === granularity) { |
|
intervalsNames.every((interval) => { |
|
const duration = intervals[interval]; |
|
if (interval === lastUnit) { |
|
refresh = refresh < duration ? duration : refresh; |
|
return false; |
|
} |
|
return true; |
|
}); |
|
return refresh; |
|
} |
|
// The time difference value parts count might be smaller than the |
|
// granularity when the lowest part is missed because is 0. In this case |
|
// the missed part interval duration is used as refresh. For example, if |
|
// the refresh is '10 seconds', the granularity is 2 and the time |
|
// difference is '59 minutes 59 seconds', on the next refresh the time |
|
// difference will be '1 hour' (because minutes are 0, therefore are not |
|
// shown) but we want the next refresh to occur, not in one hour, but in |
|
// one minute. |
|
const lastIntervalIndex = intervalsNames.indexOf(lastUnit); |
|
const nextInterval = intervalsNames[lastIntervalIndex + 1]; |
|
refresh = intervals[nextInterval]; |
|
} |
|
return refresh; |
|
}, |
|
|
|
/** |
|
* Formats a time interval between two timestamps. |
|
* |
|
* @param {number} diff |
|
* A UNIX timestamps difference in seconds. |
|
* @param {object} [options] |
|
* An optional object with additional options. |
|
* @param {number} [options.granularity=2] |
|
* An integer value that signals how many different units to display in the |
|
* string. Defaults to 2. |
|
* @param {boolean} [options.strict=false] |
|
* A boolean value indicating whether or not, a negative diff should be |
|
* rendered as "0 seconds". If the time difference is negative (i.e. the |
|
* timestamp is in the past) and this option is false (default) the result |
|
* string will be the formatted time difference. If the option is true the |
|
* result string will be "0 seconds". |
|
* |
|
* @return {timeDiff} |
|
* A time difference type object. |
|
*/ |
|
format(diff, options = {}) { |
|
// Provide appropriate defaults. |
|
options = { granularity: 2, strict: false, ...options }; |
|
|
|
if (options.strict && diff < 0) { |
|
return { |
|
formatted: Drupal.formatPlural(0, '1 second', '@count seconds'), |
|
value: { second: 0 }, |
|
}; |
|
} |
|
diff = Math.abs(diff); |
|
|
|
const output = []; |
|
const value = {}; |
|
let units; |
|
let { granularity } = options; |
|
|
|
intervalsNames.every((interval) => { |
|
const duration = intervals[interval]; |
|
units = Math.floor(diff / duration); |
|
if (units > 0) { |
|
diff %= units * duration; |
|
switch (interval) { |
|
case 'year': |
|
output.push(Drupal.formatPlural(units, '1 year', '@count years')); |
|
break; |
|
case 'month': |
|
output.push( |
|
Drupal.formatPlural(units, '1 month', '@count months'), |
|
); |
|
break; |
|
case 'week': |
|
output.push(Drupal.formatPlural(units, '1 week', '@count weeks')); |
|
break; |
|
case 'day': |
|
output.push(Drupal.formatPlural(units, '1 day', '@count days')); |
|
break; |
|
case 'hour': |
|
output.push(Drupal.formatPlural(units, '1 hour', '@count hours')); |
|
break; |
|
case 'minute': |
|
output.push( |
|
Drupal.formatPlural(units, '1 minute', '@count minutes'), |
|
); |
|
break; |
|
default: |
|
output.push( |
|
Drupal.formatPlural(units, '1 second', '@count seconds'), |
|
); |
|
} |
|
value[interval] = units; |
|
|
|
granularity -= 1; |
|
if (granularity <= 0) { |
|
// Limit the granularity of the output. |
|
return false; |
|
} |
|
} else if (output.length > 0) { |
|
// Exit if there was previous output but not any output at this level, |
|
// to avoid skipping levels and getting output like "1 year 1 second". |
|
return false; |
|
} |
|
return true; |
|
}); |
|
|
|
if (output.length === 0) { |
|
return { |
|
formatted: Drupal.formatPlural(0, '1 second', '@count seconds'), |
|
value: { second: 0 }, |
|
}; |
|
} |
|
return { formatted: output.join(' '), value }; |
|
}, |
|
}; |
|
|
|
/** |
|
* Fills all time[data-drupal-time-diff] elements with a refreshing time diff. |
|
* |
|
* @type {Drupal~behavior} |
|
* |
|
* @prop {Drupal~behaviorAttach} attach |
|
* Initializes refresh of time differences. |
|
* |
|
* @prop {Drupal~behaviorDetach} detach |
|
* Clear timers associated with time diff elements. |
|
*/ |
|
Drupal.behaviors.timeDiff = { |
|
attach(context) { |
|
// Replace each <time> element text with a time difference representation. |
|
once('time-diff', 'time[data-drupal-time-diff]', context).forEach( |
|
Drupal.timeDiff.show, |
|
); |
|
}, |
|
detach(context, settings, trigger) { |
|
if (trigger === 'unload') { |
|
once |
|
.remove('time-diff', 'time[data-drupal-time-diff]', context) |
|
.forEach((timeElement) => clearTimeout(timers.get(timeElement))); |
|
} |
|
}, |
|
}; |
|
})(Drupal, once);
|
|
|