diff --git a/package-lock.json b/package-lock.json index 040c0cb3f..54b4cafb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,6 +131,14 @@ "tslib": "1.10.0", "typescript": "3.5.3", "webpack-sources": "1.4.3" + }, + "dependencies": { + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + } } }, "@angular-devkit/build-webpack": { @@ -2026,8 +2034,7 @@ "dependencies": { "rxjs": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "bundled": true, "requires": { "tslib": "^1.9.0" } @@ -2173,9 +2180,9 @@ } }, "@types/selenium-webdriver": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.16.tgz", - "integrity": "sha512-lMC2G0ItF2xv4UCiwbJGbnJlIuUixHrioOhNGHSCsYCJ8l4t9hMCUimCytvFv7qy6AfSzRxhRHoGa+UqaqwyeA==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz", + "integrity": "sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw==", "dev": true }, "@types/source-list-map": { @@ -2454,9 +2461,9 @@ } }, "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3748,9 +3755,9 @@ "dev": true }, "compare-versions": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz", - "integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", "dev": true }, "component-bind": { @@ -4623,9 +4630,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.349", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.349.tgz", - "integrity": "sha512-uEb2zs6EJ6OZIqaMsCSliYVgzE/f7/s1fLWqtvRtHg/v5KBF2xds974fUnyatfxIDgkqzQVwFtam5KExqywx0Q==", + "version": "1.3.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.360.tgz", + "integrity": "sha512-RE1pv2sjQiDRRN1nI0fJ0eQHZ9le4oobu16OArnwEUV5ycAU5SNjFyvzjZ1gPUAqBa2Ud1XagtW8j3ZXfHuQHA==", "dev": true }, "elliptic": { @@ -5266,9 +5273,9 @@ "dev": true }, "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -5385,9 +5392,9 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" @@ -6304,9 +6311,9 @@ "dev": true }, "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true }, "is-absolute-url": { @@ -6839,9 +6846,9 @@ "integrity": "sha512-/yBqMepJNHxy+yS4Llt0KQPsHGkMi1FVwcE77cpiJZh2RwoNyGBXOwFz4Am5FNZYBF3oiL39FMzHRwwf6yl6xg==" }, "jdnconvertiblecalendardateadapter": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/jdnconvertiblecalendardateadapter/-/jdnconvertiblecalendardateadapter-0.0.10.tgz", - "integrity": "sha512-qqbWdSe5owgGtRu24QV64IvR4zeE0icnVnRF9eC1Gz+L6wPmOpjPfUfkj+Hs/z14KK0/ADTl7HcslYbO0N2vtw==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/jdnconvertiblecalendardateadapter/-/jdnconvertiblecalendardateadapter-0.0.12.tgz", + "integrity": "sha512-dQ1PNHH0LnUjj1ZYiWH7ELdMgd2RpQR1TpqDY2afC07lB61B+Y9Jmdu5mSppgOI5v8foJCxgt9uRsamRQ0ya3w==", "requires": { "tslib": "^1.9.0" } @@ -8027,9 +8034,9 @@ } }, "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "make-fetch-happen": { @@ -8624,9 +8631,9 @@ } }, "node-releases": { - "version": "1.1.49", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.49.tgz", - "integrity": "sha512-xH8t0LS0disN0mtRCh+eByxFPie+msJUBL/lJDBuap53QGiYPa9joh83K4pCZgWJ+2L4b9h88vCVdXQ60NO2bg==", + "version": "1.1.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.50.tgz", + "integrity": "sha512-lgAmPv9eYZ0bGwUYAKlr8MG6K4CvWliWqnkcT2P8mMAgVrH3lqfBPorFlxiG1pHQnqmavJZ9vbMXUTNyMLbrgQ==", "dev": true, "requires": { "semver": "^6.3.0" @@ -8735,9 +8742,9 @@ } }, "npm-registry-fetch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.2.tgz", - "integrity": "sha512-Z0IFtPEozNdeZRPh3aHHxdG+ZRpzcbQaJLthsm3VhNf6DScicTFRHZzK82u8RsJUsUHkX+QH/zcB/5pmd20H4A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz", + "integrity": "sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw==", "dev": true, "requires": { "JSONStream": "^1.3.4", @@ -9453,9 +9460,9 @@ } }, "postcss-value-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", - "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz", + "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==", "dev": true }, "prepend-http": { @@ -9669,13 +9676,13 @@ } }, "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "dev": true, "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" + "ipaddr.js": "1.9.1" } }, "prr": { @@ -10109,9 +10116,9 @@ "dev": true }, "regjsparser": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.2.tgz", - "integrity": "sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.3.tgz", + "integrity": "sha512-8uZvYbnfAtEm9Ab8NTb3hdLwL4g/LQzEYP7Xs27T96abJCCE2d6r3cPZPQEsLKy0vRSGVNG+/zVGtLr86HQduA==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -11196,9 +11203,9 @@ }, "dependencies": { "readable-stream": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz", - "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -11815,9 +11822,9 @@ "dev": true }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.0.tgz", + "integrity": "sha512-BmndXUtiTn/VDDrJzQE7Mm22Ix3PxgLltW9bSNLoeCY31gnG2OPx0QqJnuc9oMIKioYrz487i6K9o4Pdn0j+Kg==" }, "tslint": { "version": "5.15.0", diff --git a/package.json b/package.json index 52d6607e4..c0a1b6b5d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@angular/router": "~8.2.14", "@knora/api": "file:.yalc/@knora/api", "jdnconvertiblecalendar": "^0.0.5", - "jdnconvertiblecalendardateadapter": "^0.0.10", + "jdnconvertiblecalendardateadapter": "^0.0.12", "ngx-color-picker": "^8.2.0", "rxjs": "~6.4.0", "tslib": "^1.10.0", diff --git a/projects/knora-ui/package.json b/projects/knora-ui/package.json index b70fc88f7..93b9d09ec 100644 --- a/projects/knora-ui/package.json +++ b/projects/knora-ui/package.json @@ -6,6 +6,8 @@ "@angular/core": "^8.2.14", "@angular/material": "^8.2.3", "@angular/cdk": "^8.2.3", - "@knora/api": "file:.yalc/@knora/api" + "@knora/api": "file:.yalc/@knora/api", + "jdnconvertiblecalendar": "^0.0.5", + "jdnconvertiblecalendardateadapter": "^0.0.12" } } diff --git a/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.html b/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.html index fd7d88c90..c999b38b0 100644 --- a/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.html +++ b/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.html @@ -23,6 +23,7 @@

