// Copyright (c) Vidar Tonaas Fauske // Distributed under the terms of the Modified BSD License. import { ISerializers } from '@jupyter-widgets/base'; import { uuid } from './utils'; import { DescriptionView } from './widget_description'; import { CoreDescriptionModel } from './widget_core'; import { serialize_time } from './widget_time'; export interface ISerializedDatetime { /** * UTC full year */ year: number; /** * UTC zero-based month (0 means January, 11 means December) */ month: number; /** * UTC day of month */ date: number; /** * UTC hour (24H format) */ hours: number; /** * UTC minutes */ minutes: number; /** * UTC seconds */ seconds: number; /** * UTC millisconds */ milliseconds: number; } export function serialize_datetime(value: Date): ISerializedDatetime | null { if (value === null) { return null; } else { return { year: value.getUTCFullYear(), month: value.getUTCMonth(), date: value.getUTCDate(), hours: value.getUTCHours(), minutes: value.getUTCMinutes(), seconds: value.getUTCSeconds(), milliseconds: value.getUTCMilliseconds(), }; } } export function deserialize_datetime(value: ISerializedDatetime): Date | null { if (value === null) { return null; } else { const date = new Date(); date.setUTCFullYear(value.year, value.month, value.date); date.setUTCHours( value.hours, value.minutes, value.seconds, value.milliseconds ); return date; } } export const datetime_serializers = { serialize: serialize_datetime, deserialize: deserialize_datetime, }; export class DatetimeModel extends CoreDescriptionModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'DatetimeModel', _view_name: 'DatetimeView', value: null, disabled: false, min: null, max: null, }; } static serializers: ISerializers = { ...CoreDescriptionModel.serializers, value: datetime_serializers, min: datetime_serializers, max: datetime_serializers, }; } export class DatetimeView extends DescriptionView { render(): void { super.render(); this.el.classList.add('jupyter-widgets'); this.el.classList.add('widget-inline-hbox'); this.el.classList.add('widget-datetimepicker'); const test = document.createElement('input'); test.type = 'datetime-local'; if (test.type === 'text') { // No native support, split into date and time input: this._datepicker = document.createElement('input'); this._datepicker.setAttribute('type', 'date'); this._datepicker.id = this.label.htmlFor = uuid(); this._timepicker = document.createElement('input'); this._timepicker.setAttribute('type', 'time'); this._timepicker.id = uuid(); this.el.appendChild(this._datepicker); this.el.appendChild(this._timepicker); } else { this._datetimepicker = test; this._datetimepicker.id = this.label.htmlFor = uuid(); this.el.appendChild(this._datetimepicker); } this.listenTo(this.model, 'change:value', this._update_value); this.listenTo(this.model, 'change', this.update2); this._update_value(); this.update2(); } /** * Update the contents of this view * * Called when the model is changed. The model may have been * changed by another view or by a state update from the back-end. */ update2(model?: Backbone.Model, options?: any): void { if (options === undefined || options.updated_view !== this) { const min = this.model.get('min') as Date | null; const max = this.model.get('max') as Date | null; if (this._datetimepicker) { this._datetimepicker.disabled = this.model.get('disabled'); this._datetimepicker!.min = Private.dt_as_dt_string(min); this._datetimepicker!.max = Private.dt_as_dt_string(max); } else { this._datepicker!.disabled = this.model.get('disabled'); this._datepicker!.min = Private.dt_as_date_string(min); this._datepicker!.max = Private.dt_as_date_string(max); this._timepicker!.disabled = this.model.get('disabled'); // Don't support min/max time here. // It could be added by enabling min time if value is min date, // and enabling max time if value is max date, but leave as // TODO for now. } } } events(): { [e: string]: string } { // Typescript doesn't understand that these functions are called, so we // specifically use them here so it knows they are being used. void this._picker_change; void this._picker_focusout; return { 'change [type="date"]': '_picker_change', 'change [type="time"]': '_picker_change', 'change [type="datetime-local"]': '_picker_change', 'focusout [type="date"]': '_picker_focusout', 'focusout [type="datetime-local"]': '_picker_focusout', 'focusout [type="time"]': '_picker_focusout', }; } private _update_value( model?: Backbone.Model, newValue?: any, options?: any ): void { if (options === undefined || options.updated_view !== this) { const value = this.model.get('value') as Date | null; if (this._datetimepicker) { this._datetimepicker.value = Private.dt_as_dt_string(value); } else { this._datepicker!.valueAsDate = value; this._timepicker!.value = Private.dt_as_time_string(value); } } } private _picker_change(): void { if (this._datetimepicker) { if (!this._datetimepicker.validity.badInput) { const v = this._datetimepicker.value; let date = v ? new Date(v) : null; if (date && isNaN(date.valueOf())) { date = null; } this.model.set('value', date, { updated_view: this }); this.touch(); } } else { if ( !this._datepicker!.validity.badInput && !this._timepicker!.validity.badInput ) { const date = this._datepicker!.valueAsDate; const time = serialize_time(this._timepicker!.value); if (date !== null && time !== null) { // * Use local time * date.setHours( time.hours, time.minutes, time.seconds, time.milliseconds ); } this.model.set('value', time !== null && date, { updated_view: this }); this.touch(); } } } private _picker_focusout(): void { const pickers = [this._datetimepicker, this._datepicker, this._timepicker]; if (pickers.some((p) => p && p.validity.badInput)) { this.model.set('value', null); this.touch(); } } private _datetimepicker: HTMLInputElement | undefined; private _timepicker: HTMLInputElement | undefined; private _datepicker: HTMLInputElement | undefined; } namespace Private { // eslint-disable-next-line no-inner-declarations export function dt_as_dt_string(value: Date | null): string { if (value === null) { return ''; } // Replicate `toISOString()` but in local time zone: const parts = []; parts.push(`${value.getFullYear().toString().padStart(4, '0')}`); parts.push(`-${(value.getMonth() + 1).toString().padStart(2, '0')}`); parts.push(`-${value.getDate().toString().padStart(2, '0')}`); parts.push(`T${value.getHours().toString().padStart(2, '0')}`); parts.push(`:${value.getMinutes().toString().padStart(2, '0')}`); if (value.getSeconds() > 0 || value.getMilliseconds() > 0) { parts.push(`:${value.getSeconds().toString().padStart(2, '0')}`); if (value.getMilliseconds() > 0) { parts.push(`.${value.getMilliseconds().toString().padStart(3, '0')}`); } } return parts.join(''); } // eslint-disable-next-line no-inner-declarations export function dt_as_date_string(value: Date | null): string { return value ? dt_as_dt_string(value).split('T', 2)[0] : ''; } // eslint-disable-next-line no-inner-declarations export function dt_as_time_string(value: Date | null): string { return value ? dt_as_dt_string(value).split('T', 2)[1] : ''; } } export interface ISerializedNaiveDatetime { /** * full year */ year: number; /** * zero-based month (0 means January, 11 means December) */ month: number; /** * day of month */ date: number; /** * hour (24H format) */ hours: number; /** * minutes */ minutes: number; /** * seconds */ seconds: number; /** * millisconds */ milliseconds: number; } export function serialize_naive( value: Date | null ): ISerializedNaiveDatetime | null { if (value === null) { return null; } else { return { year: value.getFullYear(), month: value.getMonth(), date: value.getDate(), hours: value.getHours(), minutes: value.getMinutes(), seconds: value.getSeconds(), milliseconds: value.getMilliseconds(), }; } } export function deserialize_naive( value: ISerializedNaiveDatetime ): Date | null { if (value === null) { return null; } else { const date = new Date(); date.setFullYear(value.year, value.month, value.date); date.setHours( value.hours, value.minutes, value.seconds, value.milliseconds ); return date; } } export const naive_serializers = { serialize: serialize_naive, deserialize: deserialize_naive, }; export class NaiveDatetimeModel extends DatetimeModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'NaiveDatetimeModel' }; } static serializers: ISerializers = { ...CoreDescriptionModel.serializers, value: naive_serializers, min: naive_serializers, max: naive_serializers, }; }