UNPKG

@blockly/field-date

Version:

A Blockly date picker field that uses the browser's date picker.

261 lines (235 loc) 8.65 kB
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Plugin overview. */ import * as Blockly from 'blockly/core'; import {getLocaleDateString} from './utils'; /** * Class for a date input field. */ export class FieldDate extends Blockly.FieldTextInput { /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. */ SERIALIZABLE = true; /** * Class for a date input field. Derived from the Closure library date * picker. * * @param value The initial value of the field. Should be in * 'YYYY-MM-DD' format. Defaults to the current date. * @param validator A function that is called to validate * changes to the field's value. Takes in a date string & returns a * validated date string ('YYYY-MM-DD' format), or null to abort the * change. * @param config A map of options used to configure the field. */ constructor( value?: string, validator?: FieldDateValidator, config?: FieldDateConfig, ) { super(value, validator, config); } /** * Constructs a FieldDate from a JSON arg object. * * @param options A JSON object with options (date). * @returns The new field instance. * @package * @nocollapse */ static fromJson(options: FieldDateFromJsonConfig): FieldDate { const {date, ...fieldDateConfig} = options; // `this` might be a subclass of FieldDate if that class doesn't // override the static fromJson method. return new this(date, undefined, fieldDateConfig); } /* eslint-disable @typescript-eslint/naming-convention */ /** * Ensures that the input value is a valid date. Additionally, if the date * string provided includes a time, the time will be removed and the date for * relative to the user's timezone will be used. * * @param newValue The input value. Ex: '2023-04-28' * @returns A valid date string, or null if invalid. * @override */ protected doClassValidation_(newValue?: string): string | null { if (!newValue) return null; const newDate = typeof newValue === 'string' ? new Date(newValue) : null; if (!newDate || isNaN(newDate.getTime())) return null; // NOTE: 'newValue' should be a valid date format here. if (isISOFormat(newValue)) return newValue; // Assume the time needs to be corrected. return toLocalISOString(newDate); } /** * Get the text to display on the block when the input hasn't spawned in. * * @returns The text to display on the block. * @override */ protected getText_(): string | null { const value = this.getValue(); if (!value) return null; // NOTE: There may be discrepancies between the text and the input based on // browser. For example, 'en-US' would display the text '2/14/2020', then // clicking in Safari on iOS would display 'Feb 14, 2020'. return getLocaleDateString(value); } /** * Renders the field. If the picker is shown make sure it has the current * date selected. */ protected render_() { super.render_(); } /** * Shows the inline free-text editor on top of the text along with the date * editor. * * @param e Optional mouse event that triggered the field to * open, or undefined if triggered programmatically. * @override */ protected showEditor_(e?: Event) { // Pass in `true` for `quietInput` to disable modal inputs for the date // block without setting `this.sourceBlock_.workspace.options.modalInputs`, // which would impact the entire workspace. super.showEditor_(e, true); // Even though `quietInput` was set true, focus on the element. this.htmlInput_?.focus({ preventScroll: true, }); this.htmlInput_?.select(); this.showDropdown(); } /** * Updates the size of the field based on the text. * * @param margin margin to use when positioning the text element. * @override */ protected updateSize_(margin?: number) { // Add margin so that the date input's datepicker icon doesn't clip with // the text when sized for the date. super.updateSize_((margin ?? 0) + 20); } /** * Shows the datepicker. */ private showDropdown(): void { if (!this.htmlInput_) return; Blockly.utils.dom.addClass(this.htmlInput_, 'blocklyDateInput'); // Delay showing the picker until the editor has a chance to position window.requestAnimationFrame(() => { // NOTE: HTMLInputElement.showPicker() is not available in earlier // TypeScript versions (like 4.7.4), so casting to `any` to be compatible // with dev scripts. Additionally, it's not available for date inputs for // Safari. For browser compatibility of showPicker, see: // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/showPicker /* eslint-disable @typescript-eslint/no-explicit-any */ (this.htmlInput_ as any).showPicker(); /* eslint-enable @typescript-eslint/no-explicit-any */ }); } /** * Create the html input and set it to type date. * * @returns The newly created date input editor. */ protected widgetCreate_(): HTMLInputElement { // NOTE: field_input should return HTMLInputElement for this. const htmlInput = super.widgetCreate_() as HTMLInputElement; htmlInput.type = 'date'; return htmlInput; } /* eslint-enable @typescript-eslint/naming-convention */ } /** * NOTE: There are a few minor ways to tweak the datepicker CSS, though they're * not consistent across browsers. * * @see{@link https://developer.mozilla.org/en-US/docs/Learn/Forms/Property_compatibility_table_for_form_controls#date_pickers} * * Below are a few ways this can be tweaked on *some* browsers: * Blockly.Css.register(` * ::-webkit-datetime-edit { } * ::-webkit-datetime-edit-fields-wrapper { } * ::-webkit-datetime-edit-text { } * ::-webkit-datetime-edit-month-field { } * ::-webkit-datetime-edit-day-field { } * ::-webkit-datetime-edit-year-field { } * ::-webkit-inner-spin-button { } * ::-webkit-calendar-picker-indicator { } * `); */ if (Blockly.utils.userAgent.MAC) { // NOTE: By default, 4 px padding total are added within the User Agent // Shadow Content on Safari on MAC. Remove the padding so the inner input // matches the outer input's height and, by extension, the height of the text // node. Blockly.Css.register(` input.blocklyDateInput::-webkit-datetime-edit, input.blocklyDateInput::-webkit-datetime-edit-month-field, input.blocklyDateInput::-webkit-datetime-edit-day-field, input.blocklyDateInput::-webkit-datetime-edit-year-field { padding: 0; } `); } Blockly.fieldRegistry.register('field_date', FieldDate); /** * A config object for defining a field date. */ export interface FieldDateConfig extends Blockly.FieldTextInputConfig { // NOTE: spellcheck is defined for FieldInput though irrelevant for FieldDate. spellcheck?: never; } /** * Options used to define a field date from JSON. */ export interface FieldDateFromJsonConfig extends FieldDateConfig { date?: string; } export type FieldDateValidator = Blockly.FieldTextInputValidator; /** * Validate a string value to see if it matches the format. * * @param value The value to validate the format for. * @returns true if the value is in 'yyyy-mm-dd' format. * @example * isISOFormat('2000-02-20T00:00:00Z') === false * isISOFormat('2000-02-20') === true */ export function isISOFormat(value: string): boolean { const valueMatch = value.match(/\d\d\d\d-\d\d-\d\d/); // If it matches ####-##-## and is the same as its input string, // then assume this is the right format return valueMatch !== null && valueMatch[0] === valueMatch.input; } /** * Convert the date to ISO format for the current timezone. * * @param date The date to convert to an ISO string. * @returns The string in 'yyyy-mm-dd' format, though for the current timezone. * Ex: new Date('2000-02-20') */ export function toLocalISOString(date: Date) { // NOTE: If the date is Feb 20, 2000 at 23:00 for GMT-6, it would be // '2000-02-21' at GMT+0, which is what `date.toISOString()` would return. // For a user whose timezone is GMT-6, this should return '2000-02-20'. // For a user whose timezone is GMT-5, that date should return '2000-02-21'. return date .toLocaleDateString('en-US') .replace(/(\d+)\/(\d+)\/(\d+)/, '$3-$1-$2') .replace(/-(\d)(?!\d)/g, '-0$1'); } // NOTE: Set default here instead of in class so it's available at Field. FieldDate.prototype.DEFAULT_VALUE = toLocalISOString(new Date());