diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2c78e1c777..f1edaa4de6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -138,6 +138,9 @@ import { TextValueAsXMLComponent } from './workspace/resource/values/text-value/ import { TimeInputComponent } from './workspace/resource/values/time-value/time-input/time-input.component'; import { TimeValueComponent } from './workspace/resource/values/time-value/time-value.component'; import { UriValueComponent } from './workspace/resource/values/uri-value/uri-value.component'; +import { DatePickerComponent } from './workspace/resource/values/yet-another-date-value/date-picker/date-picker.component'; +import { DateValueHandlerComponent } from './workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component'; +import { YetAnotherDateValueComponent } from './workspace/resource/values/yet-another-date-value/yet-another-date-value.component'; import { ListViewComponent } from './workspace/results/list-view/list-view.component'; import { ResourceGridComponent } from './workspace/results/list-view/resource-grid/resource-grid.component'; import { ResourceListComponent } from './workspace/results/list-view/resource-list/resource-list.component'; @@ -152,6 +155,7 @@ import { SearchIntValueComponent } from './workspace/search/advanced-search/reso import { SearchLinkValueComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-link-value/search-link-value.component'; import { SearchDisplayListComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-list-value/search-display-list/search-display-list.component'; import { SearchListValueComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-list-value/search-list-value.component'; +import { SearchResourceComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-resource/search-resource.component'; import { SearchTextValueComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-text-value/search-text-value.component'; import { SearchUriValueComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-uri-value/search-uri-value.component'; import { SpecifyPropertyValueComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/specify-property-value.component'; @@ -160,7 +164,6 @@ import { SearchSelectOntologyComponent } from './workspace/search/advanced-searc import { ExpertSearchComponent } from './workspace/search/expert-search/expert-search.component'; import { FulltextSearchComponent } from './workspace/search/fulltext-search/fulltext-search.component'; import { SearchPanelComponent } from './workspace/search/search-panel/search-panel.component'; -import { SearchResourceComponent } from './workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-resource/search-resource.component'; // translate: AoT requires an exported function for factories export function httpLoaderFactory(httpClient: HttpClient) { @@ -197,7 +200,9 @@ export function httpLoaderFactory(httpClient: HttpClient) { DateEditComponent, DateInputComponent, DateInputTextComponent, + DatePickerComponent, DateValueComponent, + DateValueHandlerComponent, DecimalValueComponent, DialogComponent, DialogHeaderComponent, @@ -273,6 +278,7 @@ export function httpLoaderFactory(httpClient: HttpClient) { SearchLinkValueComponent, SearchListValueComponent, SearchPanelComponent, + SearchResourceComponent, SearchSelectOntologyComponent, SearchSelectPropertyComponent, SearchSelectResourceClassComponent, @@ -311,7 +317,7 @@ export function httpLoaderFactory(httpClient: HttpClient) { UsersComponent, UsersListComponent, VisualizerComponent, - SearchResourceComponent, + YetAnotherDateValueComponent, ], imports: [ AngularSplitModule.forRoot(), diff --git a/src/app/main/pipes/formatting/knoradate.pipe.spec.ts b/src/app/main/pipes/formatting/knoradate.pipe.spec.ts index 4fafa0eb7e..a3d47af20c 100644 --- a/src/app/main/pipes/formatting/knoradate.pipe.spec.ts +++ b/src/app/main/pipes/formatting/knoradate.pipe.spec.ts @@ -47,11 +47,11 @@ describe('KnoradatePipe', () => { dateWithDisplayOptions = pipe.transform(date, 'dd.MM.YYYY', 'calendar'); - expect(dateWithDisplayOptions).toEqual('04.07.1776 GREGORIAN'); + expect(dateWithDisplayOptions).toEqual('04.07.1776 Gregorian'); dateWithDisplayOptions = pipe.transform(date, 'dd.MM.YYYY', 'all'); - expect(dateWithDisplayOptions).toEqual('04.07.1776 AD GREGORIAN'); + expect(dateWithDisplayOptions).toEqual('04.07.1776 Gregorian'); }); it ('should return a string with the desired display options for a date without era', () => { @@ -63,11 +63,11 @@ describe('KnoradatePipe', () => { dateWithDisplayOptions = pipe.transform(date, 'dd.MM.YYYY', 'calendar'); - expect(dateWithDisplayOptions).toEqual('04.07.1441 ISLAMIC'); + expect(dateWithDisplayOptions).toEqual('04.07.1441 Islamic'); dateWithDisplayOptions = pipe.transform(date, 'dd.MM.YYYY', 'all'); - expect(dateWithDisplayOptions).toEqual('04.07.1441 ISLAMIC'); + expect(dateWithDisplayOptions).toEqual('04.07.1441 Islamic'); }); it ('should return a string with only the month and the year', () => { diff --git a/src/app/main/pipes/formatting/knoradate.pipe.ts b/src/app/main/pipes/formatting/knoradate.pipe.ts index ac751d81f8..435056f715 100644 --- a/src/app/main/pipes/formatting/knoradate.pipe.ts +++ b/src/app/main/pipes/formatting/knoradate.pipe.ts @@ -6,9 +6,9 @@ import { KnoraDate } from '@dasch-swiss/dsp-js'; }) export class KnoraDatePipe implements PipeTransform { - transform(date: KnoraDate, format?: string, displayOptions?: 'era' | 'calendar' | 'all'): string { + transform(date: KnoraDate, format?: string, displayOptions?: 'era' | 'calendar' | 'calendarOnly' | 'all'): string { if (!(date instanceof KnoraDate)) { - console.error('Non-KnoraDate provided. Expected a valid KnoraDate'); + // console.error('Non-KnoraDate provided. Expected a valid KnoraDate'); return ''; } @@ -34,11 +34,17 @@ export class KnoraDatePipe implements PipeTransform { addDisplayOptions(date: KnoraDate, value: string, options: string): string { switch (options) { case 'era': - return value + (date.era !== 'noEra' ? ' ' + date.era : ''); + // displays date with era; era only in case of BCE + return value + (date.era === 'noEra' ? '' : ((date.era === 'BCE' || date.era === 'AD') ? ' ' + date.era : '')); case 'calendar': - return value + ' ' + date.calendar; + // displays date without era but with calendar type + return value + ' ' + this._titleCase(date.calendar); + case 'calendarOnly': + // displays only the selected calendar type without any data + return this._titleCase(date.calendar); case 'all': - return value + (date.era !== 'noEra' ? ' ' + date.era : '') + ' ' + date.calendar; + // displays date with era (only as BCE) and selected calendar type + return value + (date.era === 'noEra' ? '' : (date.era === 'BCE' ? ' ' + date.era : '')) + ' ' + this._titleCase(date.calendar); } } @@ -79,4 +85,15 @@ export class KnoraDatePipe implements PipeTransform { } } + /** + * returns a string in Title Case format + * It's needed to transform a calendar name e.g. 'GREGORIAN' into 'Gregorian' + * + * @param str + * @returns string + */ + private _titleCase(str: string): string { + return str.split(' ').map(w => w[0].toUpperCase() + w.substr(1).toLowerCase()).join(' '); + } + } diff --git a/src/app/workspace/resource/operations/add-value/add-value.component.html b/src/app/workspace/resource/operations/add-value/add-value.component.html index c2977a37b4..a37472a645 100644 --- a/src/app/workspace/resource/operations/add-value/add-value.component.html +++ b/src/app/workspace/resource/operations/add-value/add-value.component.html @@ -14,7 +14,7 @@ - + diff --git a/src/app/workspace/resource/operations/display-edit/display-edit.component.html b/src/app/workspace/resource/operations/display-edit/display-edit.component.html index df453a66a8..896a6d6725 100644 --- a/src/app/workspace/resource/operations/display-edit/display-edit.component.html +++ b/src/app/workspace/resource/operations/display-edit/display-edit.component.html @@ -19,7 +19,8 @@ - + + {{displayValue.strval}} diff --git a/src/app/workspace/resource/operations/display-edit/display-edit.component.ts b/src/app/workspace/resource/operations/display-edit/display-edit.component.ts index 0ed7c768ed..b69f18fe32 100644 --- a/src/app/workspace/resource/operations/display-edit/display-edit.component.ts +++ b/src/app/workspace/resource/operations/display-edit/display-edit.component.ts @@ -239,6 +239,7 @@ export class DisplayEditComponent implements OnInit { mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid)) ).subscribe( (res2: ReadResource) => { + this._valueOperationEventService.emit( new EmitEvent(Events.ValueUpdated, new UpdatedEventValues( this.displayValue, res2.getValues(this.displayValue.property)[0]))); diff --git a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts index 72b6bee154..7d2ac31609 100644 --- a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts +++ b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.spec.ts @@ -444,8 +444,6 @@ describe('ResourceInstanceFormComponent', () => { const selectOntoComp = resourceInstanceFormComponentDe.query(By.directive(MockSelectOntologyComponent)); - console.log('ontos: ', (selectOntoComp.componentInstance as MockSelectOntologyComponent).ontologiesMetadata.ontologies); - expect((selectOntoComp.componentInstance as MockSelectOntologyComponent).ontologiesMetadata.ontologies.length).toEqual(11); expect(dspConnSpy.v2.onto.getOntologiesByProjectIri).toHaveBeenCalledTimes(1); diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html b/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html index 2ca0203ba6..528bc074ae 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html +++ b/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html @@ -11,7 +11,7 @@ - + diff --git a/src/app/workspace/resource/values/date-value/date-value.component.html b/src/app/workspace/resource/values/date-value/date-value.component.html index 3e04b5ccad..e848b148b6 100644 --- a/src/app/workspace/resource/values/date-value/date-value.component.html +++ b/src/app/workspace/resource/values/date-value/date-value.component.html @@ -3,11 +3,11 @@ - Period Start: + {{valueFormControl.value?.start | knoraDate:ontologyDateFormat:displayOptions}} - Period End: + {{valueFormControl.value?.end | knoraDate:ontologyDateFormat:displayOptions}} @@ -15,7 +15,7 @@ - Date: + {{valueFormControl.value | knoraDate:ontologyDateFormat:displayOptions}} diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.html b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.html new file mode 100644 index 0000000000..02f1474ce0 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.html @@ -0,0 +1,126 @@ +
+ + + {{ calendar | titlecase }} + + event + + +
+ + + + + + +
+ + + +
+ + Calendar + + + {{cal | titlecase}} + + + + +
+ + + + +
+
+ + +
+ + + Month + + -- None -- + + {{month[0]}} + {{month[1]}} + + + + + + Year * + + + {{ formErrors.year }} + + + + CE + BCE + + + + + +
+ + +
+ +
+

+ {{d}} +

+

+ + Attention: Date before common era. +

+
+
+ + + + {{d}} + + + + + not_interested + + +
+ +
+ + + not_interested + + +
+
+
+ +
diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.scss b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.scss new file mode 100644 index 0000000000..c6edaec339 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.scss @@ -0,0 +1,109 @@ +@import "../../../../../../assets/style/config"; + +.child-input-component { + width: 96%; +} + +.date-picker-content { + width: 296px; + padding: 8px 8px 16px 8px; +} + +.hidden { + display: none; +} + +.panel { + width: 280px; + margin: 0 8px; + + &.calendar-selector, + &.mont-year-selector { + display: inline-flex; + height: 56px; + } + + &.calendar-selector { + .calendar { + width: 100%; + } + + .action { + display: flex; + + } + } + &.month-year-selector { + .month { + width: 96px; + &.larger { + width: 136px; + } + } + .year { + width: 136px; + + &.withButtonToggle { + width: 180px; + } + + .era { + mat-button-toggle { + width: 48px; + } + } + } + } + + + &.day-selector { + display: grid; + margin-top: -8px; + + &.disabled { + opacity: 25%; + } + + .week-days { + margin-bottom: 0; + font-weight: lighter; + color: $dark; + } + + .week { + display: inline-flex; + } + } + +} + +.day { + width: 36px; + height: 36px; + line-height: 36px; + display: inline-table; + text-align: center; + padding: 2px; + + &.selected { + background-color: $primary_700; + color: $bright; + border-radius: 50%; + } + + > span { + display: block; + } + + .selectable { + max-height: 36px; + max-width: 36px; + &.disabled { + cursor: not-allowed; + } + + mat-icon { + margin: 6px; + } + } +} diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.spec.ts b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.spec.ts new file mode 100644 index 0000000000..6c117c66aa --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatSelectModule } from '@angular/material/select'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DatePickerComponent } from './date-picker.component'; + +describe('DatePickerComponent', () => { + let component: DatePickerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + DatePickerComponent + ], + imports: [ + BrowserAnimationsModule, + MatButtonModule, + MatButtonToggleModule, + MatFormFieldModule, + MatInputModule, + MatMenuModule, + MatSelectModule, + ReactiveFormsModule, + ], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DatePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.ts b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.ts new file mode 100644 index 0000000000..ee5b505033 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-picker/date-picker.component.ts @@ -0,0 +1,531 @@ +import { FocusMonitor } from '@angular/cdk/a11y'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Component, DoCheck, ElementRef, HostBinding, Input, OnChanges, OnDestroy, Optional, Self, SimpleChanges, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgControl, NgForm, Validators } from '@angular/forms'; +import { CanUpdateErrorState, CanUpdateErrorStateCtor, ErrorStateMatcher, mixinErrorState } from '@angular/material/core'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { KnoraDate } from '@dasch-swiss/dsp-js'; +import { Subject } from 'rxjs'; +import { KnoraDatePipe } from 'src/app/main/pipes/formatting/knoradate.pipe'; +import { ValueService } from '../../../services/value.service'; + +/** error when invalid control is dirty, touched, or submitted. */ +export class DatePickerErrorStateMatcher implements ErrorStateMatcher { + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const isSubmitted = form && form.submitted; + return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted)); + } +} + +class MatInputBase { + constructor( + public _defaultErrorStateMatcher: ErrorStateMatcher, + public _parentForm: NgForm, + public _parentFormGroup: FormGroupDirective, + public ngControl: NgControl) { } +} +const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase = + mixinErrorState(MatInputBase); + + +@Component({ + selector: 'app-date-picker', + templateUrl: './date-picker.component.html', + styleUrls: ['./date-picker.component.scss'], + providers: [ + { provide: MatFormFieldControl, useExisting: DatePickerComponent }, + { provide: KnoraDatePipe } + ] +}) +export class DatePickerComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl, OnChanges, DoCheck, CanUpdateErrorState, OnDestroy { + + static nextId = 0; + + @ViewChild(MatMenuTrigger) popover: MatMenuTrigger; + + @Input() errorStateMatcher: ErrorStateMatcher; + + // disable calendar selector in case of end date in a period date value + @Input() disableCalendarSelector: boolean; + + // set predefinde calendar + @Input() calendar = 'GREGORIAN'; + + @HostBinding() id = `app-date-picker-${DatePickerComponent.nextId++}`; + + @HostBinding('attr.aria-describedby') describedBy = ''; + dateForm: FormGroup; + stateChanges = new Subject(); + focused = false; + errorState = false; + controlType = 'app-date-picker'; + matcher = new DatePickerErrorStateMatcher(); + + // own date picker variables + date: KnoraDate; + form: FormGroup; + formErrors = { + 'year': '' + }; + + validationMessages = { + 'year': { + 'required': 'At least the year has to be set.', + 'min': 'A valid year is greater than 0.', + } + }; + + // list of months + months = [ + ['Jan', 'Muḥarram'], + ['Feb', 'Safar'], + ['Mar', 'Rabīʿ al-ʾAwwal'], + ['Apr', 'Rabīʿ ath-Thānī'], + ['May', 'Jumadā al-ʾŪlā'], + ['Jun', 'Jumādā ath-Thāniyah'], + ['Jul', 'Rajab'], + ['Aug', 'Shaʿbān'], + ['Sep', 'Ramaḍān'], + ['Oct', 'Shawwāl'], + ['Nov', 'Ḏū al-Qaʿdah'], + ['Dec', 'Ḏū al-Ḥijjah'] + ]; + + weekDays = [ + 'M', + 'T', + 'W', + 'T', + 'F', + 'S', + 'S', + ]; + + weeks = []; + days: number[] = []; + day: number; + month: number; + year: number; + + disableDaySelector: boolean; + + calendars = [ + 'GREGORIAN', + 'JULIAN', + 'ISLAMIC' + ]; + + era = 'CE'; + // ------ + + private _required = false; + private _disabled = false; + private _placeholder: string; + + onChange = (_: any) => { }; + onTouched = () => { }; + + get empty() { + const dateInput = this.dateForm.value; + return !dateInput.knoraDate; + } + + @HostBinding('class.floating') + get shouldLabelFloat() { + return this.focused || !this.empty; + } + + @Input() + get required() { + return this._required; + } + + set required(req) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + + @Input() + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this._disabled ? this.dateForm.disable() : this.dateForm.enable(); + this.stateChanges.next(); + } + + @Input() + get placeholder() { + return this._placeholder; + } + + set placeholder(plh) { + this._placeholder = plh; + this.stateChanges.next(); + } + + setDescribedByIds(ids: string[]) { + this.describedBy = ids.join(' '); + } + + @Input() + get value(): KnoraDate | null { + const dateValue = this.dateForm.value; + if (dateValue !== null) { + return dateValue.knoraDate; + } + return null; + } + + set value(dateValue: KnoraDate | null) { + + if (dateValue !== null) { + + this.dateForm.setValue({ + date: this._knoraDatePipe.transform(dateValue, 'dd.MM.YYYY', 'era'), knoraDate: dateValue + }); + this.calendar = dateValue.calendar; + this.era = (this.calendar === 'ISLAMIC' ? 'noEra' : (dateValue.era === 'noEra' ? 'CE' : dateValue.era)); + this.day = dateValue.day; + this.month = (dateValue.month ? dateValue.month : 0); + this.year = dateValue.year; + } else { + this.dateForm.setValue({ date: null, knoraDate: null }); + } + + this.stateChanges.next(); + this.buildForm(); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + constructor( + _defaultErrorStateMatcher: ErrorStateMatcher, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, + @Optional() @Self() public ngControl: NgControl, + fb: FormBuilder, + private _elRef: ElementRef, + private _fm: FocusMonitor, + private _knoraDatePipe: KnoraDatePipe, + private _valueService: ValueService, + ) { + + super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); + + this.dateForm = fb.group({ + date: [null, Validators.required], + knoraDate: [null, Validators.required] + }); + + _fm.monitor(_elRef.nativeElement, true).subscribe(origin => { + this.focused = !!origin; + this.stateChanges.next(); + }); + + if (this.ngControl != null) { + this.ngControl.valueAccessor = this; + } + + // will be replaced by calendar and era + this.placeholder = 'Click to select a date'; + + this.buildForm(); + + this.dateForm.valueChanges + .subscribe(data => this.handleInput()); + + } + + ngOnChanges(changes: SimpleChanges) { + + // in case the calendar has changed (from parent e.g. in a period) + // update the calendar form control + if (changes.calendar && this.disableCalendarSelector) { + this._updateForm(); + } + + } + + ngDoCheck() { + + if (this.ngControl) { + this.updateErrorState(); + } + } + + ngOnDestroy() { + this.stateChanges.complete(); + } + + onContainerClick(event: MouseEvent) { + if ((event.target as Element).tagName.toLowerCase() !== 'input') { + this._elRef.nativeElement.querySelector('input').focus(); + } + } + + writeValue(dateValue: KnoraDate | null): void { + this.value = dateValue; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + handleInput() { + this.onChange(this.value); + } + + buildForm() { + + this.form = new FormGroup({ + calendar: new FormControl(), + era: new FormControl(''), + year: new FormControl('', [ + Validators.required, + Validators.min(1) + ]), + month: new FormControl('') + }); + + this.disableDaySelector = (this.month === 0); + + if (this.value) { + this._updateForm(); + } else if (!this.disableCalendarSelector) { + this.setToday(); + } + + + this.form.valueChanges + .subscribe(data => this.onValueChanged(data)); + } + + /** + * this method is for the form error handling + * + * @param data Data which changed. + */ + onValueChanged(data?: any) { + + if (!this.form) { + return; + } + + this.calendar = this.form.controls.calendar.value; + + this.era = (this.calendar === 'ISLAMIC' ? 'noEra' : (this.form.controls.era.value ? this.form.controls.era.value : 'CE')); + // islamic calendar doesn't have a "before common era" + // in case of switching calendar from islamic to gregorian or julian set default era value to CE + if (this.calendar !== 'ISLAMIC' && this.era === 'noEra') { + this.form.controls.era.setValue('CE'); + } + + if (data.year > 0) { + if (data.month) { + // give possibility to select day; + this.disableDaySelector = false; + // set the corresponding days + this._setDays(this.calendar, this.era, data.year, data.month); + } else { + // set precision to year only; disable the day selector + this.disableDaySelector = true; + this.day = undefined; + } + } else { + // not valid form; disable the day selector + this.disableDaySelector = true; + this.day = undefined; + } + + const form = this.form; + + Object.keys(this.formErrors).map(field => { + this.formErrors[field] = ''; + const control = form.get(field); + if (control && control.dirty && !control.valid) { + const messages = this.validationMessages[field]; + Object.keys(control.errors).map(key => { + this.formErrors[field] += messages[key] + ' '; + }); + + } + }); + + this.setDate(this.day); + } + + setDate(day?: number) { + + // set date on year, on year and month or on year, month and day precision + if (this.form.controls.year.value > 0 && this.form.valid) { + this.day = day; + this.date = new KnoraDate( + this.calendar.toUpperCase(), + this.era, + this.form.controls.year.value, + this.form.controls.month.value ? this.form.controls.month.value : undefined, + day ? day : undefined + ); + + this.value = this.date; + + } + } + + setToday() { + + const today = new Date(); + + let day: number; + let month: number; + let year: number; + + this.era = 'CE'; + + switch (this.calendar) { + // islamic calendar + case 'ISLAMIC': + // found solution and formula here: + // https://medium.com/@Saf_Bes/get-today-hijri-date-in-javascript-90855d3cd45b + const islamicDay = new Intl.DateTimeFormat('en-TN-u-ca-islamic', { day: 'numeric' }).format(today); + const islamicMonth = new Intl.DateTimeFormat('en-TN-u-ca-islamic', { month: 'numeric' }).format(today); + const islamicYear = new Intl.DateTimeFormat('en-TN-u-ca-islamic', { year: 'numeric' }).format(today); + day = parseInt(islamicDay, 0); + month = parseInt(islamicMonth, 0); + year = parseInt(islamicYear.substr(0, 4), 0); + this.era = 'noEra'; + break; + + // julian calendar + case 'JULIAN': + // found solution and formula here: + // https://sciencing.com/convert-julian-date-calender-date-6017669.html + const julianDate = new Date(); + const difference = parseInt((julianDate.getFullYear() + '').substr(0, 2), 0) * 0.75 - 1.25; + julianDate.setDate(julianDate.getDate() - Math.floor(difference)); + day = julianDate.getDate(); + month = julianDate.getMonth() + 1; + year = julianDate.getFullYear(); + break; + + // gregorian calendar + default: + day = today.getDate(); + month = today.getMonth() + 1; + year = today.getFullYear(); + } + + this.day = day; + this.month = month; + this.year = year; + this._updateForm(); + } + + closeDatePicker() { + if (this.popover) { + this.popover.closeMenu(); + } + } + + private _updateForm() { + this.form.setValue({ + calendar: this.calendar, + era: this.era, + year: this.year, + month: this.month, + }); + if (this.disableCalendarSelector) { + this.form.controls.calendar.disable(); + } else { + this.form.controls.calendar.enable(); + } + this._setDays(this.calendar, this.era, this.year, this.month); + } + + /** + * sets available days for a given year and month. + * + * @param calendar calendar of the given date. + * @param era era of the given date. + * @param year year of the given date. + * @param month month of the given date. + */ + private _setDays(calendar: string, era: string, year: number, month: number) { + + const yearAstro = this._valueService.convertHistoricalYearToAstronomicalYear(year, era, calendar.toUpperCase()); + + // count the days of the month + let days = this._valueService.calculateDaysInMonth(calendar.toUpperCase(), yearAstro, month); + + // calculate the week day and the position of the first day of the month + // if date is before October 4th 1582, we should use the julian date converter for week day + let firstDayOfMonth: number; + + const h = (month <= 2 ? month + 12 : month); + const k = (month <= 2 ? year - 1 : year); + + // calculate weekday of the first the of the month; + // found solution and formular here: + // https://straub.as/java/basic/kalender.html + if (year < 1582 || (year === 1582 && month <= 10) || calendar === 'JULIAN') { + // get the day of the week by using the julian date converter independet from selected calendar + firstDayOfMonth = (1 + 2 * h + Math.floor((3 * h + 3) / 5) + k + Math.floor(k / 4) - 1) % 7; + } else { + // firstDayOfMonth = new Date(year, month - 1, 1).getDay(); + firstDayOfMonth = (1 + 2 * h + Math.floor((3 * h + 3) / 5) + k + Math.floor(k / 4) - Math.floor(k / 100) + Math.floor(k / 400) + 1) % 7; + } + + // empty array of the days + this.days = []; + + // if first day of the month is sunday (0) + // move it to the end of the week (7) + // because the first column is prepared for Monday + if (firstDayOfMonth === 0) { + firstDayOfMonth = 7; + } + + // if era is not before common era, we support + // week days. The following loop helps to set + // position of the first day of the month + if (era === 'CE') { + for (let i = 1; i < firstDayOfMonth; i++) { + this.days.push(0); + } + } + + // prepare list of the days + for (let i = 1; i <= days; i++) { + // special case for October 1582, which had only 21 days instead of 31 + // because of the change from julian to gregorian calendar + if (calendar === 'GREGORIAN' && (year === 1582 && month === 10) && i === 5 && era === 'CE') { + i = 15; + days = 31; + } + this.days.push(i); + } + + // split the list of the days in to + // list of days per week corresponding to the week day + const dates = this.days; + const weeks = []; + while (dates.length > 0) { + weeks.push(dates.splice(0, 7)); + } + this.weeks = weeks; + + } + +} diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.html b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.html new file mode 100644 index 0000000000..91a1ecacb2 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.html @@ -0,0 +1,33 @@ +
+ +
+
+ +
+ + + +
+ + +
+
+ +
+ + One date is required + + + End date is required + + + End date must be after start date. + +
+ +
diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.scss b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.scss new file mode 100644 index 0000000000..6fd2fc03d0 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.scss @@ -0,0 +1,16 @@ +.date-form-grid { + display: flex; + + .date-form-field { + min-width: 120px; + } + + .toggle-period-control { + height: 48px; + min-width: 32px; + padding: 0; + margin: 0 8px; + } + +} + diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.spec.ts b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.spec.ts new file mode 100644 index 0000000000..c29d3a50e5 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.spec.ts @@ -0,0 +1,384 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, NgControl, NG_VALUE_ACCESSOR, ReactiveFormsModule +} from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { ErrorStateMatcher, MatOptionModule } from '@angular/material/core'; +import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { KnoraDate, KnoraPeriod } from '@dasch-swiss/dsp-js'; +import { Subject } from 'rxjs'; +import { DateValueHandlerComponent } from './date-value-handler.component'; + +/** + * test host component to simulate parent component. + */ +@Component({ + template: ` +
+ +
` +}) +class TestHostComponent implements OnInit { + + @ViewChild('dateValueHandler') dateValueHandlerComponent: DateValueHandlerComponent; + + form: FormGroup; + + constructor(private _fb: FormBuilder) { + } + + ngOnInit(): void { + + this.form = this._fb.group({ + date: [new KnoraDate('JULIAN', 'CE', 2018, 5, 19)] + }); + + } +} + +/** + * test host component to simulate parent component. + */ +@Component({ + template: ` +
+ + +
` +}) +class NoValueRequiredTestHostComponent implements OnInit { + + @ViewChild('dateValueHandler') dateValueHandlerComponent: DateValueHandlerComponent; + + form: FormGroup; + + constructor(private _fb: FormBuilder) { + } + + ngOnInit(): void { + + this.form = this._fb.group({ + date: new FormControl(null) + }); + + } +} + +@Component({ + selector: 'app-date-picker', + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => TestDatePickerComponent), + }, + { provide: MatFormFieldControl, useExisting: TestDatePickerComponent } + ] +}) + +class TestDatePickerComponent implements ControlValueAccessor, MatFormFieldControl { + + @Input() value; + @Input() disabled: boolean; + @Input() empty: boolean; + @Input() placeholder: string; + @Input() required: boolean; + @Input() shouldLabelFloat: boolean; + @Input() errorStateMatcher: ErrorStateMatcher; + @Input() valueRequiredValidator = true; + + @Input() calendar: string; + stateChanges = new Subject(); + + errorState = false; + focused = false; + id = 'testid'; + ngControl: NgControl | null; + onChange = (_: any) => { }; + + + writeValue(date: KnoraDate | null): void { + this.value = date; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + } + + onContainerClick(event: MouseEvent): void { + } + + setDescribedByIds(ids: string[]): void { + } + + _handleInput(): void { + this.onChange(this.value); + } + +} + +describe('DateValueHandlerComponent', () => { + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + DateValueHandlerComponent, + TestDatePickerComponent, + TestHostComponent + ], + imports: [ + BrowserAnimationsModule, + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + ReactiveFormsModule, + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(testHostFixture); + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + }); + + it('should initialize a date correctly', async () => { + + expect(testHostComponent.dateValueHandlerComponent.startDate.value).toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + expect(testHostComponent.dateValueHandlerComponent.isPeriodControl.value).toBeFalse(); + + expect(testHostComponent.dateValueHandlerComponent.endDate.value).toBeNull(); + + const hostCompDe = testHostFixture.debugElement; + const datePickerComponentDe = hostCompDe.query(By.directive(TestDatePickerComponent)); + + expect((datePickerComponentDe.componentInstance as TestDatePickerComponent).value).toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(true); + + expect(testHostComponent.dateValueHandlerComponent.value instanceof KnoraDate).toBe(true); + + expect(testHostComponent.dateValueHandlerComponent.value) + .toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + }); + + it('should initialize a period correctly', async () => { + + testHostComponent.form.controls.date.setValue(new KnoraPeriod(new KnoraDate('JULIAN', 'CE', 2018, 5, 19), new KnoraDate('JULIAN', 'CE', 2019, 5, 19))); + + expect(testHostComponent.dateValueHandlerComponent.calendarControl.value).toEqual('JULIAN'); + + expect(testHostComponent.dateValueHandlerComponent.startDate.value).toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + expect(testHostComponent.dateValueHandlerComponent.isPeriodControl.value).toBeTrue(); + + expect(testHostComponent.dateValueHandlerComponent.endDate.value).toEqual(new KnoraDate('JULIAN', 'CE', 2019, 5, 19)); + + testHostFixture.detectChanges(); + + const hostCompDe = testHostFixture.debugElement; + + const startDateEditComponentDe = hostCompDe.query(By.css('.start-date')); + + const endDateEditComponentDe = hostCompDe.query(By.css('.end-date')); + + expect((startDateEditComponentDe.componentInstance as TestDatePickerComponent).value).toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + expect((endDateEditComponentDe.componentInstance as TestDatePickerComponent).value).toEqual(new KnoraDate('JULIAN', 'CE', 2019, 5, 19)); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(true); + + expect(testHostComponent.dateValueHandlerComponent.value instanceof KnoraPeriod).toBe(true); + + expect(testHostComponent.dateValueHandlerComponent.value) + .toEqual(new KnoraPeriod(new KnoraDate('JULIAN', 'CE', 2018, 5, 19), new KnoraDate('JULIAN', 'CE', 2019, 5, 19))); + + }); + + it('should react correctly to changing the calendar for a period', () => { + + testHostComponent.form.controls.date.setValue(new KnoraPeriod(new KnoraDate('JULIAN', 'CE', 2018, 5, 19), new KnoraDate('JULIAN', 'CE', 2019, 5, 19))); + + // expect(testHostComponent.dateValueHandlerComponent.calendarControl.value).toEqual('JULIAN'); + + expect(testHostComponent.dateValueHandlerComponent.startDate.value).toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + expect(testHostComponent.dateValueHandlerComponent.isPeriodControl.value).toBeTrue(); + + expect(testHostComponent.dateValueHandlerComponent.endDate.value).toEqual(new KnoraDate('JULIAN', 'CE', 2019, 5, 19)); + + testHostFixture.detectChanges(); + + const hostCompDe = testHostFixture.debugElement; + + const startDateEditComponentDe = hostCompDe.query(By.css('.start-date')); + + expect((startDateEditComponentDe.componentInstance as TestDatePickerComponent).value.calendar).toEqual('JULIAN'); + + const endDateEditComponentDe = hostCompDe.query(By.css('.end-date')); + + expect((endDateEditComponentDe.componentInstance as TestDatePickerComponent).value.calendar).toEqual('JULIAN'); + + testHostComponent.dateValueHandlerComponent.startDate.setValue(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 19)); + + testHostFixture.detectChanges(); + + expect((startDateEditComponentDe.componentInstance as TestDatePickerComponent).value.calendar).toEqual('GREGORIAN'); + + expect((endDateEditComponentDe.componentInstance as TestDatePickerComponent).value.calendar).toEqual('GREGORIAN'); + + }); + + it('should propagate changes made by the user for a single date', async () => { + + const hostCompDe = testHostFixture.debugElement; + + const startDateEditComponentDe = hostCompDe.query(By.css('.start-date')); + + (startDateEditComponentDe.componentInstance as TestDatePickerComponent).writeValue(new KnoraDate('JULIAN', 'CE', 2019, 5, 19)); + (startDateEditComponentDe.componentInstance as TestDatePickerComponent)._handleInput(); + + await testHostFixture.whenStable(); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(true); + + expect(testHostComponent.form.controls.date.value).toEqual(new KnoraDate('JULIAN', 'CE', 2019, 5, 19)); + }); + + it('should propagate changes made by the user for a period', async () => { + + testHostComponent.dateValueHandlerComponent.isPeriodControl.setValue(true); + + testHostFixture.detectChanges(); + + const hostCompDe = testHostFixture.debugElement; + + const startDateEditComponentDe = hostCompDe.query(By.css('.start-date')); + + (startDateEditComponentDe.componentInstance as TestDatePickerComponent).writeValue(new KnoraDate('JULIAN', 'CE', 2019, 5, 19)); + (startDateEditComponentDe.componentInstance as TestDatePickerComponent)._handleInput(); + + const endDateEditComponentDe = hostCompDe.query(By.css('.end-date')); + + (endDateEditComponentDe.componentInstance as TestDatePickerComponent).writeValue(new KnoraDate('JULIAN', 'CE', 2020, 5, 19)); + (endDateEditComponentDe.componentInstance as TestDatePickerComponent)._handleInput(); + + await testHostFixture.whenStable(); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(true); + + expect(testHostComponent.form.controls.date.value).toEqual(new KnoraPeriod(new KnoraDate('JULIAN', 'CE', 2019, 5, 19), new KnoraDate('JULIAN', 'CE', 2020, 5, 19))); + }); + + it('should return "null" for an invalid user input (start date greater than end date)', async () => { + + testHostComponent.dateValueHandlerComponent.isPeriodControl.setValue(true); + + testHostComponent.dateValueHandlerComponent.startDate.setValue(new KnoraDate('JULIAN', 'CE', 2021, 5, 19)); + + testHostComponent.dateValueHandlerComponent.endDate.setValue(new KnoraDate('JULIAN', 'CE', 2020, 5, 19)); + + await testHostFixture.whenStable(); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(false); + + expect(testHostComponent.form.controls.date.value).toBeNull(); + }); + + it('should return "null" for an invalid user input (start date greater than end date) 2', async () => { + + testHostComponent.dateValueHandlerComponent.isPeriodControl.setValue(true); + + testHostComponent.dateValueHandlerComponent.startDate.setValue(new KnoraDate('JULIAN', 'CE', 2021, 5, 19)); + + testHostComponent.dateValueHandlerComponent.endDate.setValue(new KnoraDate('JULIAN', 'BCE', 2022, 5, 19)); + + await testHostFixture.whenStable(); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(false); + + expect(testHostComponent.form.controls.date.value).toBeNull(); + }); + + it('should initialize the date with an empty value', () => { + + testHostComponent.form.controls.date.setValue(null); + + expect(testHostComponent.dateValueHandlerComponent.startDate.value).toBe(null); + expect(testHostComponent.dateValueHandlerComponent.isPeriodControl.value).toBe(false); + expect(testHostComponent.dateValueHandlerComponent.endDate.value).toBe(null); + expect(testHostComponent.dateValueHandlerComponent.calendarControl.value).toEqual('GREGORIAN'); + + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(false); + + }); +}); + + +describe('DateValueHandlerComponent (no validator required)', () => { + let testHostComponent: NoValueRequiredTestHostComponent; + let testHostFixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatOptionModule, + MatCheckboxModule, + BrowserAnimationsModule, + ], + declarations: [DateValueHandlerComponent, TestDatePickerComponent, NoValueRequiredTestHostComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(NoValueRequiredTestHostComponent); + testHostComponent = testHostFixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(testHostFixture); + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + }); + + it('should receive the propagated valueRequiredValidator from the parent component', () => { + expect(testHostComponent.dateValueHandlerComponent.valueRequiredValidator).toBe(false); + }); + + it('should mark the form\'s validity correctly', () => { + expect(testHostComponent.dateValueHandlerComponent.form.valid).toBe(true); + }); + +}); diff --git a/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.ts b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.ts new file mode 100644 index 0000000000..418cc5049a --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/date-value-handler/date-value-handler.component.ts @@ -0,0 +1,313 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { FocusMonitor } from '@angular/cdk/a11y'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Component, DoCheck, ElementRef, HostBinding, Input, OnDestroy, OnInit, Optional, Self } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgControl, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + CanUpdateErrorState, + CanUpdateErrorStateCtor, + ErrorStateMatcher, + mixinErrorState +} from '@angular/material/core'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { KnoraDate, KnoraPeriod } from '@dasch-swiss/dsp-js'; +import { JDNConvertibleCalendar } from 'jdnconvertiblecalendar'; +import { Subject, Subscription } from 'rxjs'; +import { ValueService } from '../../../services/value.service'; + +/** if a period is defined, start date must be before end date */ +export function periodStartEndValidator(isPeriod: FormControl, endDate: FormControl, valueService: ValueService): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + + if (isPeriod.value && control.value !== null && endDate.value !== null) { + // period: check if start is before end + + const jdnStartDate = valueService.createJDNCalendarDateFromKnoraDate(control.value); + const jdnEndDate = valueService.createJDNCalendarDateFromKnoraDate(endDate.value); + + const invalid = jdnStartDate.toJDNPeriod().periodEnd >= jdnEndDate.toJDNPeriod().periodStart; + + return invalid ? { 'periodStartEnd': { value: control.value } } : null; + + } + + return null; + }; +} + +class MatInputBase { + constructor(public _defaultErrorStateMatcher: ErrorStateMatcher, + public _parentForm: NgForm, + public _parentFormGroup: FormGroupDirective, + public ngControl: NgControl) { + } +} + +const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase = + mixinErrorState(MatInputBase); + + +@Component({ + selector: 'app-date-value-handler', + templateUrl: './date-value-handler.component.html', + styleUrls: ['./date-value-handler.component.scss'], + providers: [{ provide: MatFormFieldControl, useExisting: DateValueHandlerComponent }] +}) +export class DateValueHandlerComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl, DoCheck, CanUpdateErrorState, OnInit, OnDestroy { + + static nextId = 0; + + @Input() valueRequiredValidator = true; + + form: FormGroup; + stateChanges = new Subject(); + + isPeriodControl: FormControl; + calendarControl: FormControl; + startDate: FormControl; + endDate: FormControl; + + readonly focused = false; + + readonly controlType = 'app-date-value-handler'; + + calendars = JDNConvertibleCalendar.supportedCalendars.map(cal => cal.toUpperCase()); + + private _subscriptions: Subscription[] = []; + + @Input() + get value(): KnoraDate | KnoraPeriod | null { + + if (!this.form.valid) { + return null; + } + + if (!this.isPeriodControl.value) { + return this.startDate.value; + } else { + if (this.startDate.value.calendar !== this.endDate.value.calendar) { + this.endDate.value.calendar = this.startDate.value.calendar; + } + + return new KnoraPeriod(this.startDate.value, this.endDate.value); + } + } + + set value(date: KnoraDate | KnoraPeriod | null) { + + if (date instanceof KnoraDate) { + // single date + this.calendarControl.setValue(date.calendar); + this.isPeriodControl.setValue(false); + this.startDate.setValue(date); + } else if (date instanceof KnoraPeriod) { + // period + this.calendarControl.setValue(date.start.calendar); + this.isPeriodControl.setValue(true); + this.startDate.setValue(date.start); + this.endDate.setValue(date.end); + } else { + // null + this.calendarControl.setValue('GREGORIAN'); + this.isPeriodControl.setValue(false); + this.startDate.setValue(null); + this.endDate.setValue(null); + } + + this.stateChanges.next(); + } + + @Input() + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + this._disabled ? this.form.disable() : this.form.enable(); + this.stateChanges.next(); + } + + private _disabled = false; + + @Input() + get placeholder() { + return this._placeholder; + } + + set placeholder(plh) { + this._placeholder = plh; + this.stateChanges.next(); + } + + private _placeholder: string; + + @Input() + get required() { + return this._required; + } + + set required(req) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + + private _required = false; + + @HostBinding('class.floating') + get shouldLabelFloat() { + return this.focused || !this.empty; + } + + @HostBinding() id = `app-date-value-handler-${DateValueHandlerComponent.nextId++}`; + + constructor(fb: FormBuilder, + @Optional() @Self() public ngControl: NgControl, + private _fm: FocusMonitor, + private _elRef: ElementRef, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, + _defaultErrorStateMatcher: ErrorStateMatcher, + private _valueService: ValueService) { + super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); + + if (this.ngControl != null) { + // setting the value accessor directly (instead of using + // the providers) to avoid running into a circular import. + this.ngControl.valueAccessor = this; + } + + this.isPeriodControl = new FormControl(false); // tODO: if period, check if start is before end + this.calendarControl = new FormControl(null); + + this.endDate = new FormControl(null); + this.startDate = new FormControl(null); + + const eraChangesSubscription = this.isPeriodControl.valueChanges.subscribe( + isPeriod => { + this.endDate.clearValidators(); + + if (isPeriod && this.valueRequiredValidator) { + // end date is required in case of a period + this.endDate.setValidators([Validators.required]); + } + + this.endDate.updateValueAndValidity(); + } + ); + + this._subscriptions.push(eraChangesSubscription); + + // tODO: find better way to detect changes + const startValueSubscription = this.startDate.valueChanges.subscribe( + () => { + // form's validity has not been updated yet, + // trigger update + this.form.updateValueAndValidity(); + this.handleInput(); + } + ); + + this._subscriptions.push(startValueSubscription); + + // tODO: find better way to detect changes + const endValueSubscription = this.endDate.valueChanges.subscribe( + () => { + // trigger period check validator set on start date control + this.startDate.updateValueAndValidity(); + // form's validity has not been updated yet, + // trigger update + this.form.updateValueAndValidity(); + this.handleInput(); + } + ); + + this._subscriptions.push(endValueSubscription); + + // init form + this.form = fb.group({ + isPeriod: this.isPeriodControl, + calendar: this.calendarControl, + startDate: this.startDate, + endDate: this.endDate + }); + + } + + onChange = (_: any) => { + }; + + onTouched = () => { + }; + + get empty() { + return !this.startDate && !this.endDate; + } + + ngOnInit(): void { + if (this.valueRequiredValidator) { + this.startDate.setValidators([Validators.required, periodStartEndValidator(this.isPeriodControl, this.endDate, this._valueService)]); + } else { + this.startDate.setValidators([periodStartEndValidator(this.isPeriodControl, this.endDate, this._valueService)]); + } + this.startDate.updateValueAndValidity(); + } + + ngDoCheck() { + if (this.ngControl) { + this.updateErrorState(); + } + } + + ngOnDestroy() { + this.stateChanges.complete(); + + this._subscriptions.forEach( + subs => { + if (subs instanceof Subscription && !subs.closed) { + subs.unsubscribe(); + } + } + ); + } + + writeValue(date: KnoraDate | KnoraPeriod | null): void { + this.value = date; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + handleInput(): void { + this.onChange(this.value); + } + + togglePeriodControl() { + this.isPeriodControl.setValue(!this.isPeriodControl.value); + } + + onContainerClick(event: MouseEvent): void { + } + + setDescribedByIds(ids: string[]): void { + } + +} diff --git a/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.html b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.html new file mode 100644 index 0000000000..16f9d5f996 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.html @@ -0,0 +1,42 @@ + + + + {{valueFormControl.value?.start | knoraDate:ontologyDateFormat:'calendarOnly'}}
+ {{ valueFormControl.value?.start | knoraDate:ontologyDateFormat:'era' }} +  – {{ valueFormControl.value?.end | knoraDate:ontologyDateFormat:'era' }} +
+ + + {{valueFormControl.value | knoraDate:ontologyDateFormat:'calendarOnly'}}
+ {{ valueFormControl.value | knoraDate:ontologyDateFormat:'era' }} +
+
+ + {{commentFormControl.value}} +
+ + + + + + New value must be different than the current value. + + + This value already exists for this property. Duplicate values are not allowed. + + + + + + + + diff --git a/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.scss b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.spec.ts b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.spec.ts new file mode 100644 index 0000000000..fa446edcc8 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.spec.ts @@ -0,0 +1,653 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component, DebugElement, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { CreateDateValue, KnoraDate, KnoraPeriod, MockResource, ReadDateValue, UpdateDateValue } from '@dasch-swiss/dsp-js'; +import { Subject } from 'rxjs'; +import { KnoraDatePipe } from 'src/app/main/pipes/formatting/knoradate.pipe'; +import { YetAnotherDateValueComponent } from './yet-another-date-value.component'; + +@Component({ + selector: 'app-date-value-handler', + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => TestDateInputComponent), + }, + { provide: MatFormFieldControl, useExisting: TestDateInputComponent } + ] +}) +class TestDateInputComponent implements ControlValueAccessor, MatFormFieldControl { + + @Input() value; + @Input() disabled: boolean; + @Input() empty: boolean; + @Input() placeholder: string; + @Input() required: boolean; + @Input() shouldLabelFloat: boolean; + @Input() errorStateMatcher: ErrorStateMatcher; + @Input() valueRequiredValidator = true; + + stateChanges = new Subject(); + errorState = false; + focused = false; + id = 'testid'; + ngControl: NgControl | null; + onChange = (_: any) => { }; + + + writeValue(date: KnoraDate | KnoraPeriod | null): void { + this.value = date; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + } + + onContainerClick(event: MouseEvent): void { + } + + setDescribedByIds(ids: string[]): void { + } + + handleInput(): void { + this.onChange(this.value); + } + +} + +/** + * test host component to simulate parent component. + */ +@Component({ + template: ` + ` +}) +class TestHostDisplayValueComponent implements OnInit { + + @ViewChild('inputVal') inputValueComponent: YetAnotherDateValueComponent; + + displayInputVal: ReadDateValue; + + mode: 'read' | 'update' | 'create' | 'search'; + + ngOnInit() { + + MockResource.getTestThing().subscribe(res => { + const inputVal: ReadDateValue = + res.getValuesAs('http://0.0.0.0:3333/ontology/0001/anything/v2#hasDate', ReadDateValue)[0]; + + this.displayInputVal = inputVal; + + this.mode = 'read'; + }); + + } +} + +/** + * test host component to simulate parent component. + */ +@Component({ + template: ` + ` +}) +class TestHostCreateValueComponent implements OnInit { + + @ViewChild('inputVal') inputValueComponent: YetAnotherDateValueComponent; + + mode: 'read' | 'update' | 'create' | 'search'; + + ngOnInit() { + + this.mode = 'create'; + + } +} + +/** + * test host component to simulate parent component. + */ +@Component({ + template: ` + ` +}) +class TestHostCreateValueNoValueRequiredComponent implements OnInit { + + @ViewChild('inputVal') inputValueComponent: YetAnotherDateValueComponent; + + mode: 'read' | 'update' | 'create' | 'search'; + + ngOnInit() { + + this.mode = 'create'; + + } +} + +describe('YetAnotherDateValueComponent', () => { + let component: YetAnotherDateValueComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + MatInputModule, + BrowserAnimationsModule + ], + declarations: [ + YetAnotherDateValueComponent, + TestDateInputComponent, + TestHostDisplayValueComponent, + TestHostCreateValueComponent, + TestHostCreateValueNoValueRequiredComponent, + KnoraDatePipe + ] + }) + .compileComponents(); + })); + + + describe('display and edit a date value', () => { + let testHostComponent: TestHostDisplayValueComponent; + let testHostFixture: ComponentFixture; + let loader: HarnessLoader; + + let valueComponentDe: DebugElement; + let valueReadModeDebugElement: DebugElement; + let valueReadModeNativeElement; + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostDisplayValueComponent); + testHostComponent = testHostFixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(testHostFixture); + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + expect(testHostComponent.inputValueComponent).toBeTruthy(); + + const hostCompDe = testHostFixture.debugElement; + + valueComponentDe = hostCompDe.query(By.directive(YetAnotherDateValueComponent)); + valueReadModeDebugElement = valueComponentDe.query(By.css('.rm-value')); + valueReadModeNativeElement = valueReadModeDebugElement.nativeElement; + + }); + + it('should display an existing value', () => { + + expect(testHostComponent.inputValueComponent.displayValue.date).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('read'); + + expect(valueReadModeNativeElement.innerText).toEqual('Gregorian\n13.05.2018'); + + }); + + it('should make an existing value editable', () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + expect(testHostComponent.inputValueComponent.datePickerComponent.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + // simulate user input + const newKnoraDate = new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.datePickerComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.datePickerComponent.handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value) + .toEqual(newKnoraDate); + + expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + + const updatedValue = testHostComponent.inputValueComponent.getUpdatedValue(); + + expect(updatedValue instanceof UpdateDateValue).toBeTruthy(); + + expect((updatedValue as UpdateDateValue).calendar).toEqual('GREGORIAN'); + expect((updatedValue as UpdateDateValue).startYear).toEqual(2019); + expect((updatedValue as UpdateDateValue).endYear).toEqual(2019); + expect((updatedValue as UpdateDateValue).startMonth).toEqual(5); + expect((updatedValue as UpdateDateValue).endMonth).toEqual(5); + expect((updatedValue as UpdateDateValue).startDay).toEqual(13); + expect((updatedValue as UpdateDateValue).endDay).toEqual(13); + + }); + + it('should not accept a user input equivalent to the existing value', () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + expect(testHostComponent.inputValueComponent.datePickerComponent.value) + .toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + // simulate user input (equivalent date) + const newKnoraDate = new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13); + + testHostComponent.inputValueComponent.datePickerComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.datePickerComponent.handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value) + .toEqual(newKnoraDate); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + const updatedValue = testHostComponent.inputValueComponent.getUpdatedValue(); + + expect(updatedValue).toBe(false); + + }); + + it('should validate an existing value with an added comment', async () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.displayValue.date).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + const commentTextarea = await loader.getHarness(MatInputHarness); + + await commentTextarea.setValue('this is a comment'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + + const updatedValue = testHostComponent.inputValueComponent.getUpdatedValue(); + + expect(updatedValue instanceof UpdateDateValue).toBeTruthy(); + + expect((updatedValue as UpdateDateValue).valueHasComment).toEqual('this is a comment'); + + }); + + it('should not return an invalid update value', () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + testHostComponent.inputValueComponent.datePickerComponent.value = null; + testHostComponent.inputValueComponent.datePickerComponent.handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + const updatedValue = testHostComponent.inputValueComponent.getUpdatedValue(); + + expect(updatedValue).toBeFalsy(); + + }); + + it('should restore the initially displayed value', () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + // simulate user input + const newKnoraDate = new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.datePickerComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.datePickerComponent.handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13)); + + expect(testHostComponent.inputValueComponent.datePickerComponent.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13)); + + testHostComponent.inputValueComponent.resetFormControl(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + expect(testHostComponent.inputValueComponent.datePickerComponent.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + }); + + it('should set a new display value', done => { + + MockResource.getTestThing().subscribe(res => { + const newDate: ReadDateValue = + res.getValuesAs('http://0.0.0.0:3333/ontology/0001/anything/v2#hasDate', ReadDateValue)[0]; + + newDate.id = 'updatedId'; + + newDate.date = new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13); + + testHostComponent.displayInputVal = newDate; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13)); + + expect(valueReadModeNativeElement.innerText).toEqual('Gregorian\n13.05.2019'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + + done(); + }); + + }); + + it('should unsubscribe when destroyed', () => { + expect(testHostComponent.inputValueComponent.valueChangesSubscription.closed).toBeFalsy(); + + testHostComponent.inputValueComponent.ngOnDestroy(); + + expect(testHostComponent.inputValueComponent.valueChangesSubscription.closed).toBeTruthy(); + + }); + + it('should compare two dates', () => { + + expect(testHostComponent.inputValueComponent.sameDate( + new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13), + new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13))).toEqual(true); + + expect(testHostComponent.inputValueComponent.sameDate( + new KnoraDate('JULIAN', 'CE', 2018, 5, 13), + new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13))).toEqual(false); + + expect(testHostComponent.inputValueComponent.sameDate( + new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13), + new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13))).toEqual(false); + + }); + + it('should compare the existing version of a date to the user input', () => { + + // knoraDate('GREGORIAN', 'CE', 2018, 5, 13) + const initValue: KnoraDate | KnoraPeriod = testHostComponent.inputValueComponent.getInitValue(); + + expect( + testHostComponent.inputValueComponent.standardValueComparisonFunc( + initValue, new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13) + ) + ).toBeTruthy(); + + expect( + testHostComponent.inputValueComponent.standardValueComparisonFunc( + initValue, new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13) + ) + ).toBeFalsy(); + + expect( + testHostComponent.inputValueComponent.standardValueComparisonFunc( + initValue, null + ) + ).toBeFalsy(); + + expect( + testHostComponent.inputValueComponent.standardValueComparisonFunc( + initValue, new KnoraPeriod( + new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13), + new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13) + ) + ) + ).toBeFalsy(); + + }); + + it('should correctly populate an UpdateValue from a KnoraDate', () => { + + const date = new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13); + + const updateVal = new UpdateDateValue(); + + testHostComponent.inputValueComponent.populateValue(updateVal, date); + + expect(updateVal.calendar).toEqual('GREGORIAN'); + expect(updateVal.startEra).toEqual('CE'); + expect(updateVal.startDay).toEqual(13); + expect(updateVal.startMonth).toEqual(5); + expect(updateVal.startYear).toEqual(2018); + expect(updateVal.endEra).toEqual('CE'); + expect(updateVal.endDay).toEqual(13); + expect(updateVal.endMonth).toEqual(5); + expect(updateVal.endYear).toEqual(2018); + + }); + + it('should correctly populate an UpdateValue from a KnoraDate with an Islamic calendar date', () => { + + const date = new KnoraDate('ISLAMIC', 'noEra', 1441); + + const updateVal = new UpdateDateValue(); + + testHostComponent.inputValueComponent.populateValue(updateVal, date); + + expect(updateVal.calendar).toEqual('ISLAMIC'); + expect(updateVal.startEra).toBeUndefined(); + expect(updateVal.startDay).toBeUndefined(); + expect(updateVal.startMonth).toBeUndefined(); + expect(updateVal.startYear).toEqual(1441); + expect(updateVal.endEra).toBeUndefined(); + expect(updateVal.endDay).toBeUndefined(); + expect(updateVal.endMonth).toBeUndefined(); + expect(updateVal.endYear).toEqual(1441); + + }); + + it('should correctly populate an UpdateValue from a KnoraPeriod', () => { + + const dateStart = new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13); + const dateEnd = new KnoraDate('GREGORIAN', 'CE', 2019, 6, 14); + + const updateVal = new UpdateDateValue(); + + testHostComponent.inputValueComponent.populateValue(updateVal, new KnoraPeriod(dateStart, dateEnd)); + + expect(updateVal.calendar).toEqual('GREGORIAN'); + expect(updateVal.startEra).toEqual('CE'); + expect(updateVal.startDay).toEqual(13); + expect(updateVal.startMonth).toEqual(5); + expect(updateVal.startYear).toEqual(2018); + expect(updateVal.endEra).toEqual('CE'); + expect(updateVal.endDay).toEqual(14); + expect(updateVal.endMonth).toEqual(6); + expect(updateVal.endYear).toEqual(2019); + + }); + + it('should correctly populate an UpdateValue from a KnoraPeriod with dates in different eras', () => { + + const dateStart = new KnoraDate('GREGORIAN', 'BCE', 2018, 5, 13); + const dateEnd = new KnoraDate('GREGORIAN', 'CE', 2019, 6, 14); + + const updateVal = new UpdateDateValue(); + + testHostComponent.inputValueComponent.populateValue(updateVal, new KnoraPeriod(dateStart, dateEnd)); + + expect(updateVal.calendar).toEqual('GREGORIAN'); + expect(updateVal.startEra).toEqual('BCE'); + expect(updateVal.startDay).toEqual(13); + expect(updateVal.startMonth).toEqual(5); + expect(updateVal.startYear).toEqual(2018); + expect(updateVal.endEra).toEqual('CE'); + expect(updateVal.endDay).toEqual(14); + expect(updateVal.endMonth).toEqual(6); + expect(updateVal.endYear).toEqual(2019); + + }); + + it('should correctly populate an UpdateValue from a KnoraPeriod with Islamic calendar dates', () => { + + const dateStart = new KnoraDate('ISLAMIC', 'noEra', 1441); + const dateEnd = new KnoraDate('ISLAMIC', 'noEra', 1442); + + const updateVal = new UpdateDateValue(); + + testHostComponent.inputValueComponent.populateValue(updateVal, new KnoraPeriod(dateStart, dateEnd)); + + expect(updateVal.calendar).toEqual('ISLAMIC'); + expect(updateVal.startEra).toBeUndefined(); + expect(updateVal.startDay).toBeUndefined(); + expect(updateVal.startMonth).toBeUndefined(); + expect(updateVal.startYear).toEqual(1441); + expect(updateVal.endEra).toBeUndefined(); + expect(updateVal.endDay).toBeUndefined(); + expect(updateVal.endMonth).toBeUndefined(); + expect(updateVal.endYear).toEqual(1442); + + }); + + }); + + describe('create a date value', () => { + + let testHostComponent: TestHostCreateValueComponent; + let testHostFixture: ComponentFixture; + let loader: HarnessLoader; + + let valueComponentDe: DebugElement; + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostCreateValueComponent); + testHostComponent = testHostFixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(testHostFixture); + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + expect(testHostComponent.inputValueComponent).toBeTruthy(); + + const hostCompDe = testHostFixture.debugElement; + + valueComponentDe = hostCompDe.query(By.directive(YetAnotherDateValueComponent)); + }); + + it('should create a value', () => { + + expect(testHostComponent.inputValueComponent.datePickerComponent.value).toEqual(null); + + // simulate user input + const newKnoraDate = new KnoraDate('JULIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.datePickerComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.datePickerComponent.handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('create'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + + const newValue = testHostComponent.inputValueComponent.getNewValue(); + + expect(newValue instanceof CreateDateValue).toBeTruthy(); + + expect((newValue as CreateDateValue).calendar).toEqual('JULIAN'); + + expect((newValue as CreateDateValue).startDay).toEqual(13); + expect((newValue as CreateDateValue).endDay).toEqual(13); + expect((newValue as CreateDateValue).startMonth).toEqual(5); + expect((newValue as CreateDateValue).endMonth).toEqual(5); + expect((newValue as CreateDateValue).startYear).toEqual(2019); + expect((newValue as CreateDateValue).endYear).toEqual(2019); + + }); + + it('should reset form after cancellation', async () => { + + // simulate user input + const newKnoraDate = new KnoraDate('JULIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.datePickerComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.datePickerComponent.handleInput(); + + testHostFixture.detectChanges(); + + const commentTextarea = await loader.getHarness(MatInputHarness); + await commentTextarea.setValue('created comment'); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('create'); + + expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + + testHostComponent.inputValueComponent.resetFormControl(); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + expect(testHostComponent.inputValueComponent.datePickerComponent.value).toEqual(null); + + const comment = await commentTextarea.getValue(); + + expect(comment).toEqual(''); + + }); + + }); + + describe('create a date value no required value', () => { + + let testHostComponent: TestHostCreateValueNoValueRequiredComponent; + let testHostFixture: ComponentFixture; + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostCreateValueNoValueRequiredComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + expect(testHostComponent.inputValueComponent).toBeTruthy(); + }); + + it('should not create an empty value', () => { + expect(testHostComponent.inputValueComponent.getNewValue()).toBe(false); + expect(testHostComponent.inputValueComponent.form.valid).toBe(true); + }); + + it('should propagate valueRequiredValidator to child component', () => { + expect(testHostComponent.inputValueComponent.valueRequiredValidator).toBe(false); + }); + + }); +}); diff --git a/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.ts b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.ts new file mode 100644 index 0000000000..eeee292860 --- /dev/null +++ b/src/app/workspace/resource/values/yet-another-date-value/yet-another-date-value.component.ts @@ -0,0 +1,188 @@ +import { Component, Inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { CreateDateValue, KnoraDate, KnoraPeriod, ReadDateValue, UpdateDateValue } from '@dasch-swiss/dsp-js'; +import { Subscription } from 'rxjs'; +import { BaseValueDirective } from 'src/app/main/directive/base-value.directive'; +import { ValueErrorStateMatcher } from '../value-error-state-matcher'; +import { DatePickerComponent } from './date-picker/date-picker.component'; + +// https://stackoverflow.com/questions/45661010/dynamic-nested-reactive-form-expressionchangedafterithasbeencheckederror +const resolvedPromise = Promise.resolve(null); + +@Component({ + selector: 'app-yet-another-date-value', + templateUrl: './yet-another-date-value.component.html', + styleUrls: ['./yet-another-date-value.component.scss'] +}) +export class YetAnotherDateValueComponent extends BaseValueDirective implements OnInit, OnChanges, OnDestroy { + + @ViewChild('dateInput') datePickerComponent: DatePickerComponent; + + @Input() displayValue?: ReadDateValue; + + // @Input() displayOptions?: 'era' | 'calendar' | 'all'; + + @Input() ontologyDateFormat = 'dd.MM.YYYY'; + + // @Input() showHexCode = false; + + valueFormControl: FormControl; + commentFormControl: FormControl; + form: FormGroup; + valueChangesSubscription: Subscription; + customValidators = []; + matcher = new ValueErrorStateMatcher(); + + constructor(@Inject(FormBuilder) private _fb: FormBuilder) { + super(); + } + + /** + * returns true if both dates are the same. + * + * @param date1 date for comparison with date2 + * @param date2 date for comparison with date1 + */ + sameDate(date1: KnoraDate, date2: KnoraDate): boolean { + return (date1.calendar === date2.calendar && date1.year === date2.year && date1.month === date2.month && date1.day === date2.day && date1.era === date2.era); + } + + standardValueComparisonFunc(initValue: KnoraDate | KnoraPeriod, curValue: KnoraDate | KnoraPeriod | null): boolean { + let sameValue: boolean; + if (initValue instanceof KnoraDate && curValue instanceof KnoraDate) { + sameValue = this.sameDate(initValue, curValue); + } else if (initValue instanceof KnoraPeriod && curValue instanceof KnoraPeriod) { + sameValue = this.sameDate(initValue.start, curValue.start) && this.sameDate(initValue.end, curValue.end); + } else { + // init value and current value have different types + sameValue = false; + } + return sameValue; + } + + getInitValue(): KnoraDate | KnoraPeriod | null { + if (this.displayValue !== undefined) { + return this.displayValue.date; + } else { + return null; + } + } + + ngOnInit() { + + // initialize form control elements + this.valueFormControl = new FormControl(null); + this.commentFormControl = new FormControl(null); + + // subscribe to any change on the comment and recheck validity + this.valueChangesSubscription = this.commentFormControl.valueChanges.subscribe( + data => { + this.valueFormControl.updateValueAndValidity(); + } + ); + + this.form = this._fb.group({ + value: this.valueFormControl, + comment: this.commentFormControl + }); + + this.resetFormControl(); + + resolvedPromise.then(() => { + // add form to the parent form group + this.addToParentFormGroup(this.formName, this.form); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + this.resetFormControl(); + } + + // unsubscribe when the object is destroyed to prevent memory leaks + ngOnDestroy(): void { + this.unsubscribeFromValueChanges(); + + resolvedPromise.then(() => { + // remove form from the parent form group + this.removeFromParentFormGroup(this.formName); + }); + } + + /** + * given a value and a period or Date, populates the value. + * + * @param value the value to be populated. + * @param dateOrPeriod the date or period to read from. + */ + populateValue(value: UpdateDateValue | CreateDateValue, dateOrPeriod: KnoraDate | KnoraPeriod) { + + if (dateOrPeriod instanceof KnoraDate) { + + value.calendar = dateOrPeriod.calendar; + value.startEra = dateOrPeriod.era !== 'noEra' ? dateOrPeriod.era : undefined; + value.startDay = dateOrPeriod.day; + value.startMonth = dateOrPeriod.month; + value.startYear = dateOrPeriod.year; + + value.endEra = value.startEra; + value.endDay = value.startDay; + value.endMonth = value.startMonth; + value.endYear = value.startYear; + + } else if (dateOrPeriod instanceof KnoraPeriod) { + + value.calendar = dateOrPeriod.start.calendar; + + value.startEra = dateOrPeriod.start.era !== 'noEra' ? dateOrPeriod.start.era : undefined; + value.startDay = dateOrPeriod.start.day; + value.startMonth = dateOrPeriod.start.month; + value.startYear = dateOrPeriod.start.year; + + value.endEra = dateOrPeriod.end.era !== 'noEra' ? dateOrPeriod.end.era : undefined; + value.endDay = dateOrPeriod.end.day; + value.endMonth = dateOrPeriod.end.month; + value.endYear = dateOrPeriod.end.year; + + } + } + + getNewValue(): CreateDateValue | false { + if (this.mode !== 'create' || !this.form.valid || this.isEmptyVal()) { + return false; + } + + const newDateValue = new CreateDateValue(); + + const dateOrPeriod = this.valueFormControl.value; + + this.populateValue(newDateValue, dateOrPeriod); + + if (this.commentFormControl.value !== null && this.commentFormControl.value !== '') { + newDateValue.valueHasComment = this.commentFormControl.value; + } + + return newDateValue; + } + + getUpdatedValue(): UpdateDateValue | false { + if (this.mode !== 'update' || !this.form.valid) { + return false; + } + + const updatedDateValue = new UpdateDateValue(); + + updatedDateValue.id = this.displayValue.id; + + const dateOrPeriod = this.valueFormControl.value; + + this.populateValue(updatedDateValue, dateOrPeriod); + + // add the submitted comment to updatedIntValue only if user has added a comment + if (this.commentFormControl.value !== null && this.commentFormControl.value !== '') { + updatedDateValue.valueHasComment = this.commentFormControl.value; + } + + return updatedDateValue; + } + +} diff --git a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.html b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.html index a82baa2de4..cde1f5cef2 100644 --- a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.html +++ b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.html @@ -1,9 +1,12 @@ - + + + + diff --git a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.scss b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.scss index 86494d3087..7b96758705 100644 --- a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.scss +++ b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.scss @@ -7,3 +7,12 @@ } } } + + +.small-field { + width: 200px; + display: inline-flex; + top: .48em; + position: relative; + padding-right: 24px; +} diff --git a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.spec.ts b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.spec.ts index 43d700ceb8..4b886c8a50 100644 --- a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.spec.ts +++ b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.spec.ts @@ -1,15 +1,19 @@ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { Component, Inject, OnInit, ViewChild } from '@angular/core'; -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { MatNativeDateModule } from '@angular/material/core'; -import { MatDatepickerModule } from '@angular/material/datepicker'; +import { Component, forwardRef, Inject, Input, OnInit, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ControlValueAccessor, FormBuilder, FormGroup, NgControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { MatMenuModule } from '@angular/material/menu'; +import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { CalendarDate, CalendarPeriod, GregorianCalendarDate } from 'jdnconvertiblecalendar'; -import { CalendarHeaderComponent } from 'src/app/workspace/resource/values/date-value/calendar-header/calendar-header.component'; -import { JDNDatepickerDirective } from 'src/app/workspace/resource/values/jdn-datepicker-directive/jdndatepicker.directive'; +import { KnoraDate } from '@dasch-swiss/dsp-js'; +import { Subject } from 'rxjs'; import { ValueLiteral } from '../operator'; import { SearchDateValueComponent } from './search-date-value.component'; @@ -22,66 +26,125 @@ import { SearchDateValueComponent } from './search-date-value.component'; }) class TestHostComponent implements OnInit { - @ViewChild('dateVal', { static: false }) dateValue: SearchDateValueComponent; + @ViewChild('dateVal', { static: false }) searchDateValComp: SearchDateValueComponent; - form; + form: FormGroup; - constructor(@Inject(FormBuilder) private _fb: FormBuilder) { + constructor(private _fb: FormBuilder) { } ngOnInit() { - this.form = this._fb.group({}); + this.form = this._fb.group({ + dateValue: [new KnoraDate('JULIAN', 'CE', 2018, 5, 19)] + }); } } +@Component({ + selector: 'app-date-picker', + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => TestDatePickerComponent), + }, + { provide: MatFormFieldControl, useExisting: TestDatePickerComponent } + ] +}) + +class TestDatePickerComponent implements ControlValueAccessor, MatFormFieldControl { + + @Input() value; + @Input() disabled: boolean; + @Input() empty: boolean; + @Input() placeholder: string; + @Input() required: boolean; + @Input() shouldLabelFloat: boolean; + @Input() errorStateMatcher: ErrorStateMatcher; + @Input() valueRequiredValidator = true; + + @Input() calendar: string; + stateChanges = new Subject(); + + errorState = false; + focused = false; + id = 'testid'; + ngControl: NgControl | null; + onChange = (_: any) => { }; + + + writeValue(date: KnoraDate | null): void { + this.value = date; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + } + + onContainerClick(event: MouseEvent): void { + } + + setDescribedByIds(ids: string[]): void { + } + + _handleInput(): void { + this.onChange(this.value); + } + +} + describe('SearchDateValueComponent', () => { let testHostComponent: TestHostComponent; let testHostFixture: ComponentFixture; - let loader: HarnessLoader; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - BrowserAnimationsModule, - ReactiveFormsModule, - MatInputModule, - MatDatepickerModule, - MatNativeDateModule - ], + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [ - CalendarHeaderComponent, - JDNDatepickerDirective, SearchDateValueComponent, + TestDatePickerComponent, TestHostComponent + ], + imports: [ + BrowserAnimationsModule, + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatMenuModule, + ReactiveFormsModule, ] }) .compileComponents(); - })); + }); beforeEach(() => { testHostFixture = TestBed.createComponent(TestHostComponent); testHostComponent = testHostFixture.componentInstance; loader = TestbedHarnessEnvironment.loader(testHostFixture); - testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); }); it('should create', () => { - expect(testHostComponent).toBeTruthy(); - expect(testHostComponent.dateValue).toBeTruthy(); + expect(testHostComponent.searchDateValComp).toBeTruthy(); }); - it('should get a date', () => { + it('should get a date', async () => { - const calDate = new CalendarDate(2018, 10, 30); - testHostComponent.dateValue.form.controls['dateValue'].setValue(new GregorianCalendarDate(new CalendarPeriod(calDate, calDate))); + // set date from date picker + testHostComponent.searchDateValComp.form.controls['dateValue'].setValue(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); - const gregorianDate = new ValueLiteral('GREGORIAN:2018-10-30:2018-10-30', 'http://api.knora.org/ontology/knora-api/simple/v2#Date'); + const julianDate = new ValueLiteral('JULIAN:2018-5-19:2018-5-19', 'http://api.knora.org/ontology/knora-api/simple/v2#Date'); - const dateVal = testHostComponent.dateValue.getValue(); + const dateVal = testHostComponent.searchDateValComp.getValue(); - expect(dateVal).toEqual(gregorianDate); + expect(dateVal).toEqual(julianDate); }); }); diff --git a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.ts b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.ts index 7774b6dd30..aa6d26a783 100644 --- a/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.ts +++ b/src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/search-date-value/search-date-value.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Constants } from '@dasch-swiss/dsp-js'; +import { Constants, KnoraDate } from '@dasch-swiss/dsp-js'; import { JDNConvertibleCalendar } from 'jdnconvertiblecalendar'; import { CalendarHeaderComponent } from 'src/app/workspace/resource/values/date-value/calendar-header/calendar-header.component'; import { PropertyValue, Value, ValueLiteral } from '../operator'; @@ -23,7 +23,7 @@ export class SearchDateValueComponent implements OnInit, OnDestroy, PropertyValu form: FormGroup; // custom header for the datepicker - headerComponent = CalendarHeaderComponent; + // headerComponent = CalendarHeaderComponent; constructor(@Inject(FormBuilder) private _fb: FormBuilder) { } @@ -57,15 +57,12 @@ export class SearchDateValueComponent implements OnInit, OnDestroy, PropertyValu getValue(): Value { - const dateObj: JDNConvertibleCalendar = this.form.value.dateValue; + const dateObj: KnoraDate = this.form.value.dateValue; // get calendar format - const calendarFormat = dateObj.calendarName; - // get calendar period - const calendarPeriod = dateObj.toCalendarPeriod(); - // get the date - // eslint-disable-next-line max-len - const dateString = `${calendarFormat.toUpperCase()}:${calendarPeriod.periodStart.year}-${calendarPeriod.periodStart.month}-${calendarPeriod.periodStart.day}:${calendarPeriod.periodEnd.year}-${calendarPeriod.periodEnd.month}-${calendarPeriod.periodEnd.day}`; + const calendarFormat = dateObj.calendar; + // set date object as string + const dateString = `${calendarFormat.toUpperCase()}:${dateObj.year}-${dateObj.month}-${dateObj.day}:${dateObj.year}-${dateObj.month}-${dateObj.day}`; return new ValueLiteral(String(dateString), Constants.KnoraApi + '/ontology/knora-api/simple/v2' + Constants.HashDelimiter + 'Date'); } diff --git a/src/assets/style/_viewer.scss b/src/assets/style/_viewer.scss index c08a92735b..7df9bed1d4 100644 --- a/src/assets/style/_viewer.scss +++ b/src/assets/style/_viewer.scss @@ -53,7 +53,7 @@ .grid-container { display: grid; - grid-template-columns: 70% 30%; + grid-template-columns: 80% 20%; grid-template-rows: auto auto; } diff --git a/src/assets/style/main.scss b/src/assets/style/main.scss index c3194a1956..c1c71659b7 100644 --- a/src/assets/style/main.scss +++ b/src/assets/style/main.scss @@ -12,10 +12,10 @@ body { // material icons .material-icons { - font-family: 'Material Icons'; + font-family: "Material Icons"; font-weight: normal; font-style: normal; - font-size: 24px; /* Preferred icon size */ + font-size: 24px; /* Preferred icon size */ display: inline-block; line-height: 1; text-transform: none; @@ -33,7 +33,7 @@ body { -moz-osx-font-smoothing: grayscale; /* Support for IE. */ - font-feature-settings: 'liga'; + font-feature-settings: "liga"; } // The following styles will override material design! @@ -50,7 +50,6 @@ body { } app-color-value { - .mat-form-field-flex, .color-picker-input { cursor: pointer !important; @@ -107,8 +106,58 @@ app-color-value { white-space: pre-line; } +// date picker adaption +.mat-menu-panel.date-picker { + max-width: 312px !important; +} +.year.withButtonToggle { + top: -0.05em; + + .mat-form-field-suffix { + .suffix-toggle-group { + font-size: 12px; + border-radius: 0; + height: calc(36px - 0.25em); + + .mat-button-toggle-label-content { + line-height: 36px !important; + } + } + } +} + +.year, +.month { + // hide dropdown arrow + .mat-select-arrow { + border: none; + } + + // hide number de- and increase buttons + // input[type="number"]::-webkit-inner-spin-button, + // input[type="number"]::-webkit-outer-spin-button { + // -webkit-appearance: none; + // } + + // input[type="number"] { + // -moz-appearance: textfield; + // } +} + +// our own date picker value does not need additional space at the bottom +// because of the combination with mat-menu which contains the date picker form +.mat-form-field.date-picker-value { + .mat-form-field-wrapper { + padding-bottom: 0 !important; + + .mat-form-field-underline { + bottom: 0 !important; + } + } +} + .mat-form-field.without-border { .mat-form-field-underline { display: none; -} + } }