+ {{displayValue.strval}} diff --git a/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.spec.ts b/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.spec.ts index a6c9a9081..af83f1838 100644 --- a/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.spec.ts +++ b/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.spec.ts @@ -183,6 +183,16 @@ class TestGeonameValueComponent { } +@Component({ + selector: `kui-date-value`, + template: `` +}) +class TestDateValueComponent { + @Input() mode; + + @Input() displayValue; +} + /** * Test host component to simulate parent component. */ @@ -253,7 +263,8 @@ describe('DisplayEditComponent', () => { TestDecimalValueComponent, TestGeonameValueComponent, TestTimeValueComponent, - TestColorValueComponent + TestColorValueComponent, + TestDateValueComponent ], providers: [ { diff --git a/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.ts b/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.ts index 572526f29..342caa0e9 100644 --- a/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.ts +++ b/projects/knora-ui/src/lib/viewer/operations/display-edit/display-edit.component.ts @@ -40,7 +40,7 @@ export class DisplayEditComponent implements OnInit { editModeActive = false; shouldShowCommentToggle: boolean; - + // type of given displayValue // or knora-api-js-lib class representing the value valueTypeOrClass: string; @@ -135,7 +135,7 @@ export class DisplayEditComponent implements OnInit { checkCommentToggleVisibility() { this.shouldShowCommentToggle = (this.mode === 'read' && this.displayValue.valueHasComment !== '' && this.displayValue.valueHasComment !== undefined); } - + /** * Given a value, determines the type or class representing it. * @@ -174,7 +174,6 @@ export class DisplayEditComponent implements OnInit { isReadOnly(valueTypeOrClass: string): boolean { return valueTypeOrClass === this.readTextValueAsHtml || valueTypeOrClass === this.readTextValueAsXml || - valueTypeOrClass === this.constants.DateValue || valueTypeOrClass === this.constants.GeomValue; } } diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.html b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.html new file mode 100644 index 000000000..b87e646a4 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.html @@ -0,0 +1,4 @@ + + {{cal}} + + diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.scss b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.scss new file mode 100644 index 000000000..2bea1c416 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.scss @@ -0,0 +1,6 @@ +:host { + .mat-select.kui-calendar-header { + margin: 16px 16px 0 16px !important; + width: calc(100% - 32px) !important; + } +} diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.spec.ts b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.spec.ts new file mode 100644 index 000000000..4f8733394 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.spec.ts @@ -0,0 +1,143 @@ +import {async, ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; + +import {CalendarHeaderComponent} from './calendar-header.component'; +import {ACTIVE_CALENDAR, JDNConvertibleCalendarDateAdapter} from 'jdnconvertiblecalendardateadapter'; +import {MatSelectModule} from '@angular/material/select'; +import {DateAdapter, MatOptionModule} from '@angular/material/core'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {MatCalendar, MatDatepickerContent} from '@angular/material/datepicker'; +import {BehaviorSubject} from 'rxjs'; +import {Component, DebugElement} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {JDNConvertibleCalendarModule} from 'jdnconvertiblecalendar/dist/src/JDNConvertibleCalendar'; +import {CalendarDate, CalendarPeriod, JulianCalendarDate} from 'jdnconvertiblecalendar'; +import GregorianCalendarDate = JDNConvertibleCalendarModule.GregorianCalendarDate; + +@Component({ + selector: `mat-calendar-header`, + template: `` +}) +class TestMatCalendarHeaderComponent { + +} + +describe('CalendarHeaderComponent', () => { + let component: CalendarHeaderComponent; + let fixture: ComponentFixture>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + MatSelectModule, + MatOptionModule, + BrowserAnimationsModule + ], + declarations: [CalendarHeaderComponent, TestMatCalendarHeaderComponent], + providers: [ + { + provide: MatCalendar, useValue: { + activeDate: new GregorianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 3, 17), new CalendarDate(2020, 3, 17))), + updateTodaysDate: () => { + } + } + }, + {provide: DateAdapter, useClass: JDNConvertibleCalendarDateAdapter}, + {provide: ACTIVE_CALENDAR, useValue: new BehaviorSubject('Gregorian')}, + { + provide: MatDatepickerContent, useValue: { + datepicker: { + select: () => { + } + } + } + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CalendarHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should init the selected value and options correctly', fakeAsync(() => { + + expect(component.formControl.value).toEqual('Gregorian'); + + // https://github.com/angular/components/blob/941b5a3529727f583b76068835e07e412e69f4f7/src/material/select/select.spec.ts#L1674-L1692 + component.formControl = new FormControl('Gregorian'); + fixture.detectChanges(); + + const compDe = fixture.debugElement; + + const selectValueDebugElement = compDe.query(By.css('.mat-select-value')); + + const selectDebugElement = compDe.query(By.css('.mat-select')); + + expect(selectValueDebugElement.nativeElement.textContent).toEqual('Gregorian'); + + const trigger = compDe.query(By.css('.mat-select-trigger')).nativeElement; + trigger.click(); + fixture.detectChanges(); + flush(); + + const options: DebugElement[] = selectDebugElement.queryAll(By.css('mat-option')); + + expect(options.length).toEqual(2); + + expect(options[0].nativeElement.innerText).toEqual('Gregorian'); + + expect(options[1].nativeElement.innerText).toEqual('Julian'); + + })); + + it('should perform a calendar conversion when the selection is changed', () => { + + const dateAdapter = TestBed.get(DateAdapter); + + const dateAdapterSpy = spyOn(dateAdapter, 'convertCalendar').and.callFake( + (date, calendar) => { + return new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 3, 4), new CalendarDate(2020, 3, 4))); + }); + + const matCal = TestBed.get(MatCalendar); + + const matCalendarSpy = spyOn(matCal, 'updateTodaysDate').and.stub(); + + const datepickerContent = TestBed.get(MatDatepickerContent); + + const datepickerContentSpy = spyOn(datepickerContent.datepicker, 'select').and.stub(); + + component.formControl.setValue('Julian'); + + expect(dateAdapterSpy).toHaveBeenCalledTimes(1); + + expect(dateAdapterSpy).toHaveBeenCalledWith(new GregorianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 3, 17), new CalendarDate(2020, 3, 17))), 'Julian'); + + expect(matCal.activeDate).toEqual(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 3, 4), new CalendarDate(2020, 3, 4)))); + + expect(datepickerContentSpy).toHaveBeenCalledTimes(1); + + expect(matCalendarSpy).toHaveBeenCalledTimes(1); + + }); + + it('should unsubscribe from value changes subscription when the component is destroyed', () => { + + expect(component.valueChangesSubscription.closed).toEqual(false); + + component.ngOnDestroy(); + + expect(component.valueChangesSubscription.closed).toEqual(true); + + }); + +}); diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.ts b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.ts new file mode 100644 index 000000000..764e6e596 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/calendar-header/calendar-header.component.ts @@ -0,0 +1,85 @@ +/** Custom header component containing a calendar format switcher */ +import {JDNConvertibleCalendarDateAdapter} from 'jdnconvertiblecalendardateadapter'; +import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; +import {JDNConvertibleCalendar} from 'jdnconvertiblecalendar'; +import {MatCalendar, MatDatepickerContent} from '@angular/material/datepicker'; +import {DateAdapter} from '@angular/material/core'; +import {Component, Host, Inject, OnDestroy, OnInit} from '@angular/core'; +import {Subscription} from 'rxjs'; + +@Component({ + selector: 'kui-calendar-header', + templateUrl: './calendar-header.component.html', + styleUrls: ['./calendar-header.component.scss'] +}) +export class CalendarHeaderComponent implements OnInit, OnDestroy { + constructor(@Host() private _calendar: MatCalendar, + private _dateAdapter: DateAdapter, + private _datepickerContent: MatDatepickerContent, + @Inject(FormBuilder) private fb: FormBuilder) { + } + + form: FormGroup; + formControl: FormControl; + valueChangesSubscription: Subscription; + + // a list of supported calendars (Gregorian and Julian) + supportedCalendars = ['Gregorian', 'Julian']; + + ngOnInit() { + + let activeCal; + + // get the currently active calendar from the date adapter + if (this._dateAdapter instanceof JDNConvertibleCalendarDateAdapter) { + activeCal = this._dateAdapter.activeCalendar; + } else { + console.log('date adapter is expected to be an instance of JDNConvertibleCalendarDateAdapter'); + } + + this.formControl = new FormControl(activeCal, Validators.required); + + // build a form for the calendar format selection + this.form = this.fb.group({ + calendar: this.formControl + }); + + // do the conversion when the user selects another calendar format + this.valueChangesSubscription = this.form.valueChanges.subscribe((data) => { + // pass the target calendar format to the conversion method + this.convertDate(data.calendar); + }); + + } + + ngOnDestroy(): void { + if (this.valueChangesSubscription !== undefined) { + this.valueChangesSubscription.unsubscribe(); + } + } + + /** + * Converts the date into the target format. + * + * @param calendar the target calendar format. + */ + convertDate(calendar: 'Gregorian' | 'Julian') { + + if (this._dateAdapter instanceof JDNConvertibleCalendarDateAdapter) { + + // convert the date into the target calendar format + const convertedDate = this._dateAdapter.convertCalendar(this._calendar.activeDate, calendar); + + // set the new date + this._calendar.activeDate = convertedDate; + + // select the new date in the datepicker UI + this._datepickerContent.datepicker.select(convertedDate); + + // update view after calendar format conversion + this._calendar.updateTodaysDate(); + } else { + console.log('date adapter is expected to be an instance of JDNConvertibleCalendarDateAdapter'); + } + } +} diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.html b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.html new file mode 100644 index 000000000..32ba3248a --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.html @@ -0,0 +1,28 @@ +
+ + {{startDateControl.value?.calendarName}} + + + + Start date is required + + + In a period, start and end date must use the same calendar + + + In a period, start must be before end + + + + + + + + {{endDateControl.value?.calendarName}} + + + + + + +
diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.scss b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.scss new file mode 100644 index 000000000..671f9859c --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.scss @@ -0,0 +1,14 @@ +/** + * CSS changed here must also be changed in each child component of a wrapped component + * time-input.component.scss, interval-input.component.scss, etc. + */ + +.child-input-component { + display: inline-block; + vertical-align: bottom; + width: 49%; +} + +.child-input-component:nth-child(2) { + padding-left: 2%; +} diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.spec.ts b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.spec.ts new file mode 100644 index 000000000..50268ed0a --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.spec.ts @@ -0,0 +1,251 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DateInputComponent} from './date-input.component'; +import {Component, OnInit, ViewChild} from '@angular/core'; +import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {KnoraDate, KnoraPeriod} from '@knora/api'; +import {JDNDatepickerDirective} from '../../jdn-datepicker-directive/jdndatepicker.directive'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatInputModule} from '@angular/material/input'; +import {MatDatepickerModule} from '@angular/material/datepicker'; +import {MatJDNConvertibleCalendarDateAdapterModule} from 'jdnconvertiblecalendardateadapter'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {CalendarDate, GregorianCalendarDate, CalendarPeriod, JulianCalendarDate} from 'jdnconvertiblecalendar'; +import {By} from '@angular/platform-browser'; + +/** + * Test host component to simulate parent component. + */ +@Component({ + template: ` +
+ + + +
` +}) +class TestHostComponent implements OnInit { + + @ViewChild('dateInput', {static: false}) dateInputComponent: DateInputComponent; + + form: FormGroup; + + readonly = false; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + + this.form = this.fb.group({ + date: [new KnoraDate('JULIAN', 'CE', 2018, 5, 19)] + }); + + } +} + +describe('DateInputComponent', () => { + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatDatepickerModule, + MatCheckboxModule, + MatJDNConvertibleCalendarDateAdapterModule, + BrowserAnimationsModule + ], + declarations: [DateInputComponent, TestHostComponent, JDNDatepickerDirective] + }) + .compileComponents(); + })); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + }); + + it('should initialize a date correctly', () => { + + expect(testHostComponent.dateInputComponent.value instanceof KnoraDate).toBe(true); + expect(testHostComponent.dateInputComponent.value) + .toEqual(new KnoraDate('JULIAN', 'CE', 2018, 5, 19)); + + expect(testHostComponent.dateInputComponent.startDateControl.value) + .toEqual(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2018, 5, 19), new CalendarDate(2018, 5, 19)))); + + expect(testHostComponent.dateInputComponent.isPeriodControl.value).toBe(false); + + expect(testHostComponent.dateInputComponent.endDateControl.value).toBe(null); + + expect(testHostComponent.dateInputComponent.form.valid).toBe(true); + + }); + + it('should initialize a period correctly', () => { + + testHostComponent.form.controls.date.setValue(new KnoraPeriod(new KnoraDate('JULIAN', 'CE', 2018, 5, 19), new KnoraDate('JULIAN', 'CE', 2019, 5, 19))); + + expect(testHostComponent.dateInputComponent.value instanceof KnoraPeriod).toBe(true); + expect(testHostComponent.dateInputComponent.value) + .toEqual(new KnoraPeriod(new KnoraDate('JULIAN', 'CE', 2018, 5, 19), new KnoraDate('JULIAN', 'CE', 2019, 5, 19))); + + expect(testHostComponent.dateInputComponent.startDateControl.value) + .toEqual(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2018, 5, 19), new CalendarDate(2018, 5, 19)))); + + expect(testHostComponent.dateInputComponent.isPeriodControl.value).toBe(true); + + expect(testHostComponent.dateInputComponent.endDateControl.value) + .toEqual(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2019, 5, 19), new CalendarDate(2019, 5, 19)))); + + expect(testHostComponent.dateInputComponent.form.valid).toBe(true); + + }); + + it('should propagate changes made by the user for a single date', () => { + + testHostComponent.dateInputComponent.form.controls.dateStart.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2019, 5, 19), new CalendarDate(2019, 5, 19)))); + + testHostComponent.dateInputComponent._handleInput(); + + expect(testHostComponent.dateInputComponent.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', () => { + + testHostComponent.dateInputComponent.form.controls.dateStart.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2019, 5, 19), new CalendarDate(2019, 5, 19)))); + + testHostComponent.dateInputComponent.form.controls.isPeriod.setValue(true); + + testHostComponent.dateInputComponent.form.controls.dateEnd.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 5, 19), new CalendarDate(2020, 5, 19)))); + + testHostComponent.dateInputComponent._handleInput(); + + expect(testHostComponent.dateInputComponent.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)', () => { + + testHostComponent.dateInputComponent.form.controls.dateStart.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2021, 5, 19), new CalendarDate(2021, 5, 19)))); + + testHostComponent.dateInputComponent.form.controls.isPeriod.setValue(true); + + testHostComponent.dateInputComponent.form.controls.dateEnd.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 5, 19), new CalendarDate(2020, 5, 19)))); + + testHostComponent.dateInputComponent._handleInput(); + + expect(testHostComponent.dateInputComponent.form.valid).toBe(false); + + expect(testHostComponent.dateInputComponent.value).toEqual(null); + + }); + + it('should return "null" for an invalid user input (start date and end date have different calendars)', () => { + + testHostComponent.dateInputComponent.form.controls.dateStart.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2021, 5, 19), new CalendarDate(2021, 5, 19)))); + + testHostComponent.dateInputComponent.form.controls.isPeriod.setValue(true); + + testHostComponent.dateInputComponent.form.controls.dateEnd.setValue(new GregorianCalendarDate(new CalendarPeriod(new CalendarDate(2022, 5, 19), new CalendarDate(2022, 5, 19)))); + + testHostComponent.dateInputComponent._handleInput(); + + expect(testHostComponent.dateInputComponent.form.valid).toBe(false); + + expect(testHostComponent.dateInputComponent.value).toEqual(null); + + }); + + it('should return "null" for an invalid user input (start date is "null")', () => { + + testHostComponent.dateInputComponent.form.controls.dateStart.setValue(null); + + testHostComponent.dateInputComponent.form.controls.isPeriod.setValue(true); + + testHostComponent.dateInputComponent.form.controls.dateEnd.setValue(new JulianCalendarDate(new CalendarPeriod(new CalendarDate(2020, 5, 19), new CalendarDate(2020, 5, 19)))); + + expect(testHostComponent.dateInputComponent.form.valid).toBe(false); + + expect(testHostComponent.dateInputComponent.value).toEqual(null); + + }); + + it('should initialize the date with an empty value', () => { + + testHostComponent.form.controls.date.setValue(null); + + expect(testHostComponent.dateInputComponent.form.controls.dateStart.value).toBe(null); + expect(testHostComponent.dateInputComponent.form.controls.isPeriod.value).toBe(false); + expect(testHostComponent.dateInputComponent.form.controls.dateEnd.value).toBe(null); + + expect(testHostComponent.dateInputComponent.form.valid).toBe(false); + + }); + + it('should show the toggle when not in readonly mode', () => { + + expect(testHostComponent.dateInputComponent.readonly).toBe(false); + + testHostComponent.dateInputComponent.form.controls.isPeriod.setValue(true); + + testHostFixture.detectChanges(); + + const hostCompDe = testHostFixture.debugElement; + const dateInputComponentDe = hostCompDe.query(By.directive(DateInputComponent)); + + const startDateToggle = dateInputComponentDe.query(By.css('.start mat-datepicker-toggle')); + + expect(startDateToggle).not.toBe(null); + + const endDateToggle = dateInputComponentDe.query(By.css('.end mat-datepicker-toggle')); + + expect(endDateToggle).not.toBe(null); + }); + + it('should not show the toggle when in readonly mode', () => { + + testHostComponent.readonly = true; + + testHostComponent.dateInputComponent.form.controls.isPeriod.setValue(true); + + testHostFixture.detectChanges(); + + expect(testHostComponent.dateInputComponent.readonly).toBe(true); + + const hostCompDe = testHostFixture.debugElement; + const dateInputComponentDe = hostCompDe.query(By.directive(DateInputComponent)); + + const startDateToggle = dateInputComponentDe.query(By.css('.start mat-datepicker-toggle')); + + expect(startDateToggle).toBe(null); + + const endDateToggle = dateInputComponentDe.query(By.css('.end mat-datepicker-toggle')); + + expect(endDateToggle).toBe(null); + + }); + + it('should show the calendar of a date', () => { + + const hostCompDe = testHostFixture.debugElement; + const dateInputComponentDe = hostCompDe.query(By.directive(DateInputComponent)); + + const startDateCalendar = dateInputComponentDe.query(By.css('.start span.calendar')); + + expect(startDateCalendar).not.toBe(null); + + }); + +}); diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.ts b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.ts new file mode 100644 index 000000000..6ec1416e3 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-input/date-input.component.ts @@ -0,0 +1,332 @@ +import {Component, DoCheck, ElementRef, HostBinding, Input, OnDestroy, Optional, Self} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgControl, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import {MatFormFieldControl} from '@angular/material/form-field'; +import {KnoraDate, KnoraPeriod} from '@knora/api'; +import {CanUpdateErrorState, CanUpdateErrorStateCtor, ErrorStateMatcher, mixinErrorState} from '@angular/material/core'; +import {Subject} from 'rxjs'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {FocusMonitor} from '@angular/cdk/a11y'; +import { + CalendarDate, + CalendarPeriod, + GregorianCalendarDate, + JDNConvertibleCalendar, + JulianCalendarDate +} from 'jdnconvertiblecalendar'; +import {CalendarHeaderComponent} from '../calendar-header/calendar-header.component'; + +/** Error when invalid control is dirty, touched, or submitted. */ +export class DateInputErrorStateMatcher 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)); + } +} + +/** If a period is defined, start and end date must have the same calendar */ +export function sameCalendarValidator(isPeriod: FormControl, endDate: FormControl): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + + if (isPeriod.value) { + + let invalid = true; + if (control.value instanceof JDNConvertibleCalendar && endDate.value instanceof JDNConvertibleCalendar) { + invalid = control.value.calendarName !== endDate.value.calendarName; + } + + return invalid ? {'sameCalendarRequired': {value: control.value}} : null; + } + + return null; + }; +} + +/** If a period is defined, start date must be before end date */ +export function periodStartEndValidator(isPeriod: FormControl, endDate: FormControl): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + + if (isPeriod.value) { + let invalid = true; + + if (control.value instanceof JDNConvertibleCalendar && endDate.value instanceof JDNConvertibleCalendar) { + + // check if start is before end + const startAsJdnPeriod = (control.value as JDNConvertibleCalendar).toJDNPeriod(); + const endAsJdnPeriod = (endDate.value as JDNConvertibleCalendar).toJDNPeriod(); + + // check for start after end + invalid = startAsJdnPeriod.periodStart >= endAsJdnPeriod.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: 'kui-date-input', + templateUrl: './date-input.component.html', + styleUrls: ['./date-input.component.scss'], + providers: [{provide: MatFormFieldControl, useExisting: DateInputComponent}] +}) +export class DateInputComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl, DoCheck, CanUpdateErrorState, OnDestroy { + + static nextId = 0; + + form: FormGroup; + stateChanges = new Subject(); + @HostBinding() id = `kui-date-input-${DateInputComponent.nextId++}`; + focused = false; + errorState = false; + controlType = 'kui-date-input'; + matcher = new DateInputErrorStateMatcher(); + + calendarHeaderComponent = CalendarHeaderComponent; + startDateControl: FormControl; + endDateControl: FormControl; + isPeriodControl: FormControl; + + onChange = (_: any) => { + }; + onTouched = () => { + }; + + get empty() { + const userInput = this.form.value; + return !userInput.start && !userInput.end; + } + + @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(); + } + + private _required = false; + + @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() readonly = false; + + @HostBinding('attr.aria-describedby') describedBy = ''; + + setDescribedByIds(ids: string[]) { + this.describedBy = ids.join(' '); + } + + @Input() + get value(): KnoraDate | KnoraPeriod | null { + + if (!this.form.valid) { + return null; + } + + const userInput = this.form.value; + + if (!this.isPeriodControl.value) { + // single date + if (userInput.dateStart !== null) { + return new KnoraDate(userInput.dateStart.calendarName.toUpperCase(), 'CE', userInput.dateStart.calendarStart.year, userInput.dateStart.calendarStart.month, userInput.dateStart.calendarStart.day); + } else { + return null; + } + } else { + // period + if (userInput.dateStart !== null && userInput.dateEnd !== null) { + + const start = new KnoraDate(userInput.dateStart.calendarName.toUpperCase(), 'CE', userInput.dateStart.calendarStart.year, userInput.dateStart.calendarStart.month, userInput.dateStart.calendarStart.day); + const end = new KnoraDate(userInput.dateEnd.calendarName.toUpperCase(), 'CE', userInput.dateEnd.calendarStart.year, userInput.dateEnd.calendarStart.month, userInput.dateEnd.calendarStart.day); + + return new KnoraPeriod(start, end); + } else { + return null; + } + } + } + + set value(date: KnoraDate | KnoraPeriod | null) { + if (date !== null) { + if (date instanceof KnoraDate) { + // single date + + this.form.setValue({ + dateStart: this.createCalendarDate(date), + dateEnd: null, + isPeriod: false + }); + + this.startDateControl.updateValueAndValidity(); + + } else { + // period + const period = date as KnoraPeriod; + + this.form.setValue({ + dateStart: this.createCalendarDate(period.start), + dateEnd: this.createCalendarDate(period.end), + isPeriod: true + }); + + this.startDateControl.updateValueAndValidity(); + + } + } else { + this.form.setValue({dateStart: null, dateEnd: null, isPeriod: false}); + + this.startDateControl.updateValueAndValidity(); + } + this.stateChanges.next(); + } + + /** + * Given a `KnoraDate`, creates a Gregorian or Julian calendar date. + * + * @param date the given KnoraDate. + */ + createCalendarDate(date: KnoraDate): GregorianCalendarDate | JulianCalendarDate { + + const calDate = new CalendarDate(date.year, date.month, date.day); + const period = new CalendarPeriod(calDate, calDate); + + // determine calendar + if (date.calendar === 'GREGORIAN') { + return new GregorianCalendarDate(period); + } else if (date.calendar === 'JULIAN') { + return new JulianCalendarDate(period); + } else { + throw new Error('Unsupported calendar: ' + date.calendar); + } + } + + @Input() errorStateMatcher: ErrorStateMatcher; + + constructor(fb: FormBuilder, + @Optional() @Self() public ngControl: NgControl, + private fm: FocusMonitor, + private elRef: ElementRef, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, + _defaultErrorStateMatcher: ErrorStateMatcher) { + + super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); + + this.endDateControl = new FormControl(null); + this.isPeriodControl = new FormControl(null); + this.startDateControl + = new FormControl( + null, + [Validators.required, sameCalendarValidator(this.isPeriodControl, this.endDateControl), periodStartEndValidator(this.isPeriodControl, this.endDateControl)] + ); + + this.form = fb.group({ + dateStart: this.startDateControl, + dateEnd: this.endDateControl, + isPeriod: this.isPeriodControl + }); + + + fm.monitor(elRef.nativeElement, true).subscribe(origin => { + this.focused = !!origin; + this.stateChanges.next(); + }); + + if (this.ngControl != null) { + this.ngControl.valueAccessor = this; + } + } + + 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(date: KnoraDate | KnoraPeriod | null): void { + this.value = date; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + _handleInput(): void { + // trigger evaluation of validators defined for start date + this.startDateControl.updateValueAndValidity(); + this.onChange(this.value); + } + +} diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.html b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.html new file mode 100644 index 000000000..0b9484aaa --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.html @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.scss b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.scss new file mode 100644 index 000000000..36fb61ae9 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.scss @@ -0,0 +1,11 @@ +:host ::ng-deep .child-value-component { + .mat-form-field-underline { + display: none; + } + .mat-form-field-infix{ + border-top: 0.2em solid transparent !important; + .mat-form-field-underline{ + display: block; + } + } +} diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.spec.ts b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.spec.ts new file mode 100644 index 000000000..2828880ef --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.spec.ts @@ -0,0 +1,492 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DateValueComponent} from './date-value.component'; +import {Component, DebugElement, forwardRef, Input, OnInit, ViewChild} from '@angular/core'; +import {CreateDateValue, KnoraDate, KnoraPeriod, MockResource, ReadDateValue, UpdateDateValue} from '@knora/api'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule} from '@angular/forms'; +import {MatFormFieldControl} from '@angular/material/form-field'; +import {Subject} from 'rxjs'; +import {ErrorStateMatcher} from '@angular/material/core'; +import {By} from '@angular/platform-browser'; +import {MatInputModule} from '@angular/material/input'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; + +@Component({ + selector: `kui-date-input`, + template: ``, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => TestDateInputComponent), + }, + {provide: MatFormFieldControl, useExisting: TestDateInputComponent} + ] +}) +class TestDateInputComponent implements ControlValueAccessor, MatFormFieldControl { + + @Input() readonly = false; + @Input() value; + @Input() disabled: boolean; + @Input() empty: boolean; + @Input() placeholder: string; + @Input() required: boolean; + @Input() shouldLabelFloat: boolean; + @Input() errorStateMatcher: ErrorStateMatcher; + + errorState = false; + focused = false; + id = 'testid'; + ngControl: NgControl | null; + onChange = (_: any) => { + }; + stateChanges = new Subject(); + + 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', {static: false}) inputValueComponent: DateValueComponent; + + displayInputVal: ReadDateValue; + + mode: 'read' | 'update' | 'create' | 'search'; + + ngOnInit() { + + MockResource.getTestthing().subscribe(res => { + const inputVal: ReadDateValue = + res[0].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', {static: false}) inputValueComponent: DateValueComponent; + + mode: 'read' | 'update' | 'create' | 'search'; + + ngOnInit() { + + this.mode = 'create'; + + } +} + +describe('DateValueComponent', () => { + let component: DateValueComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + MatInputModule, + BrowserAnimationsModule + ], + declarations: [ + DateValueComponent, + TestDateInputComponent, + TestHostDisplayValueComponent, + TestHostCreateValueComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DateValueComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('display and edit a date value', () => { + let testHostComponent: TestHostDisplayValueComponent; + let testHostFixture: ComponentFixture; + + let valueComponentDe: DebugElement; + let commentTextareaDebugElement: DebugElement; + let commentTextareaNativeElement; + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostDisplayValueComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + expect(testHostComponent.inputValueComponent).toBeTruthy(); + + const hostCompDe = testHostFixture.debugElement; + + valueComponentDe = hostCompDe.query(By.directive(DateValueComponent)); + + }); + + 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(testHostComponent.inputValueComponent.dateInputComponent.readonly).toEqual(true); + + expect(testHostComponent.inputValueComponent.dateInputComponent.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + }); + + it('should make an existing value editable', () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.dateInputComponent.readonly).toEqual(false); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + expect(testHostComponent.inputValueComponent.dateInputComponent.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + // simulate user input + const newKnoraDate = new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.dateInputComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.dateInputComponent._handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value).toBeTruthy(); + + 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 validate an existing value with an added comment', () => { + + testHostComponent.mode = 'update'; + + testHostFixture.detectChanges(); + + commentTextareaDebugElement = valueComponentDe.query(By.css('textarea.comment')); + commentTextareaNativeElement = commentTextareaDebugElement.nativeElement; + + expect(testHostComponent.inputValueComponent.mode).toEqual('update'); + + expect(testHostComponent.inputValueComponent.displayValue.date).toEqual(new KnoraDate('GREGORIAN', 'CE', 2018, 5, 13)); + + expect(testHostComponent.inputValueComponent.dateInputComponent.readonly).toEqual(false); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + commentTextareaNativeElement.value = 'this is a comment'; + + commentTextareaNativeElement.dispatchEvent(new Event('input')); + + testHostFixture.detectChanges(); + + 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.dateInputComponent.readonly).toEqual(false); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + testHostComponent.inputValueComponent.dateInputComponent.value = null; + testHostComponent.inputValueComponent.dateInputComponent._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.dateInputComponent.readonly).toEqual(false); + + expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy(); + + // simulate user input + const newKnoraDate = new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.dateInputComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.dateInputComponent._handleInput(); + + testHostFixture.detectChanges(); + + expect(testHostComponent.inputValueComponent.valueFormControl.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13)); + + expect(testHostComponent.inputValueComponent.dateInputComponent.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.dateInputComponent.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[0].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(testHostComponent.inputValueComponent.dateInputComponent.value).toEqual(new KnoraDate('GREGORIAN', 'CE', 2019, 5, 13)); + + 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 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 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); + + }); + + }); + + describe('create a date value', () => { + + let testHostComponent: TestHostCreateValueComponent; + let testHostFixture: ComponentFixture; + + let valueComponentDe: DebugElement; + let commentTextareaDebugElement: DebugElement; + let commentTextareaNativeElement; + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostCreateValueComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + expect(testHostComponent.inputValueComponent).toBeTruthy(); + + const hostCompDe = testHostFixture.debugElement; + + valueComponentDe = hostCompDe.query(By.directive(DateValueComponent)); + commentTextareaDebugElement = valueComponentDe.query(By.css('textarea.comment')); + commentTextareaNativeElement = commentTextareaDebugElement.nativeElement; + }); + + it('should create a value', () => { + + expect(testHostComponent.inputValueComponent.dateInputComponent.value).toEqual(null); + + // simulate user input + const newKnoraDate = new KnoraDate('JULIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.dateInputComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.dateInputComponent._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', () => { + + // simulate user input + const newKnoraDate = new KnoraDate('JULIAN', 'CE', 2019, 5, 13); + + testHostComponent.inputValueComponent.dateInputComponent.value = newKnoraDate; + testHostComponent.inputValueComponent.dateInputComponent._handleInput(); + + testHostFixture.detectChanges(); + + commentTextareaNativeElement.value = 'created comment'; + + commentTextareaNativeElement.dispatchEvent(new Event('input')); + + 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.dateInputComponent.value).toEqual(null); + + expect(commentTextareaNativeElement.value).toEqual(''); + + }); + + }); + + +}); diff --git a/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.ts b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.ts new file mode 100644 index 000000000..d3deb120e --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/date-value/date-value.component.ts @@ -0,0 +1,197 @@ +import {Component, Inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild} from '@angular/core'; +import {CreateDateValue, KnoraDate, KnoraPeriod, ReadDateValue, UpdateDateValue} from '@knora/api'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn +} from '@angular/forms'; +import {Subscription} from 'rxjs'; +import {BaseValueComponent} from '../base-value.component'; +import {ErrorStateMatcher} from '@angular/material'; +import {DateInputComponent} from './date-input/date-input.component'; + +/** Error when invalid control is dirty, touched, or submitted. */ +export class DateErrorStateMatcher 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)); + } +} + +@Component({ + selector: 'kui-date-value', + templateUrl: './date-value.component.html', + styleUrls: ['./date-value.component.scss'] +}) +export class DateValueComponent extends BaseValueComponent implements OnInit, OnChanges, OnDestroy { + + @ViewChild('dateInput', {static: false}) dateInputComponent: DateInputComponent; + + @Input() displayValue?: ReadDateValue; + + valueFormControl: FormControl; + commentFormControl: FormControl; + + form: FormGroup; + + valueChangesSubscription: Subscription; + + customValidators = []; + + matcher = new DateErrorStateMatcher(); + + 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 date 1 + */ + sameDate(date1: KnoraDate, date2: KnoraDate): boolean { + return (date1.calendar === date2.calendar && date1.year === date2.year && date1.month === date2.month && date1.day === date2.day); + } + + standardValidatorFunc: (val: any, comment: string, commentCtrl: FormControl) => ValidatorFn + = (initValue: any, initComment: string, commentFormControl: FormControl): ValidatorFn => { + return (control: AbstractControl): { [key: string]: any } | null => { + + let sameValue: boolean; + if (initValue instanceof KnoraDate && control.value instanceof KnoraDate) { + sameValue = this.sameDate(initValue, control.value); + } else if (initValue instanceof KnoraPeriod && control.value instanceof KnoraPeriod) { + sameValue = this.sameDate(initValue.start, control.value.start) && this.sameDate(initValue.end, control.value.end); + } else { + // init value and current value have different types + sameValue = false; + } + + const invalid = (sameValue && initValue.end === control.value.end) + && (initComment === commentFormControl.value || (initComment === null && commentFormControl.value === '')); + + return invalid ? {valueNotChanged: {value: control.value}} : null; + }; + }; + + 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({ + dateValue: this.valueFormControl, + comment: this.commentFormControl + }); + + this.resetFormControl(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.resetFormControl(); + } + + // unsubscribe when the object is destroyed to prevent memory leaks + ngOnDestroy(): void { + this.unsubscribeFromValueChanges(); + } + + /** + * 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; + 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; + value.startDay = dateOrPeriod.start.day; + value.startMonth = dateOrPeriod.start.month; + value.startYear = dateOrPeriod.start.year; + + value.endEra = dateOrPeriod.end.era; + 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) { + 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/projects/knora-ui/src/lib/viewer/values/int-value/int-value.component.spec.ts b/projects/knora-ui/src/lib/viewer/values/int-value/int-value.component.spec.ts index 4bcea25e8..4ca346a9f 100644 --- a/projects/knora-ui/src/lib/viewer/values/int-value/int-value.component.spec.ts +++ b/projects/knora-ui/src/lib/viewer/values/int-value/int-value.component.spec.ts @@ -6,7 +6,6 @@ import { OnInit, Component, ViewChild, DebugElement } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { $ } from 'protractor'; import { By } from '@angular/platform-browser'; /** diff --git a/projects/knora-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts b/projects/knora-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts index f06044ca6..a206334da 100644 --- a/projects/knora-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts +++ b/projects/knora-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts @@ -41,7 +41,7 @@ const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase = selector: 'kui-interval-input', templateUrl: './interval-input.component.html', styleUrls: ['./interval-input.component.scss'], - providers: [{provide: MatFormFieldControl, useExisting: IntervalInputComponent}], + providers: [{provide: MatFormFieldControl, useExisting: IntervalInputComponent}] }) export class IntervalInputComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl, DoCheck, CanUpdateErrorState, OnDestroy { static nextId = 0; diff --git a/projects/knora-ui/src/lib/viewer/values/jdn-datepicker-directive/jdndatepicker.directive.spec.ts b/projects/knora-ui/src/lib/viewer/values/jdn-datepicker-directive/jdndatepicker.directive.spec.ts new file mode 100644 index 000000000..74e0ac319 --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/jdn-datepicker-directive/jdndatepicker.directive.spec.ts @@ -0,0 +1,115 @@ +import {JDNDatepickerDirective} from './jdndatepicker.directive'; +import {Component, OnInit, ViewChild} from "@angular/core"; +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {ACTIVE_CALENDAR, JDNConvertibleCalendarDateAdapter} from "jdnconvertiblecalendardateadapter"; +import {DateAdapter} from "@angular/material/core"; + +/** + * Test host component to simulate parent component. + */ +@Component({ + template: ` + ` +}) +class TestHostComponent implements OnInit { + + @ViewChild(JDNDatepickerDirective, {static: false}) jdnDir; + + activeCalendar: string; + + ngOnInit() { + this.activeCalendar = 'Gregorian'; + } +} + + +describe('JDNDatepickerDirective', () => { + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + + let testBehaviourSubject; + let setNextCalSpy; + let setCompleteSpy; + + let testBehaviourSubjSpy; + + beforeEach(async(() => { + + testBehaviourSubject = jasmine.createSpyObj('ACTIVE_CALENDAR', ['next', 'complete']); + + setNextCalSpy = testBehaviourSubject.next.and.stub(); + setCompleteSpy = testBehaviourSubject.complete.and.stub(); + + TestBed.configureTestingModule({ + declarations: [ + JDNDatepickerDirective, + TestHostComponent + ], + providers: [ + { + provide: DateAdapter, useValue: {}}, + { + provide: ACTIVE_CALENDAR, useValue: testBehaviourSubject + } + ], + imports: [ + BrowserAnimationsModule + ], + }) + .compileComponents(); + + // overrides the injection token defined in JDNDatepickerDirective's metadata + TestBed.overrideProvider(ACTIVE_CALENDAR, {useValue: testBehaviourSubject}); + TestBed.overrideProvider(DateAdapter, {useValue: {}}); + + testBehaviourSubjSpy = TestBed.get(ACTIVE_CALENDAR); + })); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + expect(testHostComponent.jdnDir).toBeTruthy(); + }); + + it('should create an instance', () => { + expect(testBehaviourSubjSpy.next).toHaveBeenCalledTimes(1); + expect(testBehaviourSubjSpy.next).toHaveBeenCalledWith('Gregorian'); + }); + + it('should update the calendar when the input changes', () => { + testHostComponent.activeCalendar = 'Julian'; + testHostFixture.detectChanges(); + + expect(testBehaviourSubjSpy.next).toHaveBeenCalledTimes(2); + + expect(testBehaviourSubjSpy.next.calls.all()[0].args).toEqual(['Gregorian']); + expect(testBehaviourSubjSpy.next.calls.all()[1].args).toEqual(['Julian']); + + }); + + it('should set the calendar to Gregorian when called with null', () => { + testHostComponent.activeCalendar = null; + testHostFixture.detectChanges(); + + expect(testBehaviourSubjSpy.next).toHaveBeenCalledTimes(2); + + expect(testBehaviourSubjSpy.next.calls.all()[0].args).toEqual(['Gregorian']); + expect(testBehaviourSubjSpy.next.calls.all()[1].args).toEqual(['Gregorian']); + + }); + + it('should complete the BehaviourSubject when destroyed', () => { + + expect(setCompleteSpy).toHaveBeenCalledTimes(0); + + testHostComponent.jdnDir.ngOnDestroy(); + + expect(setCompleteSpy).toHaveBeenCalledTimes(1); + + }); + +}); diff --git a/projects/knora-ui/src/lib/viewer/values/jdn-datepicker-directive/jdndatepicker.directive.ts b/projects/knora-ui/src/lib/viewer/values/jdn-datepicker-directive/jdndatepicker.directive.ts new file mode 100644 index 000000000..1c693134a --- /dev/null +++ b/projects/knora-ui/src/lib/viewer/values/jdn-datepicker-directive/jdndatepicker.directive.ts @@ -0,0 +1,46 @@ +import {Directive, Inject, Input, OnChanges, OnDestroy, SimpleChanges} from '@angular/core'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material'; +import {JDNConvertibleCalendar} from 'jdnconvertiblecalendar'; +import {ACTIVE_CALENDAR, JDNConvertibleCalendarDateAdapter} from 'jdnconvertiblecalendardateadapter'; +import {BehaviorSubject} from 'rxjs'; + +export function makeCalendarToken() { + return new BehaviorSubject('Gregorian'); +} + +@Directive({ + selector: 'kui-jdn-datepicker', + providers: [ + {provide: DateAdapter, useClass: JDNConvertibleCalendarDateAdapter, deps: [MAT_DATE_LOCALE, ACTIVE_CALENDAR]}, + {provide: ACTIVE_CALENDAR, useFactory: makeCalendarToken} + ] +}) +export class JDNDatepickerDirective implements OnChanges, OnDestroy { + + private _activeCalendar: 'Gregorian' | 'Julian' | 'Islamic'; + + @Input() + set activeCalendar(value: 'Gregorian' | 'Julian' | 'Islamic' | null) { + if (value !== null && value !== undefined) { + this._activeCalendar = value; + } else { + this._activeCalendar = 'Gregorian'; + } + } + + get activeCalendar() { + return this._activeCalendar; + } + + constructor(@Inject(ACTIVE_CALENDAR) private activeCalendarToken, private adapter: DateAdapter) { + } + + ngOnChanges(changes: SimpleChanges): void { + this.activeCalendarToken.next(this.activeCalendar); + } + + ngOnDestroy(): void { + this.activeCalendarToken.complete(); + } + +} diff --git a/projects/knora-ui/src/lib/viewer/values/time-value/jdn-datepicker-directive/jdndatepicker.directive.spec.ts b/projects/knora-ui/src/lib/viewer/values/time-value/jdn-datepicker-directive/jdndatepicker.directive.spec.ts deleted file mode 100644 index 674517186..000000000 --- a/projects/knora-ui/src/lib/viewer/values/time-value/jdn-datepicker-directive/jdndatepicker.directive.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { JDNDatepickerDirective } from './jdndatepicker.directive'; - -describe('JDNDatepickerDirective', () => { - -}); \ No newline at end of file diff --git a/projects/knora-ui/src/lib/viewer/values/time-value/jdn-datepicker-directive/jdndatepicker.directive.ts b/projects/knora-ui/src/lib/viewer/values/time-value/jdn-datepicker-directive/jdndatepicker.directive.ts deleted file mode 100644 index 44c50d4d5..000000000 --- a/projects/knora-ui/src/lib/viewer/values/time-value/jdn-datepicker-directive/jdndatepicker.directive.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Directive} from '@angular/core'; -import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material'; -import {JDNConvertibleCalendar} from 'jdnconvertiblecalendar'; -import {JDNConvertibleCalendarDateAdapter} from 'jdnconvertiblecalendardateadapter'; - -@Directive({ - selector: 'kuiJDNDatepicker', - providers: [{provide: DateAdapter, useClass: JDNConvertibleCalendarDateAdapter, deps: [MAT_DATE_LOCALE]}] -}) -export class JDNDatepickerDirective { - - constructor(private adapter: DateAdapter) { - } - -} \ No newline at end of file diff --git a/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.html b/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.html index 4334aa44d..a990ad49a 100644 --- a/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.html +++ b/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.html @@ -1,8 +1,10 @@
+ + diff --git a/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.spec.ts b/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.spec.ts index 076e6b79e..49b3bece5 100644 --- a/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.spec.ts +++ b/projects/knora-ui/src/lib/viewer/values/time-value/time-input/time-input.component.spec.ts @@ -1,14 +1,15 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { TimeInputComponent, DateTime } from './time-input.component'; -import { Component, OnInit, ViewChild, DebugElement } from '@angular/core'; -import { FormGroup, FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { MatFormFieldModule, MatInputModule } from '@angular/material'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { By } from '@angular/platform-browser'; -import { MatDatepickerModule } from '@angular/material/datepicker' -import { GregorianCalendarDate, CalendarPeriod, CalendarDate } from 'jdnconvertiblecalendar'; -import { MatJDNConvertibleCalendarDateAdapterModule } from 'jdnconvertiblecalendardateadapter'; +import {TimeInputComponent, DateTime} from './time-input.component'; +import {Component, OnInit, ViewChild, DebugElement} from '@angular/core'; +import {FormGroup, FormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {MatFormFieldModule, MatInputModule} from '@angular/material'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {By} from '@angular/platform-browser'; +import {MatDatepickerModule} from '@angular/material/datepicker' +import {GregorianCalendarDate, CalendarPeriod, CalendarDate} from 'jdnconvertiblecalendar'; +import {MatJDNConvertibleCalendarDateAdapterModule} from 'jdnconvertiblecalendardateadapter'; +import {JDNDatepickerDirective} from "../../jdn-datepicker-directive/jdndatepicker.directive"; /** @@ -54,8 +55,14 @@ describe('TimeInputComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatDatepickerModule, MatJDNConvertibleCalendarDateAdapterModule, BrowserAnimationsModule], - declarations: [TimeInputComponent, TestHostComponent] + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatDatepickerModule, + MatJDNConvertibleCalendarDateAdapterModule, + BrowserAnimationsModule], + declarations: [TimeInputComponent, TestHostComponent, JDNDatepickerDirective] }) .compileComponents(); })); @@ -84,7 +91,7 @@ describe('TimeInputComponent', () => { it('should initialize the date correctly', () => { expect(dateInputNativeElement.value).toEqual('06-08-2019'); - + expect(timeInputNativeElement.value).toEqual('14:00'); }); @@ -127,7 +134,7 @@ describe('TimeInputComponent', () => { expect(dateTime.date.toCalendarPeriod().periodStart.month).toEqual(10); expect(dateTime.date.toCalendarPeriod().periodStart.day).toEqual(10); - expect (dateTime.time).toEqual('12:00'); + expect(dateTime.time).toEqual('12:00'); }); -}); \ No newline at end of file +}); diff --git a/projects/knora-ui/src/lib/viewer/viewer.module.ts b/projects/knora-ui/src/lib/viewer/viewer.module.ts index 497dcc998..0b17bb103 100644 --- a/projects/knora-ui/src/lib/viewer/viewer.module.ts +++ b/projects/knora-ui/src/lib/viewer/viewer.module.ts @@ -3,12 +3,11 @@ import {TextValueAsStringComponent} from './values/text-value/text-value-as-stri import {ReactiveFormsModule} from '@angular/forms'; import {MatInputModule} from '@angular/material/input'; import {MatCheckboxModule} from '@angular/material/checkbox'; -import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {CommonModule} from '@angular/common'; import {ColorPickerModule} from 'ngx-color-picker'; -import { MatMenuModule } from '@angular/material/menu'; - +import {MatMenuModule} from '@angular/material/menu'; import {IntValueComponent} from './values/int-value/int-value.component'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {LinkValueComponent} from './values/link-value/link-value.component'; import {DisplayEditComponent} from './operations/display-edit/display-edit.component'; import {BooleanValueComponent} from './values/boolean-value/boolean-value.component'; @@ -25,9 +24,14 @@ import {TimeValueComponent} from './values/time-value/time-value.component'; import {TimeInputComponent} from './values/time-value/time-input/time-input.component'; import {MatDatepickerModule} from '@angular/material'; import {MatJDNConvertibleCalendarDateAdapterModule} from 'jdnconvertiblecalendardateadapter'; -import { JDNDatepickerDirective } from './values/time-value/jdn-datepicker-directive/jdndatepicker.directive'; -import { ColorPickerComponent } from './values/color-value/color-picker/color-picker.component'; +import {ColorPickerComponent} from './values/color-value/color-picker/color-picker.component'; +import {MatOptionModule} from '@angular/material/core'; +import {MatSelectModule} from '@angular/material/select'; +import {DateValueComponent} from './values/date-value/date-value.component'; +import {CalendarHeaderComponent} from './values/date-value/calendar-header/calendar-header.component'; +import {DateInputComponent} from './values/date-value/date-input/date-input.component'; import {MatIconModule} from '@angular/material/icon'; +import {JDNDatepickerDirective} from './values/jdn-datepicker-directive/jdndatepicker.directive'; @NgModule({ declarations: [ @@ -36,18 +40,18 @@ import {MatIconModule} from '@angular/material/icon'; IntValueComponent, DisplayEditComponent, BooleanValueComponent, - ColorValueComponent, - ColorPickerComponent, - LinkValueComponent, DecimalValueComponent, UriValueComponent, IntervalValueComponent, IntervalInputComponent, TimeValueComponent, TimeInputComponent, - JDNDatepickerDirective, ColorValueComponent, ColorPickerComponent, + DateValueComponent, + DateInputComponent, + JDNDatepickerDirective, + CalendarHeaderComponent, LinkValueComponent, ListValueComponent, SublistValueComponent, @@ -59,13 +63,15 @@ import {MatIconModule} from '@angular/material/icon'; MatInputModule, MatAutocompleteModule, MatCheckboxModule, - MatMenuModule, + MatOptionModule, + MatSelectModule, MatDatepickerModule, MatIconModule, MatJDNConvertibleCalendarDateAdapterModule, + MatMenuModule, ColorPickerModule - ], - + ], + entryComponents: [CalendarHeaderComponent], exports: [ TextValueAsStringComponent, TextValueAsHtmlComponent, @@ -77,6 +83,7 @@ import {MatIconModule} from '@angular/material/icon'; UriValueComponent, IntervalValueComponent, TimeValueComponent, + DateValueComponent, LinkValueComponent, ListValueComponent, SublistValueComponent, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c6305ccc1..f957e1f67 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,9 +1,17 @@ -import { Component, Inject, OnInit, ViewChild } from '@angular/core'; -import { ApiResponseData, IHasProperty, KnoraApiConnection, LoginResponse, ReadResource, ReadValue, ResourcePropertyDefinition } from '@knora/api'; -import { PropertyDefinition } from '@knora/api/src/models/v2/ontologies/property-definition'; -import { KnoraApiConnectionToken } from 'knora-ui'; -import { DisplayEditComponent } from 'knora-ui/lib/viewer/operations/display-edit/display-edit.component'; -import { mergeMap } from 'rxjs/operators'; +import {Component, Inject, OnInit, ViewChild} from '@angular/core'; +import { + ApiResponseData, + IHasProperty, + KnoraApiConnection, + LoginResponse, + ReadResource, + ReadValue, + ResourcePropertyDefinition +} from '@knora/api'; +import {mergeMap} from 'rxjs/operators'; +import {DisplayEditComponent} from 'knora-ui/lib/viewer/operations/display-edit/display-edit.component'; +import {KnoraApiConnectionToken} from 'knora-ui'; +import {PropertyDefinition} from '@knora/api/src/models/v2/ontologies/property-definition'; // object of property information from ontology class, properties and property values export interface PropertyInfoValues { @@ -65,4 +73,4 @@ export class AppComponent implements OnInit { }); } -} \ No newline at end of file +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e15d50713..2f447160b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,11 +1,12 @@ -import { BrowserModule } from '@angular/platform-browser'; +import {BrowserModule} from '@angular/platform-browser'; import {APP_INITIALIZER, NgModule} from '@angular/core'; -import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; -import { KnoraApiConfigToken, KnoraApiConnectionToken, KuiConfigToken, ViewerModule} from 'knora-ui'; +import {AppRoutingModule} from './app-routing.module'; +import {AppComponent} from './app.component'; +import {KnoraApiConfigToken, KnoraApiConnectionToken, KuiConfigToken, ViewerModule} from 'knora-ui'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {AppInitService} from './app-init.service'; +import {MatJDNConvertibleCalendarDateAdapterModule} from 'jdnconvertiblecalendardateadapter'; export function initializeApp(appInitService: AppInitService) { return (): Promise => { @@ -21,7 +22,8 @@ export function initializeApp(appInitService: AppInitService) { BrowserModule, BrowserAnimationsModule, AppRoutingModule, - ViewerModule + ViewerModule, + MatJDNConvertibleCalendarDateAdapterModule ], providers: [ AppInitService, @@ -46,4 +48,5 @@ export function initializeApp(appInitService: AppInitService) { ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule { +} diff --git a/yalc.lock b/yalc.lock index 21bd1b0c0..f116b9f21 100644 --- a/yalc.lock +++ b/yalc.lock @@ -2,7 +2,7 @@ "version": "v1", "packages": { "@knora/api": { - "signature": "cbf63807338d8cbc462923ad349eb3b4", + "signature": "3208f93884cf4115a1c97ef116855f3f", "file": true } }