/**
* @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);