UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

535 lines (469 loc) • 22.3 kB
import { Options } from '../misc/Options'; import { Utils } from './Utils'; import { l } from '../strings/Strings'; import * as _ from 'underscore'; import * as moment from 'moment'; import { Logger } from '../misc/Logger'; declare const Globalize; /** * The `IDateToStringOptions` interface describes a set of options to use when converting a standard Date object to a * string using the [ `dateToString` ]{@link DateUtils.dateToString}, or the * [ `dateTimeToString` ]{@link DateUtils.dateTimeToString} method from the [ `DateUtils` ]{@link DateUtils} class. * The precedence orders for the options are: * [ `useTodayYesterdayAndTomorrow` ]{@link IDateToStringOptions.useTodayYesterdayAndTomorrow} * -> [ `useWeekdayIfThisWeek` ]{@link IDateToStringOptions.useWeekdayIfThisWeek} * -> [ `omitYearIfCurrentOne` ]{@link IDateToStringOptions.omitYearIfCurrentOne} * -> [ `useLongDateFormat` ]{@link IDateToStringOptions.useLongDateFormat} * and [ `alwaysIncludeTime` ]{@link IDateToStringOptions.alwaysIncludeTime} * -> [ `includeTimeIfThisWeek` ]{@link IDateToStringOptions.includeTimeIfThisWeek} * -> [ `includeTimeIfToday` ]{@link IDateToStringOptions.includeTimeIfToday}. */ export interface IDateToStringOptions { /** * Contains a standard Date object that specifies the current date and time. * * Default value is `undefined`. */ now?: Date; /** * Specifies whether to convert the Date object to the localized version of `Today`, `Yesterday`, or `Tomorrow`, * if possible. This option takes precedence over * [ `useWeekdayIfThisWeek` ]{@link IDateToStringOptions.useWeekdayIfThisWeek}. * * **Examples** * * If [ `useTodayYesterdayAndTomorrow` ]{@link IDateToStringOptions.useTodayYesterdayAndTomorrow} is `true`, * and [ `now` ]{@link IDateToStringOptions.now} contains a Date object equivalent to `March 8, 2017`, then: * * - If the Date object to convert contains a value equivalent to `March 7, 2017`, the resulting string is the * localized version of `Yesterday`. * * - If the Date object to convert contains a value equivalent to `March 8, 2017`, the resulting string is the * localized version of `Today`. * * - If the Date object to convert contains a value equivalent to `March 9, 2017`, the resulting string is the * localized version of `Tomorrow`. * * Default value is `true`. */ useTodayYesterdayAndTomorrow?: boolean; /** * Specifies whether to convert the Date object to the localized version of the corresponding day of the week, * if the date to convert is part of the current week. This option takes precedence over * [ `omitYearIfCurrentOne` ]{@link IDateToStringOptions.omitYearIfCurrentOne}. * * **Examples** * * If [ `useWeekdayIfThisWeek` ]{@link IDateToStringOptions.useWeekdayIfThisWeek} is `true` * and [ `now` ]{@link IDateToStringOptions.now} contains a Date object equivalent to `Monday, March 8, 2017`, then: * * - If the date to convert is equivalent to `Saturday, March 6, 2017`, the resulting string is the localized * version of `Last Saturday`. * * - If the date to convert is equivalent to `Thursday, March 11, 2017`, the resulting string is the localized * version of `Next Thursday`. * * Default value is `true`. */ useWeekdayIfThisWeek?: boolean; /** * Specifies whether to omit the year from the resulting string when converting the Date object, if the year * is the current one. This option takes precedence over * [ `useLongDateFormat` ]{@link IDateToStringOptions.useLongDateFormat}. * * **Examples** * * - If the Date object to convert is equivalent to `September 22, 2017`, the resulting string does not contain * the year (e.g., `September 22`). * * - If the Date object to convert is equivalent to `September 22, 2016`, the resulting string contains the year * (e.g., `September 22, 2016`). * * Default value is `true`. */ omitYearIfCurrentOne?: boolean; /** * Specifies whether to format the resulting string in the long date format (e.g., `Friday, August 04, 2017`). * * Default value is `false`. */ useLongDateFormat?: boolean; /** * Specifies whether to include the time in the resulting string when converting the Date object (e.g. `May 15, 4:17 PM`) * if the date to convert is equivalent to [ `now` ]{@link IDateToStringOptions.now}. * * **Examples** * * If [ `includeTimeIfToday` ]{@link IDateToStringOptions.includeTimeIfToday} is `true` * and [ `now` ]{@link IDateToStringOptions.now} contains a Date object equivalent to `Monday, March 8, 2017`, then: * * - If the Date object to convert is equivalent to `2017/03/08 17:23:11`, the resulting string is `3/8/2017, 5:23 PM`. * * - If the Date object to convert is equivalent to `2017/03/09 17:23:11`, the resulting string is `3/9/2017`. * * Default value is `true`. */ includeTimeIfToday?: boolean; /** * Specifies whether to include the time in the resulting string when converting the Date object (e.g. `May 15, 4:17 PM`) * if the date to convert within a week from [ `now` ]{@link IDateToStringOptions.now}. This option takes precedence over * [ `includeTimeIfToday` ]{@link IDateToStringOptions.includeTimeIfToday}. * * **Examples** * * If [ `includeTimeIfToday` ]{@link IDateToStringOptions.includeTimeIfToday} is `true` * and [ `now` ]{@link IDateToStringOptions.now} contains a Date object equivalent to `Monday, March 8, 2017`, then: * * - If the Date object to convert is equivalent to `2017/03/08 17:23:11`, the resulting string is `3/8/2017, 5:23 PM`. * * - If the Date object to convert is equivalent to `2017/03/09 17:23:11`, the resulting string is `3/9/2017 ,5:23 PM`. * * Default value is `true`. */ includeTimeIfThisWeek?: boolean; /** * Specifies whether to always include the time in the resulting string when converting the Date object (e.g. `May 15, 4:17 PM`) * This option takes precedence over [ `includeTimeIfThisWeek` ]{@link IDateToStringOptions.includeTimeIfThisWeek}. * * **Example** * * If [ `includeTimeIfToday` ]{@link IDateToStringOptions.includeTimeIfToday} is `true` * and [ `now` ]{@link IDateToStringOptions.now} contains a Date object equivalent to `Monday, March 8, 2017`, then: * * - If the Date object to convert is equivalent to `2010/03/08 17:23:11`, the resulting string is `3/8/2010, 5:23 PM`. * * Default value is `false`. */ alwaysIncludeTime?: boolean; /** * Specifies a custom date format (e.g., dd/MM/yyyy), regardless of browser locale or any other option. * * This option uses the following syntax. All examples use the April 5th, 2018 14:15:34 time. * - `yyyy`: full length year (e.g., 2018) * - `yy`: short length year (e.g., 18) * - `MMMM`: month name (e.g., April) * - `MMM`: shortened month name (e.g., Apr) * - `MM`: month number (e.g., 04) * - `M`: single digit month number for months before October (e.g., 4) * - `dddd`: day name (e.g., Thursday) * - `ddd`: shortened day name (e.g., Thu) * - `dd`: day number (e.g., 05) * - `d`: single digit day for days before the 10th (e.g., 5) * - `hh`: hour, in the 24-hour format (e.g., 14) * - `h`: hour, in the 12-hour format (e.g., 2) * - `mm`: minutes (e.g., 15) * - `ss`: seconds (e.g., 34) */ predefinedFormat?: string; } class DefaultDateToStringOptions extends Options implements IDateToStringOptions { now: Date = moment().toDate(); useTodayYesterdayAndTomorrow = true; useWeekdayIfThisWeek = true; omitYearIfCurrentOne = true; useLongDateFormat = false; includeTimeIfToday = true; includeTimeIfThisWeek = true; alwaysIncludeTime = false; predefinedFormat: string = undefined; } /** * The `DateUtils` class exposes methods to convert strings, numbers and date objects to standard ISO 8601 Date objects, * using the correct culture, language and format. It also offers methods to convert date objects to strings. */ export class DateUtils { private static momentjsLocaleDataMap: Record<string, moment.Locale> = {}; // This function is used to call convertToStandardDate for legacy reasons. convertFromJsonDateIfNeeded was refactored to // convertToStandardDate, which would be a breaking change otherwise. static convertFromJsonDateIfNeeded(date: any): Date { return DateUtils.convertToStandardDate(date); } /** * Tries to parse an argument of any type to a standard Date object. * @param date The value to parse. Can be of any type (string, number, Date, etc.). * @returns {any} The parsed Date object, or `Invalid Date` if the `date` argument was not recognized as a valid date. */ static convertToStandardDate(date: any): Date { if (_.isDate(date)) { return moment(date).toDate(); } else if (date !== null && !isNaN(Number(date))) { return moment(Number(date)).toDate(); } else if (_.isString(date)) { const dateMoment = moment(date, 'YYYY/MM/DD@HH:mm:ssZ'); return dateMoment.toDate(); } } public static setLocale(): void { DateUtils.saveOriginalMomentLocaleData(); moment.updateLocale(DateUtils.momentjsCompatibleLocale, DateUtils.transformGlobalizeCalendarToMomentCalendar()); moment.locale(DateUtils.momentjsCompatibleLocale); } private static saveOriginalMomentLocaleData() { const locale = DateUtils.momentjsCompatibleLocale; const alreadySaved = DateUtils.momentjsLocaleDataMap[locale] != null; if (alreadySaved) { return; } DateUtils.momentjsLocaleDataMap[locale] = moment.localeData(); } /** * Creates a string from a Date object. The resulting string is in the date format required for queries. * @param date The Date object to create a string from. * @returns {string} A string corresponding to the `date` argument value, in the `YYYY/MM/DD` format. */ static dateForQuery(date: Date): string { DateUtils.setLocale(); const dateMoment = moment(date).format('YYYY/MM/DD'); return dateMoment; } /** * Creates a string from a Date object. The resulting string is in the datetime format required for queries. * @param date The Date object to create a string from. * @returns {string} A string corresponding to the `date` argument value, in the `YYYY/MM/DD@HH:mm:ss` format. */ static dateTimeForQuery(date: Date): string { DateUtils.setLocale(); const dateMoment = moment(date).format('YYYY/MM/DD@HH:mm:ss'); return dateMoment; } /** * Creates a cropped version of a Date object. The resulting object contains no time information. * @param date The original Date object to create a cropped Date object from. * @returns {Date} A cropped Date object corresponding to the `date` argument value, excluding its time information. */ static keepOnlyDatePart(date: Date): Date { DateUtils.setLocale(); const dateMoment = moment(date); return new Date(dateMoment.year(), dateMoment.month(), dateMoment.date()); } /** * Creates an offset version of a Date object. The offset is counted in days. * @param date The original Date object to create an offset Date object from. * @param offset The number of days to add to (or subtract from) the `date` argument. * @returns {Date} An offset Date object corresponding to the `date` argument value plus the `offset` value. */ static offsetDateByDays(date: Date, offset: number): Date { return moment(date).add(offset, 'days').toDate(); } private static isTodayYesterdayOrTomorrow(d: Date, options?: IDateToStringOptions): boolean { const dateOnly = moment(DateUtils.keepOnlyDatePart(d)); const today = moment(DateUtils.keepOnlyDatePart(options.now)); const daysDifference = dateOnly.diff(today, 'days'); return daysDifference == 0 || daysDifference == 1 || daysDifference == -1; } private static getMomentJsFormat(format: string) { let correctedFormat = format; const fourLowercaseY = DateUtils.buildRegexMatchingExactCharSequence('y', 4); correctedFormat = correctedFormat.replace(fourLowercaseY, '$1YYYY'); const twoLowercaseY = DateUtils.buildRegexMatchingExactCharSequence('y', 2); correctedFormat = correctedFormat.replace(twoLowercaseY, '$1YY'); const twoLowercaseD = DateUtils.buildRegexMatchingExactCharSequence('d', 2); correctedFormat = correctedFormat.replace(twoLowercaseD, '$1DD'); const oneLowercaseD = DateUtils.buildRegexMatchingExactCharSequence('d', 1); correctedFormat = correctedFormat.replace(oneLowercaseD, '$1D'); const twoLowercaseH = DateUtils.buildRegexMatchingExactCharSequence('h', 2); correctedFormat = correctedFormat.replace(twoLowercaseH, '$1H'); return correctedFormat; } private static buildRegexMatchingExactCharSequence(char: string, sequenceLength: number) { const negativeNonCapturingGroup = `(?:([^${char}]|^))`; // look-behind is not supported in Firefox const charSequence = `${char}{${sequenceLength}}`; const negativeLookAhead = `(?!${char})`; const exactSequence = `${negativeNonCapturingGroup}${charSequence}${negativeLookAhead}`; return new RegExp(exactSequence, 'g'); } /** * Creates a string from a Date object. The resulting string is formatted according to a set of options. * This method calls [ `keepOnlyDatePart` ]{@link DateUtils.keepOnlyDatePart} to remove time information from the date. * If you need to create a timestamp, use the [ `dateTimeToString` ]{@link DateUtils.dateTimeToString} method instead. * @param date The Date object to create a string from. * @param options The set of options to apply when formatting the resulting string. If you do not specify a value for * this parameter, the method uses a default set of options. * @returns {string} A date string corresponding to the `date` argument value, formatted according to the specified `options`. */ static dateToString(date: Date, options?: IDateToStringOptions): string { DateUtils.setLocale(); if (Utils.isNullOrUndefined(date)) { new Logger(this).warn(`Impossible to format an undefined or null date.`); return ''; } options = new DefaultDateToStringOptions().merge(options); const dateOnly = moment(DateUtils.keepOnlyDatePart(date)); const today = moment(DateUtils.keepOnlyDatePart(options.now)); if (options.predefinedFormat) { return dateOnly.format(this.getMomentJsFormat(options.predefinedFormat)); } if (options.useTodayYesterdayAndTomorrow) { if (DateUtils.isTodayYesterdayOrTomorrow(date, options)) { return moment(dateOnly).calendar(moment(today)); } } const isThisWeek = dateOnly.diff(moment(today), 'weeks') == 0; if (options.useWeekdayIfThisWeek && isThisWeek) { if (dateOnly.valueOf() > today.valueOf()) { return l('NextDay', l(dateOnly.format('dddd'))); } else if (dateOnly.valueOf() < today.valueOf()) { return l('LastDay', l(dateOnly.format('dddd'))); } else { return dateOnly.format('dddd'); } } if (options.omitYearIfCurrentOne && dateOnly.year() === today.year()) { return dateOnly.format('LL'); } if (options.useLongDateFormat) { return dateOnly.format(this.longDateFormat); } return dateOnly.format('L'); } private static get longDateFormat() { const momentLocaleData = DateUtils.momentjsLocaleDataMap[DateUtils.momentjsCompatibleLocale]; return momentLocaleData .longDateFormat('LLLL') .replace(/[h:mA]/g, '') .trim(); } /** * Creates a string from a Date object. The string corresponds to the time information of the Date object. * @param date The Date object to create a string from. * @param options The set of options to apply when formatting the resulting string. If you do not specify a * value for this parameter, the method uses a default set of options. * @returns {string} A string containing the time information of the `date` argument, and formatted according to the specified `options`. */ static timeToString(date: Date, options?: IDateToStringOptions): string { if (Utils.isNullOrUndefined(date)) { return ''; } return moment(date).format('h:mm A'); } /** * Creates a string from a Date object. The resulting string is formatted according to a set of options. * This method calls [ `timeToString` ]{@link DateUtils.timeToString} to add time information to the date. * If you need to create a date string without a timestamp, use the [ `dateToString` ]{@link DateUtils.dateToString} method instead. * @param date The date object to create a string from. * @param options The set of options to apply when formatting the resulting string. If you do not specify a value for * this parameter, the method uses a default set of options. * @returns {string} A date string corresponding to the `date` argument value, formatted according to the specified `options`. */ static dateTimeToString(date: Date, options?: IDateToStringOptions): string { DateUtils.setLocale(); options = new DefaultDateToStringOptions().merge(options); if (Utils.isNullOrUndefined(date)) { new Logger(this).warn(`Impossible to format an undefined or null date.`); return ''; } if (!moment(date).isValid()) { new Logger(this).warn(`Impossible to format an invalid date: ${date}`); return ''; } if (options.predefinedFormat) { return moment(date).format(this.getMomentJsFormat(options.predefinedFormat)); } const today = DateUtils.keepOnlyDatePart(options.now); const datePart = DateUtils.dateToString(date, options); const dateWithoutTime = DateUtils.keepOnlyDatePart(date); const isThisWeek = moment(date).diff(moment(today), 'weeks') == 0; const isToday = dateWithoutTime.valueOf() == today.valueOf(); const shouldIncludeTime = () => { if (options.alwaysIncludeTime) { return true; } if (options.includeTimeIfThisWeek && isThisWeek) { return true; } if (options.includeTimeIfToday && isToday) { return true; } return false; }; if (shouldIncludeTime()) { return `${datePart}, ${DateUtils.timeToString(date)}`; } return datePart; } /** * Creates a string from a number. The resulting string is the localized name of the month that corresponds * to this number (e.g., `0` results in the localized version of `January`). * @param month The number to create a string from. Minimum value is `0` (which corresponds to `January`). Maximum * value is `11` (which corresponds to `December`). * @returns {string} A string whose value is the localized name of the corresponding `month`. */ static monthToString(month: number): string { DateUtils.setLocale(); const date = moment(new Date(1980, month)).toDate(); return moment(date).format('MMMM'); } /** * Validates whether a value is an instance of Date. * @param date The value to verify. * @returns {boolean} `true` if the `date` argument is an instance of Date; `false` otherwise. */ static isValid(date: any) { DateUtils.setLocale(); if (date instanceof Date) { return !isNaN(date.getTime()); } return false; } /** * Creates a string from two Date objects. The resulting string corresponds to the amount of time between those two dates. * @param from The Date object which contains the "oldest" value. * @param to The Date object which contains the "newest" value. * @returns {any} A string whose value corresponds to the amount of time between `from` and `to`, * or an empty string if either argument was undefined. */ static timeBetween(from: Date, to: Date) { if (Utils.isNullOrUndefined(from) || Utils.isNullOrUndefined(to)) { return ''; } return ( ('0' + ((moment(to).valueOf() - moment(from).valueOf()) / (1000 * 60 * 60)).toFixed()).slice(-2) + ':' + ('0' + (((moment(to).valueOf() - moment(from).valueOf()) % (1000 * 60 * 60)) / (1000 * 60)).toFixed()).slice(-2) + ':' + ('0' + (((moment(to).valueOf() - moment(from).valueOf()) % (1000 * 60)) / 1000).toFixed()).slice(-2) ); } static get currentGlobalizeCalendar(): GlobalizeCalendar { return Globalize.culture(DateUtils.currentLocale).calendar as GlobalizeCalendar; } static get currentLocale() { return String['locale']; } static get momentjsCompatibleLocale(): string { let currentLocale = DateUtils.currentLocale; // Our cultures.js directory contains 'no' which is the equivalent to 'nn' for momentJS if (currentLocale.toLowerCase() == 'no') { currentLocale = 'nn'; } else if (currentLocale.toLowerCase() == 'es-es') { // Our cultures.js directory contains 'es-es' which is the equivalent to 'es' for momentJS currentLocale = 'es'; } return currentLocale; } static transformGlobalizeCalendarToMomentCalendar(): moment.LocaleSpecification { const cldrToMomentFormat = (cldrFormat: string) => { return cldrFormat.replace(/y/g, 'Y').replace(/d/g, 'D'); }; return { months: DateUtils.currentGlobalizeCalendar.months.names, monthsShort: DateUtils.currentGlobalizeCalendar.months.namesAbbr, weekdays: DateUtils.currentGlobalizeCalendar.days.names, weekdaysShort: DateUtils.currentGlobalizeCalendar.days.namesAbbr, weekdaysMin: DateUtils.currentGlobalizeCalendar.days.namesShort, longDateFormat: { LT: cldrToMomentFormat(DateUtils.currentGlobalizeCalendar.patterns.t), LTS: cldrToMomentFormat(DateUtils.currentGlobalizeCalendar.patterns.T), L: cldrToMomentFormat(DateUtils.currentGlobalizeCalendar.patterns.d), LL: cldrToMomentFormat(DateUtils.currentGlobalizeCalendar.patterns.M), LLL: cldrToMomentFormat(DateUtils.currentGlobalizeCalendar.patterns.f), LLLL: cldrToMomentFormat(DateUtils.currentGlobalizeCalendar.patterns.F) }, calendar: { lastDay: `[${l('Yesterday')}]`, sameDay: `[${l('Today')}]`, nextDay: `[${l('Tomorrow')}]` } }; } }