mirror of
https://github.com/discourse/discourse.git
synced 2025-01-26 15:13:16 +08:00
339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
/* global BreakString:true */
|
|
|
|
/*
|
|
* memoize.js
|
|
* by @philogb and @addyosmani
|
|
* with further optimizations by @mathias
|
|
* and @DmitryBaranovsk
|
|
* perf tests: http://bit.ly/q3zpG3
|
|
* Released under an MIT license.
|
|
*
|
|
* modified with cap by Sam
|
|
*/
|
|
function cappedMemoize(fn, max) {
|
|
fn.maxMemoize = max;
|
|
fn.memoizeLength = 0;
|
|
|
|
return function () {
|
|
const args = Array.prototype.slice.call(arguments);
|
|
let hash = "";
|
|
let i = args.length;
|
|
let currentArg = null;
|
|
while (i--) {
|
|
currentArg = args[i];
|
|
hash += (currentArg === new Object(currentArg)) ?
|
|
JSON.stringify(currentArg) : currentArg;
|
|
if(!fn.memoize) {
|
|
fn.memoize = {};
|
|
}
|
|
}
|
|
if (hash in fn.memoize) {
|
|
return fn.memoize[hash];
|
|
} else {
|
|
fn.memoizeLength++;
|
|
if(fn.memoizeLength > max) {
|
|
fn.memoizeLength = 0;
|
|
fn.memoize = {};
|
|
}
|
|
const result = fn.apply(this, args);
|
|
fn.memoize[hash] = result;
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
|
|
const breakUp = cappedMemoize(function(str, hint){
|
|
return new BreakString(str).break(hint);
|
|
}, 100);
|
|
export { breakUp };
|
|
|
|
export function shortDate(date){
|
|
return moment(date).format(I18n.t("dates.medium.date_year"));
|
|
}
|
|
|
|
function shortDateNoYear(date) {
|
|
return moment(date).format(I18n.t("dates.tiny.date_month"));
|
|
}
|
|
|
|
// Suppress year if it's this year
|
|
export function smartShortDate(date, withYear=tinyDateYear) {
|
|
return (date.getFullYear() === new Date().getFullYear()) ? shortDateNoYear(date) : withYear(date);
|
|
}
|
|
|
|
export function tinyDateYear(date) {
|
|
return moment(date).format(I18n.t("dates.tiny.date_year"));
|
|
}
|
|
|
|
// http://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript
|
|
// TODO: locale support ?
|
|
export function toTitleCase(str) {
|
|
return str.replace(/\w\S*/g, function(txt){
|
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
|
});
|
|
}
|
|
|
|
export function longDate(dt) {
|
|
if (!dt) return;
|
|
return moment(dt).format(I18n.t("dates.long_with_year"));
|
|
}
|
|
|
|
// suppress year, if current year
|
|
export function longDateNoYear(dt) {
|
|
if (!dt) return;
|
|
|
|
if ((new Date()).getFullYear() !== dt.getFullYear()) {
|
|
return moment(dt).format(I18n.t("dates.long_date_with_year"));
|
|
} else {
|
|
return moment(dt).format(I18n.t("dates.long_date_without_year"));
|
|
}
|
|
}
|
|
|
|
export function updateRelativeAge(elems) {
|
|
// jQuery .each
|
|
elems.each(function(){
|
|
const $this = $(this);
|
|
$this.html(relativeAge(new Date($this.data('time')), {format: $this.data('format'), wrapInSpan: false}));
|
|
});
|
|
}
|
|
|
|
export function autoUpdatingRelativeAge(date,options) {
|
|
if (!date) return "";
|
|
if (+date === +new Date(0)) return "";
|
|
|
|
options = options || {};
|
|
let format = options.format || "tiny";
|
|
|
|
let append = "";
|
|
if (format === 'medium') {
|
|
append = " date";
|
|
if(options.leaveAgo) {
|
|
format = 'medium-with-ago';
|
|
}
|
|
options.wrapInSpan = false;
|
|
}
|
|
|
|
const relAge = relativeAge(date, options);
|
|
|
|
if (format === 'tiny' && relativeAgeTinyShowsYear(relAge)) {
|
|
append += " with-year";
|
|
}
|
|
|
|
if (options.title) {
|
|
append += "' title='" + longDate(date);
|
|
}
|
|
|
|
return "<span class='relative-date" + append + "' data-time='" + date.getTime() + "' data-format='" + format + "'>" + relAge + "</span>";
|
|
}
|
|
|
|
function wrapAgo(dateStr) {
|
|
return I18n.t("dates.wrap_ago", { date: dateStr });
|
|
}
|
|
|
|
export function durationTiny(distance, ageOpts) {
|
|
if (typeof(distance) !== 'number') { return '—'; }
|
|
|
|
const dividedDistance = Math.round(distance / 60.0);
|
|
const distanceInMinutes = (dividedDistance < 1) ? 1 : dividedDistance;
|
|
|
|
const t = function(key, opts) {
|
|
const result = I18n.t("dates.tiny." + key, opts);
|
|
return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
|
|
};
|
|
|
|
let formatted;
|
|
|
|
switch(true) {
|
|
case(distance <= 59):
|
|
formatted = t("less_than_x_minutes", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 0 && distanceInMinutes <= 44):
|
|
formatted = t("x_minutes", {count: distanceInMinutes});
|
|
break;
|
|
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
|
formatted = t("about_x_hours", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
|
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
|
break;
|
|
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
|
|
formatted = t("x_days", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 2520 && distanceInMinutes <= 129599):
|
|
formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
|
|
break;
|
|
case(distanceInMinutes >= 129600 && distanceInMinutes <= 525599):
|
|
formatted = t("x_months", {count: Math.round(distanceInMinutes / 43200.0)});
|
|
break;
|
|
default:
|
|
const numYears = distanceInMinutes / 525600.0;
|
|
const remainder = numYears % 1;
|
|
if (remainder < 0.25) {
|
|
formatted = t("about_x_years", {count: parseInt(numYears)});
|
|
} else if (remainder < 0.75) {
|
|
formatted = t("over_x_years", {count: parseInt(numYears)});
|
|
} else {
|
|
formatted = t("almost_x_years", {count: parseInt(numYears) + 1});
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
function relativeAgeTiny(date, ageOpts) {
|
|
const format = "tiny";
|
|
const distance = Math.round((new Date() - date) / 1000);
|
|
const dividedDistance = Math.round(distance / 60.0);
|
|
const distanceInMinutes = (dividedDistance < 1) ? 1 : dividedDistance;
|
|
|
|
let formatted;
|
|
const t = function(key, opts) {
|
|
const result = I18n.t("dates." + format + "." + key, opts);
|
|
return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
|
|
};
|
|
|
|
|
|
switch(true) {
|
|
case(distanceInMinutes >= 0 && distanceInMinutes <= 44):
|
|
formatted = t("x_minutes", {count: distanceInMinutes});
|
|
break;
|
|
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
|
formatted = t("about_x_hours", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
|
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
|
break;
|
|
case(Discourse.SiteSettings.relative_date_duration === 0 && distanceInMinutes <= 525599):
|
|
formatted = shortDateNoYear(date);
|
|
break;
|
|
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
|
|
formatted = t("x_days", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 2520 && distanceInMinutes <= ((Discourse.SiteSettings.relative_date_duration||14) * 1440)):
|
|
formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
|
|
break;
|
|
default:
|
|
formatted = (ageOpts.defaultFormat || smartShortDate)(date);
|
|
break;
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
/*
|
|
* Returns true if the given tiny date string includes the year.
|
|
* Useful for checking if the string isn't so tiny.
|
|
*/
|
|
function relativeAgeTinyShowsYear(relativeAgeString) {
|
|
return relativeAgeString.match(/'[\d]{2}$/);
|
|
}
|
|
|
|
function relativeAgeMediumSpan(distance, leaveAgo) {
|
|
let formatted;
|
|
const distanceInMinutes = Math.round(distance / 60.0);
|
|
|
|
const t = function(key, opts){
|
|
return I18n.t("dates.medium" + (leaveAgo?"_with_ago":"") + "." + key, opts);
|
|
};
|
|
|
|
switch(true){
|
|
case(distanceInMinutes >= 1 && distanceInMinutes <= 55):
|
|
formatted = t("x_minutes", {count: distanceInMinutes});
|
|
break;
|
|
case(distanceInMinutes >= 56 && distanceInMinutes <= 89):
|
|
formatted = t("x_hours", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
|
formatted = t("x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
|
break;
|
|
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2159):
|
|
formatted = t("x_days", {count: 1});
|
|
break;
|
|
case(distanceInMinutes >= 2160):
|
|
formatted = t("x_days", {count: Math.round((distanceInMinutes - 720.0) / 1440.0)});
|
|
break;
|
|
}
|
|
return formatted || '—';
|
|
}
|
|
|
|
function relativeAgeMedium(date, options) {
|
|
const wrapInSpan = options.wrapInSpan !== false;
|
|
const leaveAgo = options.leaveAgo;
|
|
const distance = Math.round((new Date() - date) / 1000);
|
|
|
|
if (!date) {
|
|
return "—";
|
|
}
|
|
|
|
const fullReadable = longDate(date);
|
|
const fiveDaysAgo = 432000;
|
|
const oneMinuteAgo = 60;
|
|
|
|
let displayDate = "";
|
|
if (distance < oneMinuteAgo) {
|
|
displayDate = I18n.t("now");
|
|
} else if (distance > fiveDaysAgo) {
|
|
displayDate = smartShortDate(date, shortDate);
|
|
} else {
|
|
displayDate = relativeAgeMediumSpan(distance, leaveAgo);
|
|
}
|
|
if(wrapInSpan) {
|
|
return "<span class='date' title='" + fullReadable + "'>" + displayDate + "</span>";
|
|
} else {
|
|
return displayDate;
|
|
}
|
|
}
|
|
|
|
// mostly lifted from rails with a few amendments
|
|
export function relativeAge(date, options) {
|
|
options = options || {};
|
|
const format = options.format || "tiny";
|
|
|
|
if (format === "tiny") {
|
|
return relativeAgeTiny(date, options);
|
|
} else if (format === "medium") {
|
|
return relativeAgeMedium(date, options);
|
|
} else if (format === 'medium-with-ago') {
|
|
return relativeAgeMedium(date, _.extend(options, {format: 'medium', leaveAgo: true}));
|
|
}
|
|
|
|
return "UNKNOWN FORMAT";
|
|
}
|
|
|
|
export function number(val) {
|
|
let formattedNumber;
|
|
|
|
val = parseInt(val, 10);
|
|
if (isNaN(val)) val = 0;
|
|
|
|
if (val > 999999) {
|
|
formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
|
|
return I18n.t("number.short.millions", {number: formattedNumber});
|
|
} else if (val > 99999) {
|
|
formattedNumber = I18n.toNumber(Math.floor(val / 1000), {precision: 0});
|
|
return I18n.t("number.short.thousands", {number: formattedNumber});
|
|
} else if (val > 999) {
|
|
formattedNumber = I18n.toNumber(val / 1000, {precision: 1});
|
|
return I18n.t("number.short.thousands", {number: formattedNumber});
|
|
}
|
|
return val.toString();
|
|
}
|
|
|
|
export function ensureJSON(json) {
|
|
return typeof json === 'string' ? JSON.parse(json) : json;
|
|
}
|
|
|
|
export function plainJSON(val) {
|
|
let json = ensureJSON(val);
|
|
let headers = '';
|
|
Object.keys(json).forEach(k => {
|
|
headers += `${k}: ${json[k]}\n`;
|
|
});
|
|
return headers;
|
|
}
|
|
|
|
export function prettyJSON(json) {
|
|
return JSON.stringify(ensureJSON(json), null, 2);
|
|
}
|