mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
|
// tslint:disable:max-classes-per-file
|
||
|
|
||
|
import {CellValue} from 'app/common/DocActions';
|
||
|
import * as gristTypes from 'app/common/gristTypes';
|
||
|
import * as gutil from 'app/common/gutil';
|
||
|
import {buildNumberFormat, NumberFormatOptions} from 'app/common/NumberFormat';
|
||
|
import * as moment from 'moment-timezone';
|
||
|
|
||
|
// Some text to show on cells whose values are pending.
|
||
|
export const PENDING_DATA_PLACEHOLDER = "Loading...";
|
||
|
|
||
|
/**
|
||
|
* Formats a custom object received as a value in a DocAction, as "Constructor(args...)".
|
||
|
* E.g. ["Foo", 1, 2, 3] becomes the string "Foo(1, 2, 3)".
|
||
|
*/
|
||
|
export function formatObject(args: [string, ...any[]]): string {
|
||
|
const objType = args[0], objArgs = args.slice(1);
|
||
|
switch (objType) {
|
||
|
case 'L': return JSON.stringify(objArgs);
|
||
|
// First arg is seconds since epoch (moment takes ms), second arg is timezone
|
||
|
case 'D': return moment.tz(objArgs[0] * 1000, objArgs[1]).format("YYYY-MM-DD HH:mm:ssZ");
|
||
|
case 'd': return moment.tz(objArgs[0] * 1000, 'UTC').format("YYYY-MM-DD");
|
||
|
case 'R': return `${objArgs[0]}[${objArgs[1]}]`;
|
||
|
case 'E': return gristTypes.formatError(args);
|
||
|
case 'P': return PENDING_DATA_PLACEHOLDER;
|
||
|
}
|
||
|
return objType + "(" + JSON.stringify(objArgs).slice(1, -1) + ")";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Formats a value of unknown type, using formatObject() for encoded objects.
|
||
|
*/
|
||
|
export function formatUnknown(value: any): string {
|
||
|
return gristTypes.isObject(value) ? formatObject(value) : (value == null ? "" : String(value));
|
||
|
}
|
||
|
|
||
|
export type IsRightTypeFunc = (value: CellValue) => boolean;
|
||
|
|
||
|
export class BaseFormatter {
|
||
|
public readonly isRightType: IsRightTypeFunc;
|
||
|
|
||
|
constructor(public type: string, public opts: object) {
|
||
|
this.isRightType = gristTypes.isRightType(gristTypes.extractTypeFromColType(type)) ||
|
||
|
gristTypes.isRightType('Any')!;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Formats a value that matches the type of this formatter. This should be overridden by derived
|
||
|
* classes to handle values in formatter-specific ways.
|
||
|
*/
|
||
|
public format(value: any): string {
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Formats using this.format() if a value is of the right type for this formatter, or using
|
||
|
* AnyFormatter otherwise. This method the recommended API. There is no need to override it.
|
||
|
*/
|
||
|
public formatAny(value: any): string {
|
||
|
return this.isRightType(value) ? this.format(value) : formatUnknown(value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class AnyFormatter extends BaseFormatter {
|
||
|
public format(value: any): string {
|
||
|
return formatUnknown(value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class NumericFormatter extends BaseFormatter {
|
||
|
private _numFormat: Intl.NumberFormat;
|
||
|
private _formatter: (val: number) => string;
|
||
|
|
||
|
constructor(type: string, options: NumberFormatOptions) {
|
||
|
super(type, options);
|
||
|
this._numFormat = buildNumberFormat(options);
|
||
|
this._formatter = (options.numSign === 'parens') ? this._formatParens : this._formatPlain;
|
||
|
}
|
||
|
|
||
|
public format(value: any): string {
|
||
|
return value === null ? '' : this._formatter(value);
|
||
|
}
|
||
|
|
||
|
public _formatPlain(value: number): string {
|
||
|
return this._numFormat.format(value);
|
||
|
}
|
||
|
|
||
|
public _formatParens(value: number): string {
|
||
|
// Surround positive numbers with spaces to align them visually to parenthesized numbers.
|
||
|
return (value >= 0) ?
|
||
|
` ${this._numFormat.format(value)} ` :
|
||
|
`(${this._numFormat.format(-value)})`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class IntFormatter extends NumericFormatter {
|
||
|
constructor(type: string, opts: object) {
|
||
|
super(type, {decimals: 0, ...opts});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DateFormatter extends BaseFormatter {
|
||
|
private _dateTimeFormat: string;
|
||
|
private _timezone: string;
|
||
|
|
||
|
constructor(type: string, opts: {dateFormat?: string}, timezone: string = 'UTC') {
|
||
|
super(type, opts);
|
||
|
this._dateTimeFormat = opts.dateFormat || 'YYYY-MM-DD';
|
||
|
this._timezone = timezone;
|
||
|
}
|
||
|
|
||
|
public format(value: any): string {
|
||
|
if (value === null) { return ''; }
|
||
|
const time = moment.tz(value * 1000, this._timezone);
|
||
|
return time.format(this._dateTimeFormat);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DateTimeFormatter extends DateFormatter {
|
||
|
constructor(type: string, opts: {dateFormat?: string; timeFormat?: string}) {
|
||
|
const timezone = gutil.removePrefix(type, "DateTime:") || '';
|
||
|
const timeFormat = opts.timeFormat === undefined ? 'h:mma' : opts.timeFormat;
|
||
|
const dateFormat = (opts.dateFormat || 'YYYY-MM-DD') + " " + timeFormat;
|
||
|
super(type, {dateFormat}, timezone);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const formatters: {[name: string]: typeof BaseFormatter} = {
|
||
|
Numeric: NumericFormatter,
|
||
|
Int: IntFormatter,
|
||
|
Bool: BaseFormatter,
|
||
|
Date: DateFormatter,
|
||
|
DateTime: DateTimeFormatter,
|
||
|
// We don't list anything that maps to AnyFormatter, since that's the default.
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Takes column type and widget options and returns a constructor with a format function that can
|
||
|
* properly convert a value passed to it into the right format for that column.
|
||
|
*/
|
||
|
export function createFormatter(type: string, opts: object): BaseFormatter {
|
||
|
const ctor = formatters[gristTypes.extractTypeFromColType(type)] || AnyFormatter;
|
||
|
return new ctor(type, opts);
|
||
|
}
|