mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
d35574c198
Summary: Addresses request https://github.com/gristlabs/grist-core/issues/443 The bootstrap-datepicker used for the Date/DateTime dropdown calendar does support a number of locales. This diff loads them on-demand, if available, based on the document's locale setting. Also: - Improves NewBaseEditor typings, reduces some casts, adds comments. - Converts DateEditor and DateTimeEditor to typescript. - Moves DateEditor nbrowser test to core. Test Plan: Added a test case for locales to DateEditor test. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4335
174 lines
6.5 KiB
TypeScript
174 lines
6.5 KiB
TypeScript
import {DateEditor} from 'app/client/widgets/DateEditor';
|
|
import {FieldOptions} from 'app/client/widgets/NewBaseEditor';
|
|
import {removePrefix} from 'app/common/gutil';
|
|
import {parseDate} from 'app/common/parseDate';
|
|
|
|
import moment from 'moment-timezone';
|
|
import {dom} from 'grainjs';
|
|
|
|
/**
|
|
* DateTimeEditor - Editor for DateTime type. Includes a dropdown datepicker.
|
|
* See reference: http://bootstrap-datepicker.readthedocs.org/en/latest/index.html
|
|
*/
|
|
export class DateTimeEditor extends DateEditor {
|
|
private _timeFormat: string|undefined;
|
|
private _dateSizer: HTMLElement;
|
|
private _timeSizer: HTMLElement;
|
|
private _dateInput: HTMLTextAreaElement;
|
|
private _timeInput: HTMLTextAreaElement;
|
|
|
|
constructor(options: FieldOptions) {
|
|
// Get the timezone from the end of the type string.
|
|
const timezone = removePrefix(options.field.column().type(), "DateTime:");
|
|
|
|
// Adjust the command group, but not for readonly mode.
|
|
if (!options.readonly) {
|
|
const origCommands = options.commands;
|
|
options.commands = {
|
|
...origCommands,
|
|
prevField: () => this._focusIndex() === 1 ? this._setFocus(0) : origCommands.prevField(),
|
|
nextField: () => this._focusIndex() === 0 ? this._setFocus(1) : origCommands.nextField(),
|
|
};
|
|
}
|
|
|
|
// Call the superclass.
|
|
super(options, timezone || 'UTC');
|
|
this._timeFormat = this.options.field.widgetOptionsJson.peek().timeFormat;
|
|
|
|
// To reuse code, this knows all about the DOM that DateEditor builds (using TextEditor), and
|
|
// modifies that to be two side-by-side textareas.
|
|
this._dateSizer = this.contentSizer; // For consistency with _timeSizer.
|
|
this._dateInput = this.textInput; // For consistency with _timeInput.
|
|
|
|
const isValid = (typeof options.cellValue === 'number');
|
|
const formatted = this.formatValue(options.cellValue, this._timeFormat, false);
|
|
// Use a placeholder of 12:00am, since that is the autofill time value.
|
|
const placeholder = moment.tz('0', 'H', this.timezone).format(this._timeFormat);
|
|
|
|
// for readonly
|
|
if (options.readonly) {
|
|
if (!isValid) {
|
|
// do nothing - DateEditor will show correct error
|
|
} else {
|
|
// append time format or a placeholder
|
|
const time = (formatted || placeholder);
|
|
const sep = time ? ' ' : '';
|
|
this.textInput.value = this.textInput.value + sep + time;
|
|
}
|
|
} else {
|
|
const widgetElem = this.getDom();
|
|
dom.update(widgetElem, dom.cls('celleditor_datetime'));
|
|
dom.update(this.cellEditorDiv, dom.cls('celleditor_datetime_editor'));
|
|
widgetElem.appendChild(
|
|
dom('div',
|
|
dom.cls('celleditor_cursor_editor'),
|
|
dom.cls('celleditor_datetime_editor'),
|
|
this._timeSizer = dom('div', dom.cls('celleditor_content_measure')),
|
|
this._timeInput = dom('textarea', dom.cls('celleditor_text_editor'),
|
|
dom.attr('placeholder', placeholder),
|
|
dom.prop('value', formatted),
|
|
this.commandGroup.attach(),
|
|
dom.on('input', () => this._onChange())
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
// If the edit value is encoded json, use those values as a starting point
|
|
if (typeof options.state == 'string') {
|
|
try {
|
|
const { date, time } = JSON.parse(options.state);
|
|
this._dateInput.value = date;
|
|
this._timeInput.value = time;
|
|
this._onChange();
|
|
} catch(e) {
|
|
console.error("DateTimeEditor can't restore its previous state");
|
|
}
|
|
}
|
|
}
|
|
|
|
public getCellValue() {
|
|
const date = this._dateInput.value;
|
|
const time = this._timeInput.value;
|
|
const timestamp = parseDate(date, {
|
|
dateFormat: this.safeFormat,
|
|
time: time,
|
|
timeFormat: this._timeFormat,
|
|
timezone: this.timezone
|
|
});
|
|
return timestamp !== null ? timestamp :
|
|
(date && time ? `${date} ${time}` : date || time);
|
|
}
|
|
|
|
public setSizerLimits() {
|
|
const maxSize = this.editorPlacement.calcSize({width: Infinity, height: Infinity}, {calcOnly: true});
|
|
if (this.options.readonly) {
|
|
return;
|
|
}
|
|
this._dateSizer.style.maxWidth =
|
|
this._timeSizer.style.maxWidth = Math.ceil(maxSize.width / 2 - 6) + 'px';
|
|
}
|
|
|
|
/**
|
|
* Overrides the resizing function in TextEditor.
|
|
*/
|
|
protected resizeInput() {
|
|
|
|
// for readonly field, we will use logic from a super class
|
|
if (this.options.readonly) {
|
|
return super.resizeInput();
|
|
}
|
|
// Use the size calculation provided in options.calcSize (that takes into account cell size and
|
|
// screen size), with both date and time parts as the input. The resulting size is applied to
|
|
// the parent (containing date + time), with date and time each expanding or shrinking from the
|
|
// measured sizes using flexbox logic.
|
|
this._dateSizer.textContent = this._dateInput.value;
|
|
this._timeSizer.textContent = this._timeInput.value;
|
|
const dateRect = this._dateSizer.getBoundingClientRect();
|
|
const timeRect = this._timeSizer.getBoundingClientRect();
|
|
// Textboxes get 3px of padding on left/right/top (see TextEditor.css); we specify it manually
|
|
// since editorPlacement can't do a good job figuring it out with the flexbox arrangement.
|
|
const size = this.editorPlacement.calcSize({
|
|
width: dateRect.width + timeRect.width + 12,
|
|
height: Math.max(dateRect.height, timeRect.height) + 3
|
|
});
|
|
this.getDom().style.width = size.width + 'px';
|
|
this._dateInput.parentElement!.style.flexBasis = (dateRect.width + 6) + 'px';
|
|
this._timeInput.parentElement!.style.flexBasis = (timeRect.width + 6) + 'px';
|
|
this._dateInput.style.height = Math.ceil(size.height - 3) + 'px';
|
|
this._timeInput.style.height = Math.ceil(size.height - 3) + 'px';
|
|
}
|
|
|
|
/**
|
|
* Returns which element has focus: 0 if date, 1 if time, null if neither.
|
|
*/
|
|
private _focusIndex() {
|
|
return document.activeElement === this._dateInput ? 0 :
|
|
(document.activeElement === this._timeInput ? 1 : null);
|
|
}
|
|
|
|
/**
|
|
* Sets focus to date if index is 0, or time if index is 1.
|
|
*/
|
|
private _setFocus(index: 0|1) {
|
|
const elem = (index === 0 ? this._dateInput : (index === 1 ? this._timeInput : null));
|
|
if (elem) {
|
|
elem.focus();
|
|
elem.selectionStart = 0;
|
|
elem.selectionEnd = elem.value.length;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Occurs when user types something into the editor
|
|
*/
|
|
private _onChange() {
|
|
this.resizeInput();
|
|
|
|
// store editor state as an encoded JSON string
|
|
const date = this._dateInput.value;
|
|
const time = this._timeInput.value;
|
|
this.editorState.set(JSON.stringify({ date, time}));
|
|
}
|
|
}
|