import { Disposable, dom, Observable, styled } from "grainjs"; import { ColumnFilter } from "app/client/models/ColumnFilter"; import { testId } from "app/client/ui2018/cssVars"; import { textButton } from "app/client/ui2018/buttons"; import { IColumnFilterViewType } from "app/client/ui/ColumnFilterMenu"; import getCurrentTime from "app/common/getCurrentTime"; import { IRelativeDateSpec, isRelativeBound } from "app/common/FilterState"; import { toUnixTimestamp } from "app/common/RelativeDates"; import { updateRelativeDate } from "app/client/ui/RelativeDatesOptions"; import moment from "moment-timezone"; export class ColumnFilterCalendarView extends Disposable { private _$el: any; constructor(private _opts: { viewTypeObs: Observable, // Note the invariant: `selectedBoundObs.get() !== null` until this gets disposed. selectedBoundObs: Observable<'min' | 'max' | null>, columnFilter: ColumnFilter, }) { super(); this._moveToSelected = this._moveToSelected.bind(this); this.autoDispose(this.columnFilter.min.addListener(() => this._setRange())); this.autoDispose(this.columnFilter.max.addListener(() => this._setRange())); this.autoDispose(this._opts.selectedBoundObs.addListener(this._moveToSelected)); } public get columnFilter() { return this._opts.columnFilter; } public get selectedBoundObs() { return this._opts.selectedBoundObs; } public buildDom() { setTimeout(() => this._moveToSelected(), 0); return cssContainer( cssLinkRow( cssLink( '← List view', dom.on('click', () => this._opts.selectedBoundObs.set(null)), ), cssLink( 'Today', dom.on('click', () => { this._$el.datepicker('update', this._getCurrentTime()); this._cleanup(); }), ), testId('calendar-links'), ), cssDatepickerContainer( (el) => { const $el = this._$el = $(el) as any; $el.datepicker({ defaultViewDate: this._getCurrentTime(), todayHighlight: true, }); $el[0].querySelector('.datepicker'); this._setRange(); $el.on('changeDate', () => this._onChangeDate()); // Schedules cleanups after users navigations (ie: navigating to next/prev month). $el.on('changeMonth', () => setTimeout(() => this._cleanup(), 0)); $el.on('changeYear', () => setTimeout(() => this._cleanup(), 0)); $el.on('changeDecade', () => setTimeout(() => this._cleanup(), 0)); $el.on('changeCentury', () => setTimeout(() => this._cleanup(), 0)); }, ) ); } private _setRange() { this._$el.datepicker('setRange', this._getRange()); this._moveToSelected(); } // Move calendar to the selected bound's current date. private _moveToSelected() { const minMax = this._opts.selectedBoundObs.get(); let dateValue = this._getCurrentTime(); if (minMax !== null) { const value = this.columnFilter.getBoundsValue(minMax); if (value !== undefined) { dateValue = new Date(value * 1000); } } this._$el.datepicker('update', dateValue); this._cleanup(); } private _getCurrentTime(): Date { return getCurrentTime().toDate(); } private _onChangeDate() { const d = this._$el.datepicker('getUTCDate').valueOf() / 1000; const {min, max} = this.columnFilter; // Check the the min bounds is before max bounds. If not update the other bounds to the same // value. // TODO: also perform this check when users pick relative dates from popup if (this.selectedBoundObs.get() === 'min') { min.set(this._updateBoundValue(min.get(), d)); if (max.get() !== undefined && toUnixTimestamp(max.get()!) < d) { max.set(this._updateBoundValue(max.get(), d)); } } else { max.set(this._updateBoundValue(max.get(), d)); if (min.get() !== undefined && d < toUnixTimestamp(min.get()!)) { min.set(this._updateBoundValue(min.get(), d)); } } this._cleanup(); } private _getRange() { const min = this.columnFilter.getBoundsValue('min'); const max = this.columnFilter.getBoundsValue('max'); const toDate = (val: number) => { const m = moment.utc(val * 1000); return new Date(Date.UTC(m.year(), m.month(), m.date())); }; if (min === undefined && max === undefined) { return []; } if (min === undefined) { return [{valueOf: () => -Infinity}, toDate(max!)]; } if (max === undefined) { return [toDate(min), {valueOf: () => +Infinity}]; } return [toDate(min), toDate(max)]; } // Update val with date. Returns the new updated value. Useful to update bounds' value after users // have picked new value from calendar. private _updateBoundValue(val: IRelativeDateSpec|number|undefined, date: number) { return isRelativeBound(val) ? updateRelativeDate(val, date) : date; } // Removes the `.active` class from date elements in the datepicker. The active dates background // takes precedence over other backgrounds which are more important to us, such as range's bounds // and current day. private _cleanup() { const elements = this._$el.get()[0].querySelectorAll('.active'); for (const el of elements) { el.classList.remove('active'); } } } const cssContainer = styled('div', ` padding: 16px 16px; `); const cssLink = textButton; const cssLinkRow = styled('div', ` display: flex; justify-content: space-between; `); const cssDatepickerContainer = styled('div', ` padding-top: 16px; `);