react-intl-formatted-duration
Version:
React intl component to express time duration
356 lines (290 loc) • 11.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var React = _interopDefault(require('react'));
var reactIntl = require('react-intl');
var intlMessageformat = _interopDefault(require('intl-messageformat'));
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _interopDefault$1(ex) {
return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
}
var IntlMessageFormat = _interopDefault$1(intlMessageformat);
const INITIAL_ZERO = /^0/;
function leftTrim(parts) {
let previousEmpty = true;
return parts.filter(token => {
if (token.type === 'literal' && !token.value.trim()) {
if (previousEmpty) return false;
previousEmpty = true;
return true;
} else {
previousEmpty = false;
}
return true;
});
}
function trim(parts, trimFirstPaddedValue = false) {
const trimmed = leftTrim(leftTrim(parts).reverse()).reverse();
if (trimFirstPaddedValue) {
const first = trimmed.find(token => token.type !== 'literal');
first.value = first.value.replace(INITIAL_ZERO, '');
}
return trimmed;
}
function DurationUnitFormat(locales, options = defaultOptions) {
this.locales = locales; // TODO I'm ignoring the unit for now, value is always expressed in seconds
this.unit = 'second'; // .style determines how the placeholders are converted to plain text
this.style = options.style || DurationUnitFormat.styles.LONG; // .isTimer determines some special behaviour, we want to keep the 0s
this.isTimer = this.style === DurationUnitFormat.styles.TIMER; // .format used `seconds`, `minutes`, `hours`, ... as placeholders
this.format = options.format || (this.isTimer ? '{minutes}:{seconds}' : '{seconds}'); // How to format unit according to style
this.formatUnits = options.formatUnits || defaultOptions.formatUnits; // .formatDuration determines whether we use a space or not
this.formatDuration = options.formatDuration || defaultOptions.formatDuration;
this.shouldRound = options.round === true;
}
DurationUnitFormat.units = {
DAY: 'day',
HOUR: 'hour',
MINUTE: 'minute',
SECOND: 'second'
};
DurationUnitFormat.styles = {
CUSTOM: 'custom',
TIMER: 'timer',
// http://www.unicode.org/cldr/charts/27/summary/pl.html#5556
LONG: 'long',
SHORT: 'short',
NARROW: 'narrow'
};
DurationUnitFormat.prototype.formatToParts = function (value) {
// Extract all the parts that are actually used from the localised format
const parts = new IntlMessageFormat(this.format, this.locales).formatToParts({
second: {
unit: DurationUnitFormat.units.SECOND
},
seconds: {
unit: DurationUnitFormat.units.SECOND
},
minute: {
unit: DurationUnitFormat.units.MINUTE
},
minutes: {
unit: DurationUnitFormat.units.MINUTE
},
hour: {
unit: DurationUnitFormat.units.HOUR
},
hours: {
unit: DurationUnitFormat.units.HOUR
},
day: {
unit: DurationUnitFormat.units.DAY
},
days: {
unit: DurationUnitFormat.units.DAY
}
}); // Compute the value of each bucket depending on which parts are used
const buckets = splitSecondsInBuckets(value, this.unit, parts, this.shouldRound); // Each part from the format message could potentially contain multiple parts
const result = parts.reduce((all, token) => all.concat(this._formatToken(token, buckets)), []);
return this._trimOutput(result, parts);
};
DurationUnitFormat.prototype._formatToken = function (token, buckets) {
const {
value
} = token;
if (value.unit) {
const number = buckets[value.unit];
return number || this.isTimer ? this._formatDurationToParts(value.unit, number) : [];
} else if (value) {
// If there is no .unit it's text, but it could be an empty string
return [{
type: 'literal',
value
}];
}
return [];
};
DurationUnitFormat.prototype._formatDurationToParts = function (unit, number) {
if (this.isTimer) {
// With timer style, we only show the value
return [{
type: unit,
value: this._formatValue(number)
}];
} else if (isSpecialStyle(this.style)) {
return new Intl.NumberFormat(this.locales, {
style: 'unit',
unit: unit,
unitDisplay: this.style
}).formatToParts(number).map(_ => ({
// NumberFormat uses 'integer' for types, but I prefer using the unit
// This is more similar to what happens in DateTimeFormat
type: _.type === 'integer' ? unit : _.type,
value: _.value
}));
} // This is now only needed for the custom formatting
return this.formatDuration.split(SPLIT_POINTS).map(text => {
if (text === '{value}') {
return {
type: unit,
value: this._formatValue(number)
};
}
if (text === '{unit}') {
const message = this.formatUnits[unit] || '{value}';
const formattedUnit = new IntlMessageFormat(message, this.locales).format({
value: number
});
return {
type: 'unit',
value: formattedUnit
};
}
if (text) {
return {
type: 'literal',
value: text
};
}
}).filter(Boolean);
};
DurationUnitFormat.prototype._formatValue = function (number) {
return this.isTimer ? number.toString().padStart(2, '0') : number.toString();
};
DurationUnitFormat.prototype._trimOutput = function (result, parts) {
const trimmed = trim(result, this.isTimer);
if (!trimmed.find(_ => _.type !== 'literal')) {
// if everything cancels out and there are only literals,
// then return 0 on the lowest available unit
const minUnit = [DurationUnitFormat.units.SECOND, DurationUnitFormat.units.MINUTE, DurationUnitFormat.units.HOUR, DurationUnitFormat.units.DAY].find(unit => has(parts, unit));
return this._formatDurationToParts(minUnit, 0);
}
return trimmed;
};
const defaultOptions = {
// unit: DurationUnitFormat.units.SECOND,
formatDuration: '{value} {unit}',
formatUnits: {
// custom values
[DurationUnitFormat.units.DAY]: '{value, plural, one {day} other {days}}',
[DurationUnitFormat.units.HOUR]: '{value, plural, one {hour} other {hours}}',
[DurationUnitFormat.units.MINUTE]: '{value, plural, one {minute} other {minutes}}',
[DurationUnitFormat.units.SECOND]: '{value, plural, one {second} other {seconds}}'
},
style: DurationUnitFormat.styles.LONG
};
const SPLIT_POINTS = /(\{value\}|\{unit\})/;
const SECONDS_IN = {
day: 24 * 60 * 60,
hour: 60 * 60,
minute: 60,
second: 1
};
function has(parts, unit) {
return !!parts.find(_ => _.value.unit === unit);
}
function splitSecondsInBuckets(value, valueUnit, parts, shouldRound) {
let seconds = value * SECONDS_IN[valueUnit]; // Rounding will only affect the lowest unit
// check how many seconds we need to add
if (shouldRound) {
const lowestUnit = [DurationUnitFormat.units.SECOND, DurationUnitFormat.units.MINUTE, DurationUnitFormat.units.HOUR, DurationUnitFormat.units.DAY].find(unit => has(parts, unit)); // These many seconds will be ignored by the lowest unit
const remainder = seconds % SECONDS_IN[lowestUnit];
if (2 * remainder >= SECONDS_IN[lowestUnit]) {
// The remainder is large, add enough seconds to increse the lowest unit
seconds += SECONDS_IN[lowestUnit] - remainder;
}
}
const buckets = {};
[DurationUnitFormat.units.DAY, DurationUnitFormat.units.HOUR, DurationUnitFormat.units.MINUTE, DurationUnitFormat.units.SECOND].forEach(unit => {
if (has(parts, unit)) {
buckets[unit] = Math.floor(seconds / SECONDS_IN[unit]);
seconds -= buckets[unit] * SECONDS_IN[unit];
}
});
return buckets;
}
function isSpecialStyle(style) {
return [DurationUnitFormat.styles.LONG, DurationUnitFormat.styles.SHORT, DurationUnitFormat.styles.NARROW].includes(style);
}
var durationUnitFormat_cjs = DurationUnitFormat;
var EXTENDED_FORMAT = 'EXTENDED_FORMAT';
var TIMER_FORMAT = 'TIMER_FORMAT';
function DurationMessage(_ref) {
var _formatUnits;
var intl = _ref.intl,
seconds = _ref.seconds,
format = _ref.format,
textComponent = _ref.textComponent,
unitDisplay = _ref.unitDisplay,
valueComponent = _ref.valueComponent,
otherProps = _objectWithoutProperties(_ref, ["intl", "seconds", "format", "textComponent", "unitDisplay", "valueComponent"]);
var actualFormat = intl.messages["react-intl-formatted-duration/custom-format/".concat(format || '')] || format;
if (!format || format === EXTENDED_FORMAT) {
actualFormat = intl.messages['react-intl-formatted-duration.longFormatting'] || '{minutes} {seconds}';
}
if (format === TIMER_FORMAT) {
actualFormat = intl.messages['react-intl-formatted-duration.timerFormatting'] || '{minutes}:{seconds}';
}
var actualSytle = unitDisplay;
if (!actualSytle) {
actualSytle = format === TIMER_FORMAT ? durationUnitFormat_cjs.styles.TIMER : durationUnitFormat_cjs.styles.CUSTOM;
}
var parts = new durationUnitFormat_cjs(intl.locale, {
format: actualFormat,
formatUnits: (_formatUnits = {}, _defineProperty(_formatUnits, durationUnitFormat_cjs.units.DAY, intl.messages['react-intl-formatted-duration.daysUnit'] || '{value, plural, one {day} other {days}}'), _defineProperty(_formatUnits, durationUnitFormat_cjs.units.HOUR, intl.messages['react-intl-formatted-duration.hoursUnit'] || '{value, plural, one {hour} other {hours}}'), _defineProperty(_formatUnits, durationUnitFormat_cjs.units.MINUTE, intl.messages['react-intl-formatted-duration.minutesUnit'] || '{value, plural, one {minute} other {minutes}}'), _defineProperty(_formatUnits, durationUnitFormat_cjs.units.SECOND, intl.messages['react-intl-formatted-duration.secondsUnit'] || '{value, plural, one {second} other {seconds}}'), _formatUnits),
formatDuration: intl.messages['react-intl-formatted-duration.duration'] || '{value} {unit}',
round: true,
// TODO backward compatible, add a prop to configure it
style: actualSytle
}).formatToParts(seconds);
var Text = textComponent || intl.textComponent;
var Value = valueComponent || textComponent || intl.textComponent;
return React.createElement(Text, otherProps, parts.map(function (token) {
if (token.type === 'literal' || token.type === 'unit') return token.value;
return React.createElement(Value, {
key: token.type
}, token.value);
}));
}
var index = reactIntl.injectIntl(DurationMessage);
exports.EXTENDED_FORMAT = EXTENDED_FORMAT;
exports.TIMER_FORMAT = TIMER_FORMAT;
exports.default = index;