"bootstrap-css-only": "^4.2.1",
"core-js": "^2.6.3",
"file-saver": "^2.0.0",
+ "moment": "2.24.0",
+ "moment-timezone": "0.5.23",
"ngx-cookie": "^4.1.2",
"rxjs": "^6.4.0",
"zone.js": "^0.8.29"
import {DatePipe, CurrencyPipe} from '@angular/common';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import * as Moment from 'moment-timezone';
/**
* Format IDL vield values for display.
datatype?: string;
orgField?: string; // 'shortname' || 'name'
datePlusTime?: boolean;
+ timezoneContextOrg?: number;
}
@Injectable({providedIn: 'root'})
return org ? org[orgField]() : '';
case 'timestamp':
- const date = new Date(value);
- let fmt = this.dateFormat || 'shortDate';
- if (params.datePlusTime) {
- fmt = this.dateTimeFormat || 'short';
+ const date = Moment(value).tz(this.wsOrgTimezone);
+ if (date) {
+ let fmt = this.dateFormat || 'shortDate';
+ if (params.datePlusTime) {
+ fmt = this.dateTimeFormat || 'short';
+ }
+ return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
}
- return this.datePipe.transform(date, fmt);
+ return value;
case 'money':
return this.currencyPipe.transform(value);
return value + '';
}
}
+ /**
+ * Create an IDL-friendly display version of a human-readable date
+ */
+ idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); }
+
+ /**
+ * Create an IDL-friendly display version of a human-readable datetime
+ */
+ idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
+
+ momentizeDateString(date: string, timezone: string): Moment {
+ const parseableFormat = this.makeFormatParseable(this.dateFormat);
+ if (parseableFormat.length) { return Moment.tz(date, parseableFormat, timezone); }
+ // TODO: The following fallback returns the date at midnight UTC,
+ // rather than midnight in the local TZ
+ return Moment.tz(date, timezone);
+ }
+
+ momentizeDateTimeString(datetime: string, timezone: string): Moment {
+ const parseableFormat = this.makeFormatParseable(this.dateTimeFormat);
+ if (parseableFormat.length) { return Moment(datetime, parseableFormat, timezone); }
+ return Moment(datetime, timezone);
+ }
+
+ /**
+ * Takes a dateFormate or dateTimeFormat string (which uses Angular syntax) and transforms
+ * it into a format string that MomentJs can use to parse input human-readable strings
+ * (https://momentjs.com/docs/#/parsing/string-format/)
+ *
+ * Returns a blank string if it can't do this transformation.
+ */
+ private makeFormatParseable(original: string): string {
+ const specialFormats = ['short', 'medium', 'long', 'full',
+ 'shortDate', 'mediumDate', 'longDate', 'fullDate',
+ 'shortTime', 'mediumTime', 'longTime', 'fullTime'];
+ if (!original || specialFormats.includes(original)) { return ''; };
+ return original
+ .replace(/a+/, 'a') // MomentJs can handle all sorts of meridian strings
+ .replace('d', 'D') // MomentJs capitalizes day of month
+ .replace('EEEEEE', '') // MomentJs does not handle short day of week
+ .replace('EEEEE', '') // MomentJs does not handle narrow day of week
+ .replace('EEEE', 'dddd') // MomentJs has different syntax for long day of week
+ .replace(/E{1,3}/, 'ddd') // MomentJs has different syntax for abbreviated day of week
+ .replace('L', 'M') // MomentJs does not differentiate between month and month standalone
+ .replace('W', '') // MomentJs uses W for something else
+ .replace('y', 'Y'); // MomentJs capitalizes year
+ }
}
--- /dev/null
+<ngb-datepicker #dp (select)="onDateSelection($event)" [displayMonths]="2" [dayTemplate]="t" outsideDays="hidden">
+</ngb-datepicker>
+
+<ng-template #t let-date let-focused="focused">
+ <span class="custom-day"
+ [class.focused]="focused"
+ [class.range]="isRange(date)"
+ [class.faded]="isHovered(date) || isInside(date)"
+ (mouseenter)="hoveredDate = date"
+ (mouseleave)="hoveredDate = null">
+ {{ date.day }}
+ </span>
+</ng-template>
+
--- /dev/null
+import {Component, Output, EventEmitter} from '@angular/core';
+import {NgbDate, NgbCalendar} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'eg-daterange-select',
+ templateUrl: './daterange-select.component.html',
+ styles: [`
+ .custom-day {
+ text-align: center;
+ padding: 0.185rem 0.25rem;
+ display: inline-block;
+ height: 2rem;
+ width: 2rem;
+ }
+ .custom-day.focused {
+ background-color: #e6e6e6;
+ }
+ .custom-day.range, .custom-day:hover {
+ background-color: rgb(2, 117, 216);
+ color: white;
+ }
+ .custom-day.faded {
+ background-color: rgba(2, 117, 216, 0.5);
+ }
+ `]
+})
+export class DateRangeSelectComponent {
+
+ hoveredDate: NgbDate;
+
+ fromDate: NgbDate;
+ toDate: NgbDate;
+
+ @Output() onChange = new EventEmitter<{start: NgbDate, end: NgbDate}>();
+
+ constructor(calendar: NgbCalendar) {
+ this.fromDate = calendar.getToday();
+ this.toDate = calendar.getNext(calendar.getToday(), 'd', 10);
+ }
+
+ onDateSelection(date: NgbDate) {
+ if (!this.fromDate && !this.toDate) {
+ this.fromDate = date;
+ } else if (this.fromDate && !this.toDate && date.after(this.fromDate)) {
+ this.toDate = date;
+ } else {
+ this.toDate = null;
+ this.fromDate = date;
+ }
+ this.onChange.emit({start: this.fromDate, end: this.toDate});
+ }
+
+ isHovered(date: NgbDate) {
+ return this.fromDate && !this.toDate && this.hoveredDate && date.after(this.fromDate) && date.before(this.hoveredDate);
+ }
+
+ isInside(date: NgbDate) {
+ return date.after(this.fromDate) && date.before(this.toDate);
+ }
+
+ isRange(date: NgbDate) {
+ return date.equals(this.fromDate) || date.equals(this.toDate) || this.isInside(date) || this.isHovered(date);
+ }
+}
--- /dev/null
+<span class="material-icons" *ngIf="validatorError">error</span>
+<div class="input-group">
+ <input type="datetime"
+ [attr.id]="domId.length ? domId : null"
+ name="{{fieldName}}"
+ class="form-control"
+ [placeholder]="initialIso"
+ [(ngModel)]="stringVersion"
+ (blur)="handleInputChanged($event)"
+ (change)="handleInputChanged($event)"
+ #dtPicker="ngbPopover"
+ [ngbPopover]="dt"
+ placement="bottom"
+ [attr.disabled]="readOnly ? true : null"
+ [autoClose]="'outside'"
+ popoverTitle="Select date and time"
+ i18n-popoverTitle
+ [required]="required">
+ <div class="input-group-btn">
+ <button class="btn btn-primary" (click)="dtPicker.toggle()">
+ <span title="Select Date" i18n-title
+ class="material-icons mat-icon-in-button">calendar_today</span>
+ </button>
+ </div>
+</div>
+
+<ng-template #dt>
+ <div i18n *ngIf="readOnly">
+ Cannot edit this date or time.
+ </div>
+ <div *ngIf="!readOnly">
+ <div *ngIf="validatorError" class="alert alert-danger">
+ <span class="material-icons">error</span>
+ {{validatorError}}
+ </div>
+ <ngb-datepicker
+ [(ngModel)]="dateModel"
+ (select)="handleDateChanged($event)"
+ [footerTemplate]="time">
+ </ngb-datepicker>
+ </div>
+</ng-template>
+<ng-template #time>
+ <ngb-timepicker name="time"
+ [(ngModel)]="timeModel" [meridian]="true"
+ (ngModelChange)="handleTimeChanged()"
+ [spinners]="true"
+ [hourStep]="1"
+ [minuteStep]="minuteStep || 30" >
+ </ngb-timepicker>
+ <span *ngIf="showTZ" class="badge badge-info">{{ timezone || 'America/Los_Angeles'}}</span>
+ <button i18n class="btn btn-success" (click)="dtPicker.close()">Choose time</button>
+</ng-template>
+
--- /dev/null
+import { Component, Input, Output, EventEmitter, ViewChild, OnInit } from '@angular/core';
+import { ControlValueAccessor } from '@angular/forms';
+import {FormatService} from '@eg/core/format.service';
+
+import * as Moment from 'moment-timezone';
+
+import { NgbTimeStruct, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'eg-datetime-select',
+ templateUrl: './datetime-select.component.html'
+})
+export class DateTimeSelectComponent implements OnInit {
+ @Input() domId = '';
+ @Input() fieldName: string;
+ @Input() required: boolean;
+ @Input() minuteStep = 15;
+ @Input() showTZ = true;
+ @Input() timezone: string = this.format.wsOrgTimezone;
+ @Input() readOnly = false;
+ @Input() validatorError = '';
+
+ @Input() initialIso: string;
+ @Input() initialMoment: Moment;
+
+ @Output() onChangeAsIso = new EventEmitter<string>();
+ @Output() onChangeAsMoment = new EventEmitter<Moment>();
+
+ stringVersion: any; // Used internally on internal input
+ timeModel: NgbTimeStruct;
+ dateModel: NgbDateStruct;
+
+ constructor(
+ private format: FormatService
+ ) {
+ }
+
+ ngOnInit() {
+ let start = this.initialIso ? Moment.tz(this.initialIso, Moment.ISO_8601, this.timezone) : Moment.tz([], this.timezone);
+ if (this.initialMoment) { start = this.initialMoment; }
+ this.stringVersion = this.format.transform({value: start, datatype: 'timestamp', datePlusTime: true});
+ this.setDatePicker(start);
+ this.setTimePicker(start);
+
+ }
+
+ setDatePicker(current: Moment) {
+ this.dateModel = {
+ year: current.tz(this.timezone).year(),
+ month: current.tz(this.timezone).month() + 1,
+ day: current.tz(this.timezone).date() };
+ }
+
+ setTimePicker(current: Moment) {
+ this.timeModel = {
+ hour: current.tz(this.timezone).hour(),
+ minute: current.tz(this.timezone).minute(),
+ second: 0 };
+ }
+
+ handleDateChanged($event) {
+ const newDate = Moment.tz([$event.year, ($event.month - 1), $event.day,
+ this.timeModel.hour, this.timeModel.minute, 0], this.timezone);
+ this.stringVersion = this.format.transform({value: newDate, datatype: 'timestamp', datePlusTime: true});
+ this.onChangeAsMoment.emit(newDate);
+ this.onChangeAsIso.emit(newDate.toISOString());
+ }
+
+ handleTimeChanged(event) {
+ const newDate = Moment.tz([this.dateModel.year, (this.dateModel.month - 1), this.dateModel.day,
+ this.timeModel.hour, this.timeModel.minute, 0], this.timezone);
+ this.stringVersion = this.format.transform({value: newDate, datatype: 'timestamp', datePlusTime: true});
+ this.onChangeAsMoment.emit(newDate);
+ this.onChangeAsIso.emit(newDate.toISOString());
+ }
+
+ handleInputChanged(event) {
+ let newDate: Moment;
+
+ if (event) {
+ newDate = this.format.momentizeDateTimeString(this.stringVersion, this.timezone);
+ this.setDatePicker(newDate);
+ this.setTimePicker(newDate);
+ this.onChangeAsMoment.emit(newDate);
+ this.onChangeAsIso.emit(newDate.toISOString());
+ }
+ }
+
+}
+
import {StringService} from '@eg/share/string/string.service';
import {TitleComponent} from '@eg/share/title/title.component';
import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {DateRangeSelectComponent} from '@eg/share/daterange-select/daterange-select.component';
import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
import {BucketDialogComponent} from '@eg/staff/share/buckets/bucket-dialog.component';
+import {DateTimeSelectComponent} from '@eg/share/datetime-select/datetime-select.component';
import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
TitleComponent,
OpChangeComponent,
FmRecordEditorComponent,
+ DateRangeSelectComponent,
DateSelectComponent,
BucketDialogComponent,
+ DateTimeSelectComponent,
BibSummaryComponent,
TranslateComponent,
AdminPageComponent
TitleComponent,
OpChangeComponent,
FmRecordEditorComponent,
+ DateRangeSelectComponent,
DateSelectComponent,
BucketDialogComponent,
+ DateTimeSelectComponent,
BibSummaryComponent,
TranslateComponent,
AdminPageComponent