mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Change datepicker in DateEditor to use moment format, show AltText in DateEditor
Summary: - Rather than translate from moment format to that of bootstrap-datepicker, use the customization methods to format datepicker dates using moment directly. - Fix issue with parseDate() when format includes tokens like Mo or Do - Fix issue in parseDateTime() that could produce an off-by-one error in date depending on local timezone. - When opening DateEditor, show AltText value if present. - Add crossorigin=anonymous to scripts that were missing it (including bootstrap-datepicker), to ensure that errors from them are reported properly rather than as 'Script error.' Test Plan: Added test cases to parseDate() test for low-level fixes; added a browser test for the fixed DateEditor behavior. Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3169
This commit is contained in:
parent
faec8177ab
commit
7a6d726daa
@ -34,13 +34,11 @@ function DateEditor(options) {
|
|||||||
this.dateFormat = options.field.widgetOptionsJson.peek().dateFormat;
|
this.dateFormat = options.field.widgetOptionsJson.peek().dateFormat;
|
||||||
this.locale = options.field.documentSettings.peek().locale;
|
this.locale = options.field.documentSettings.peek().locale;
|
||||||
|
|
||||||
// Strip moment format string to remove markers unsupported by the datepicker.
|
// Update moment format string to represent a date unambiguously.
|
||||||
this.safeFormat = DateEditor.parseMomentToSafe(this.dateFormat);
|
this.safeFormat = makeFullMomentFormat(this.dateFormat);
|
||||||
|
|
||||||
this._readonly = options.readonly;
|
|
||||||
|
|
||||||
// Use the default local timezone to format the placeholder date.
|
// Use the default local timezone to format the placeholder date.
|
||||||
let defaultTimezone = moment.tz.guess();
|
const defaultTimezone = moment.tz.guess();
|
||||||
let placeholder = moment.tz(defaultTimezone).format(this.safeFormat);
|
let placeholder = moment.tz(defaultTimezone).format(this.safeFormat);
|
||||||
if (options.readonly) {
|
if (options.readonly) {
|
||||||
// clear placeholder for readonly mode
|
// clear placeholder for readonly mode
|
||||||
@ -48,13 +46,7 @@ function DateEditor(options) {
|
|||||||
}
|
}
|
||||||
TextEditor.call(this, _.defaults(options, { placeholder: placeholder }));
|
TextEditor.call(this, _.defaults(options, { placeholder: placeholder }));
|
||||||
|
|
||||||
const isValid = _.isNumber(options.cellValue);
|
const cellValue = this.formatValue(options.cellValue, this.safeFormat, true);
|
||||||
const formatted = this.formatValue(options.cellValue, this.safeFormat);
|
|
||||||
// Formatted value will be empty if a cell contains an error,
|
|
||||||
// but for a readonly mode we actually want to show what user typed
|
|
||||||
// into the cell.
|
|
||||||
const readonlyValue = isValid ? formatted : options.cellValue;
|
|
||||||
const cellValue = options.readonly ? readonlyValue : formatted;
|
|
||||||
|
|
||||||
// Set the edited value, if not explicitly given, to the formatted version of cellValue.
|
// Set the edited value, if not explicitly given, to the formatted version of cellValue.
|
||||||
this.textInput.value = gutil.undef(options.state, options.editValue, cellValue);
|
this.textInput.value = gutil.undef(options.state, options.editValue, cellValue);
|
||||||
@ -74,8 +66,17 @@ function DateEditor(options) {
|
|||||||
// or by script tag, i.e.
|
// or by script tag, i.e.
|
||||||
// <script src="bootstrap-datepicker/dist/locales/bootstrap-datepicker.pl.min.js"></script>
|
// <script src="bootstrap-datepicker/dist/locales/bootstrap-datepicker.pl.min.js"></script>
|
||||||
language : this.getLanguage(),
|
language : this.getLanguage(),
|
||||||
// Convert the stripped format string to one suitable for the datepicker.
|
// Use the stripped format converted to one suitable for the datepicker.
|
||||||
format: DateEditor.parseSafeToCalendar(this.safeFormat)
|
format: {
|
||||||
|
toDisplay: (date, format, language) => moment.utc(date).format(this.safeFormat),
|
||||||
|
toValue: (date, format, language) => {
|
||||||
|
const timestampSec = parseDate(date, {
|
||||||
|
dateFormat: this.safeFormat,
|
||||||
|
timezone: this.timezone,
|
||||||
|
});
|
||||||
|
return (timestampSec === null) ? null : new Date(timestampSec * 1000);
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.autoDisposeCallback(() => this._datePickerWidget.datepicker('destroy'));
|
this.autoDisposeCallback(() => this._datePickerWidget.datepicker('destroy'));
|
||||||
|
|
||||||
@ -137,43 +138,15 @@ DateEditor.prototype._allowKeyboardNav = function(bool) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Moment value formatting helper.
|
// Moment value formatting helper.
|
||||||
DateEditor.prototype.formatValue = function(value, formatString) {
|
DateEditor.prototype.formatValue = function(value, formatString, shouldFallBackToValue) {
|
||||||
if (_.isNumber(value) && formatString) {
|
if (_.isNumber(value) && formatString) {
|
||||||
return moment.tz(value*1000, this.timezone).format(formatString);
|
return moment.tz(value*1000, this.timezone).format(formatString);
|
||||||
} else {
|
} else {
|
||||||
return "";
|
// If value is AltText, return it unchanged. This way we can see it and edit in the editor.
|
||||||
|
return (shouldFallBackToValue && typeof value === 'string') ? value : "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Formats Moment string to remove markers unsupported by the datepicker.
|
|
||||||
// Moment reference: http://momentjs.com/docs/#/displaying/
|
|
||||||
DateEditor.parseMomentToSafe = function(mFormat) {
|
|
||||||
// Remove markers not representing year, month, or date, and also DDD, DDDo, DDDD, d, do,
|
|
||||||
// (and following whitespace/punctuation) since they are unsupported by the datepicker.
|
|
||||||
mFormat = mFormat.replace(/\b(?:[^DMY\W]+|D{3,4}o*)\b\W+/g, '');
|
|
||||||
// Convert other markers unsupported by the datepicker to similar supported markers.
|
|
||||||
mFormat = mFormat.replace(/\b([MD])o\b/g, '$1'); // Mo -> M, Do -> D
|
|
||||||
// Check which information the format contains. Format is only valid for editing if it
|
|
||||||
// contains day, month and year information.
|
|
||||||
var dayRe = /D{1,2}/g;
|
|
||||||
var monthRe = /M{1,4}/g;
|
|
||||||
var yearRe = /Y{2,4}/g;
|
|
||||||
var valid = dayRe.test(mFormat) && monthRe.test(mFormat) && yearRe.test(mFormat);
|
|
||||||
return valid ? mFormat : 'YYYY-MM-DD'; // Use basic format if given is invalid.
|
|
||||||
};
|
|
||||||
|
|
||||||
// Formats Moment string without datepicker unsupported markers for the datepicker.
|
|
||||||
// Datepicker reference: http://bootstrap-datepicker.readthedocs.org/en/latest/options.html#format
|
|
||||||
DateEditor.parseSafeToCalendar = function(sFormat) {
|
|
||||||
// M -> m, MM -> mm, D -> d, DD -> dd, YY -> yy, YYYY -> yyyy
|
|
||||||
sFormat = sFormat.replace(/\b(?:[MD]{1,2}|Y{2,4})\b/g, function(x) {
|
|
||||||
return x.toLowerCase();
|
|
||||||
});
|
|
||||||
sFormat = sFormat.replace(/\bM{2}(?=M{1,2}\b)/g, ''); // MMM -> M, MMMM -> MM
|
|
||||||
sFormat = sFormat.replace(/\bddd\b/g, 'D'); // ddd -> D
|
|
||||||
return sFormat.replace(/\bdddd\b/g, 'DD'); // dddd -> DD
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gets the language based on the current locale.
|
// Gets the language based on the current locale.
|
||||||
DateEditor.prototype.getLanguage = function() {
|
DateEditor.prototype.getLanguage = function() {
|
||||||
// this requires a polyfill, i.e. https://www.npmjs.com/package/@formatjs/intl-locale
|
// this requires a polyfill, i.e. https://www.npmjs.com/package/@formatjs/intl-locale
|
||||||
@ -182,5 +155,17 @@ DateEditor.prototype.getLanguage = function() {
|
|||||||
return this.locale.substr(0, this.locale.indexOf("-"));
|
return this.locale.substr(0, this.locale.indexOf("-"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates the given Moment format to specify a complete date, so that the datepicker sees an
|
||||||
|
// unambiguous date in the textbox input. If the format is incomplete, fall back to YYYY-MM-DD.
|
||||||
|
function makeFullMomentFormat(mFormat) {
|
||||||
|
let safeFormat = mFormat;
|
||||||
|
if (!safeFormat.includes('Y')) {
|
||||||
|
safeFormat += " YYYY";
|
||||||
|
}
|
||||||
|
if (!safeFormat.includes('D') || !safeFormat.includes('M')) {
|
||||||
|
safeFormat = 'YYYY-MM-DD';
|
||||||
|
}
|
||||||
|
return safeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = DateEditor;
|
module.exports = DateEditor;
|
||||||
|
@ -39,7 +39,7 @@ function DateTimeEditor(options) {
|
|||||||
this._dateInput = this.textInput;
|
this._dateInput = this.textInput;
|
||||||
|
|
||||||
const isValid = _.isNumber(options.cellValue);
|
const isValid = _.isNumber(options.cellValue);
|
||||||
const formatted = this.formatValue(options.cellValue, this._timeFormat);
|
const formatted = this.formatValue(options.cellValue, this._timeFormat, false);
|
||||||
// Use a placeholder of 12:00am, since that is the autofill time value.
|
// Use a placeholder of 12:00am, since that is the autofill time value.
|
||||||
const placeholder = moment.tz('0', 'H', this.timezone).format(this._timeFormat);
|
const placeholder = moment.tz('0', 'H', this.timezone).format(this._timeFormat);
|
||||||
|
|
||||||
|
@ -200,7 +200,9 @@ export function parseDateTime(dateTime: string, options: ParseOptions): number |
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateString = moment.unix(date).format("YYYY-MM-DD");
|
// date is a timestamp of midnight in UTC, so to get a formatted representation (for parsing
|
||||||
|
// together with time), take care to interpret it in UTC.
|
||||||
|
const dateString = moment.unix(date).utc().format("YYYY-MM-DD");
|
||||||
dateTime = dateString + ' ' + parsedTime.time + tzOffset;
|
dateTime = dateString + ' ' + parsedTime.time + tzOffset;
|
||||||
const fullFormat = "YYYY-MM-DD HH:mm:ss" + (tzOffset ? 'Z' : '');
|
const fullFormat = "YYYY-MM-DD HH:mm:ss" + (tzOffset ? 'Z' : '');
|
||||||
return moment.tz(dateTime, fullFormat, true, timezone).valueOf() / 1000;
|
return moment.tz(dateTime, fullFormat, true, timezone).valueOf() / 1000;
|
||||||
@ -213,7 +215,7 @@ export function parseDateTime(dateTime: string, options: ParseOptions): number |
|
|||||||
// feature.
|
// feature.
|
||||||
function _getPartialFormat(input: string, format: string): string {
|
function _getPartialFormat(input: string, format: string): string {
|
||||||
// Define a regular expression to match contiguous non-separators.
|
// Define a regular expression to match contiguous non-separators.
|
||||||
const re = /Y+|M+|D+|[a-zA-Z0-9]+/g;
|
const re = /Y+|M+o?|D+o?|[a-zA-Z0-9]+/ig;
|
||||||
// Count the number of meaningful parts in the input.
|
// Count the number of meaningful parts in the input.
|
||||||
const numInputParts = input.match(re)?.length || 0;
|
const numInputParts = input.match(re)?.length || 0;
|
||||||
|
|
||||||
|
@ -57,12 +57,12 @@
|
|||||||
|
|
||||||
<!-- INSERT CONFIG -->
|
<!-- INSERT CONFIG -->
|
||||||
|
|
||||||
<script src="jquery/dist/jquery.min.js"></script>
|
<script src="jquery/dist/jquery.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="jqueryui/jquery-ui.min.js"></script>
|
<script src="jqueryui/jquery-ui.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="bootstrap/dist/js/bootstrap.min.js"></script>
|
<script src="bootstrap/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
|
<script src="bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="main.bundle.js" crossorigin="anonymous"></script>
|
<script src="main.bundle.js" crossorigin="anonymous"></script>
|
||||||
<script type="application/javascript" src="browser-check.js"></script>
|
<script type="application/javascript" src="browser-check.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1012,7 +1012,7 @@ export async function setType(type: RegExp, options: {skipWait?: boolean} = {})
|
|||||||
await toggleSidePanel('right', 'open');
|
await toggleSidePanel('right', 'open');
|
||||||
await driver.find('.test-right-tab-field').click();
|
await driver.find('.test-right-tab-field').click();
|
||||||
await driver.find('.test-fbuilder-type-select').click();
|
await driver.find('.test-fbuilder-type-select').click();
|
||||||
await driver.findContent('.test-select-menu .test-select-row', type).click();
|
await driver.findContentWait('.test-select-menu .test-select-row', type, 200).click();
|
||||||
if (!options.skipWait) { await waitForServer(); }
|
if (!options.skipWait) { await waitForServer(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1710,7 +1710,7 @@ export async function getDateFormat(): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export async function setDateFormat(format: string) {
|
export async function setDateFormat(format: string) {
|
||||||
await driver.find('[data-test-id=Widget_dateFormat]').click();
|
await driver.find('[data-test-id=Widget_dateFormat]').click();
|
||||||
await driver.findContent('.test-select-menu .test-select-row', format).click();
|
await driver.findContentWait('.test-select-menu .test-select-row', format, 200).click();
|
||||||
await waitForServer();
|
await waitForServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user