UNPKG

@knora/search

Version:
773 lines (766 loc) 92.5 kB
import { __decorate, __metadata, __param } from 'tslib'; import { OverlayConfig, ConnectionPositionPair, Overlay } from '@angular/cdk/overlay'; import { TemplatePortal } from '@angular/cdk/portal'; import { EventEmitter, Inject, ViewContainerRef, Input, Output, ViewChild, ElementRef, TemplateRef, Component, ViewChildren, QueryList, Host, NgModule } from '@angular/core'; import { MatMenuTrigger } from '@angular/material'; import { Router, ActivatedRoute } from '@angular/router'; import { Constants, KnoraApiConnection, ReadResource, KnoraApiConfig } from '@knora/api'; import { KnoraApiConnectionToken, OntologyCacheService, GravsearchGenerationService, ExtendedSearchParams, KnoraApiConfigToken, SearchParamsService, Exists, Equals, NotEquals, LessThan, LessThanEquals, GreaterThan, GreaterThanEquals, Like, Match, ComparisonOperatorAndValue, Property, CardinalityOccurrence, OntologyInformation, PropertyWithValue, Properties, ResourceClass, ValueLiteral, KnoraConstants, IRI, Utils, KuiCoreModule } from '@knora/core'; import { FormBuilder, FormGroup, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DateAdapter } from '@angular/material/core'; import { MatCalendar, MatDatepickerContent, MatDatepickerModule } from '@angular/material/datepicker'; import { JDNConvertibleCalendarDateAdapter, MatJDNConvertibleCalendarDateAdapterModule } from 'jdnconvertiblecalendardateadapter'; import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { CommonModule } from '@angular/common'; import { MatListModule } from '@angular/material/list'; import { MatMenuTrigger as MatMenuTrigger$1, MatMenuModule } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; import { KuiActionModule } from '@knora/action'; import { KuiViewerModule } from '@knora/viewer'; /** * */ let FulltextSearchComponent = class FulltextSearchComponent { constructor(knoraApiConnection, _overlay, _router, _viewContainerRef) { this.knoraApiConnection = knoraApiConnection; this._overlay = _overlay; this._router = _router; this._viewContainerRef = _viewContainerRef; /** * * @param {string} route Route to navigate after search. * This route path should contain a component for search results. */ this.route = '/search'; /** * * @param {boolean} [projectfilter] If true it shows the selection * of projects to filter by one of them */ this.projectfilter = false; this.showState = new EventEmitter(); // previous search = full-text search history this.prevSearch = JSON.parse(localStorage.getItem('prevSearch')); this.defaultProjectLabel = 'All projects'; this.projectLabel = this.defaultProjectLabel; // is search panel focused? this.searchPanelFocus = false; // do not show the following projects: default system projects from knora this.doNotDisplay = [ Constants.SystemProjectIRI, Constants.DefaultSharedOntologyIRI ]; } ngOnInit() { // this.setFocus(); if (this.filterbyproject) { this.getProject(this.filterbyproject); } if (this.projectfilter) { this.getAllProjects(); if (localStorage.getItem('currentProject') !== null) { this.setProject(JSON.parse(localStorage.getItem('currentProject'))); } } } openPanelWithBackdrop() { const config = new OverlayConfig({ hasBackdrop: true, backdropClass: 'cdk-overlay-transparent-backdrop', // backdropClass: 'cdk-overlay-dark-backdrop', positionStrategy: this.getOverlayPosition(), scrollStrategy: this._overlay.scrollStrategies.block() }); this.overlayRef = this._overlay.create(config); this.overlayRef.attach(new TemplatePortal(this.searchMenu, this._viewContainerRef)); this.overlayRef.backdropClick().subscribe(() => { this.searchPanelFocus = false; this.overlayRef.detach(); }); } getOverlayPosition() { const positions = [ new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }), new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }) ]; const overlayPosition = this._overlay.position().flexibleConnectedTo(this.searchPanel).withPositions(positions).withLockedPosition(false); return overlayPosition; } getAllProjects() { this.knoraApiConnection.admin.projectsEndpoint.getProjects().subscribe((response) => { this.projects = response.body.projects; // this.loadSystem = false; if (localStorage.getItem('currentProject') !== null) { this.project = JSON.parse(localStorage.getItem('currentProject')); } }, (error) => { console.error(error); this.error = error; }); } getProject(id) { this.knoraApiConnection.admin.projectsEndpoint.getProjectByIri(id).subscribe((project) => { this.setProject(project.body.project); }, (error) => { console.error(error); }); } // set current project and switch focus to input field setProject(project) { if (!project) { // set default project: all this.projectLabel = this.defaultProjectLabel; this.projectIri = undefined; localStorage.removeItem('currentProject'); } else { // set current project shortname and id this.projectLabel = project.shortname; this.projectIri = project.id; localStorage.setItem('currentProject', JSON.stringify(project)); } } doSearch() { if (this.searchQuery !== undefined && this.searchQuery !== null) { if (this.projectIri !== undefined) { this._router.navigate([ this.route + '/fulltext/' + this.searchQuery + '/' + encodeURIComponent(this.projectIri) ]); } else { this._router.navigate([ this.route + '/fulltext/' + this.searchQuery ]); } // push the search query into the local storage prevSearch array (previous search) // to have a list of recent search requests let existingPrevSearch = JSON.parse(localStorage.getItem('prevSearch')); if (existingPrevSearch === null) { existingPrevSearch = []; } let i = 0; for (const entry of existingPrevSearch) { // remove entry, if exists already if (this.searchQuery === entry.query && this.projectIri === entry.projectIri) { existingPrevSearch.splice(i, 1); } i++; } // A search value is expected to have at least length of 3 if (this.searchQuery.length > 2) { let currentQuery = { query: this.searchQuery }; if (this.projectIri) { currentQuery = { projectIri: this.projectIri, projectLabel: this.projectLabel, query: this.searchQuery }; } existingPrevSearch.push(currentQuery); localStorage.setItem('prevSearch', JSON.stringify(existingPrevSearch)); } } this.resetSearch(); this.overlayRef.detach(); this.show = false; this.showState.emit(this.show); } resetSearch() { this.searchPanelFocus = false; this.searchInput.nativeElement.blur(); this.overlayRef.detach(); } setFocus() { this.prevSearch = JSON.parse(localStorage.getItem('prevSearch')); this.searchPanelFocus = true; this.openPanelWithBackdrop(); } doPrevSearch(prevSearch) { this.searchQuery = prevSearch.query; if (prevSearch.projectIri !== undefined) { this.projectIri = prevSearch.projectIri; this.projectLabel = prevSearch.projectLabel; this._router.navigate([this.route + '/fulltext/' + this.searchQuery + '/' + encodeURIComponent(prevSearch.projectIri)]); } else { this.projectIri = undefined; this.projectLabel = this.defaultProjectLabel; this._router.navigate([this.route + '/fulltext/' + this.searchQuery]); } this.resetSearch(); this.overlayRef.detach(); } resetPrevSearch(prevSearch) { if (prevSearch) { // delete only this item with the name ... const i = this.prevSearch.indexOf(prevSearch); this.prevSearch.splice(i, 1); localStorage.setItem('prevSearch', JSON.stringify(this.prevSearch)); } else { // delete the whole "previous search" array localStorage.removeItem('prevSearch'); } this.prevSearch = JSON.parse(localStorage.getItem('prevSearch')); } changeFocus() { this.selectProject.closeMenu(); this.searchInput.nativeElement.focus(); this.setFocus(); } }; FulltextSearchComponent.ctorParameters = () => [ { type: KnoraApiConnection, decorators: [{ type: Inject, args: [KnoraApiConnectionToken,] }] }, { type: Overlay }, { type: Router }, { type: ViewContainerRef } ]; __decorate([ Input(), __metadata("design:type", String) ], FulltextSearchComponent.prototype, "route", void 0); __decorate([ Input(), __metadata("design:type", Boolean) ], FulltextSearchComponent.prototype, "projectfilter", void 0); __decorate([ Input(), __metadata("design:type", String) ], FulltextSearchComponent.prototype, "filterbyproject", void 0); __decorate([ Input(), __metadata("design:type", Boolean) ], FulltextSearchComponent.prototype, "show", void 0); __decorate([ Output(), __metadata("design:type", Object) ], FulltextSearchComponent.prototype, "showState", void 0); __decorate([ ViewChild('fulltextSearchPanel', { static: false }), __metadata("design:type", ElementRef) ], FulltextSearchComponent.prototype, "searchPanel", void 0); __decorate([ ViewChild('fulltextSearchInput', { static: false }), __metadata("design:type", ElementRef) ], FulltextSearchComponent.prototype, "searchInput", void 0); __decorate([ ViewChild('fulltextSearchMenu', { static: false }), __metadata("design:type", TemplateRef) ], FulltextSearchComponent.prototype, "searchMenu", void 0); __decorate([ ViewChild('btnToSelectProject', { static: false }), __metadata("design:type", MatMenuTrigger) ], FulltextSearchComponent.prototype, "selectProject", void 0); FulltextSearchComponent = __decorate([ Component({ selector: 'kui-fulltext-search', template: "<!-- full-text search panel -->\n<div class=\"kui-fulltext-search-panel\" [class.active]=\"searchPanelFocus\" [class.with-project-filter]=\"projectfilter\"\n #fulltextSearchPanel cdkOverlayOrigin>\n\n <!-- DESKTOP / TABLET VERSION -->\n <div class=\"kui-project-filter\" *ngIf=\"projectfilter\">\n <button mat-button class=\"kui-project-filter-button\" [matMenuTriggerFor]=\"selectProject\"\n #btnToSelectProject=\"matMenuTrigger\" isIconButton>\n <p class=\"mat-caption placeholder\">Filter by project</p>\n <p class=\"label\">{{projectLabel}}</p>\n <mat-icon class=\"icon\" matSuffix>keyboard_arrow_down</mat-icon>\n </button>\n <mat-menu #selectProject=\"matMenu\">\n <div class=\"kui-project-filter-menu\">\n <button mat-menu-item class=\"center\"\n (click)=\"setProject();changeFocus()\">{{defaultProjectLabel}}</button>\n <mat-divider></mat-divider>\n <span *ngFor=\"let project of projects | kuiSortBy: 'shortname'\">\n <button mat-menu-item *ngIf=\"!doNotDisplay.includes(project.id)\"\n (click)=\"setProject(project);changeFocus()\" [matTooltip]=\"project.longname\"\n [matTooltipPosition]=\"'after'\">{{project.shortname}}</button>\n </span>\n </div>\n </mat-menu>\n </div>\n\n <div class=\"kui-fulltext-search\" [class.with-project-filter]=\"projectfilter\">\n <div class=\"kui-fulltext-search-field\">\n <input #fulltextSearchInput class=\"kui-fulltext-search-input\" type=\"search\" [(ngModel)]=\"searchQuery\"\n name=\"search\" minlength=\"3\" autocomplete=\"off\" [placeholder]=\"'Search'\" (keyup.esc)=\"resetSearch()\"\n (keyup.enter)=\"doSearch()\" (click)=\"setFocus()\">\n </div>\n <button class=\"kui-fulltext-search-button suffix\" (click)=\"doSearch()\" type=\"submit\">\n <mat-icon>search</mat-icon>\n </button>\n </div>\n\n <!-- PHONE VERSION *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -->\n <div class=\"kui-project-filter-mobile\" *ngIf=\"projectfilter\">\n <button mat-stroked-button class=\"kui-project-filter-button kui-project-filter-button-mobile\"\n [matMenuTriggerFor]=\"selectProject\" #btnToSelectProject=\"matMenuTrigger\" isIconButton>\n <p class=\"mat-caption placeholder\">Filter by project</p>\n <p class=\"label\">{{projectLabel}}</p>\n <mat-icon class=\"icon\" matSuffix>keyboard_arrow_down</mat-icon>\n </button>\n <mat-menu #selectProject=\"matMenu\">\n <div class=\"kui-project-filter-menu-mobile\">\n <button mat-menu-item class=\"center\"\n (click)=\"setProject();changeFocus()\">{{defaultProjectLabel}}</button>\n <mat-divider></mat-divider>\n <span *ngFor=\"let project of projects | kuiSortBy: 'shortname'\">\n <button mat-menu-item *ngIf=\"!doNotDisplay.includes(project.id)\"\n (click)=\"setProject(project);changeFocus()\" [matTooltip]=\"project.longname\"\n [matTooltipPosition]=\"'after'\">{{project.shortname}}</button>\n </span>\n </div>\n </mat-menu>\n <!-- <mat-form-field class=\"kui-project-filter-select-mobile\">\n <mat-select [(ngModel)]=\"All projects\">\n <mat-option *ngFor=\"let project of projects | kuiSortBy: 'shortname'\" [value]=\"project.shortname\">\n {{project.shortname}}\n </mat-option>\n </mat-select>\n </mat-form-field> -->\n </div>\n\n <div class=\"kui-fulltext-search-mobile\" [class.with-project-filter]=\"projectfilter\">\n <div class=\"kui-fulltext-search-field-mobile\">\n <input #fulltextSearchInput class=\"kui-fulltext-search-input-mobile\" type=\"search\" [(ngModel)]=\"searchQuery\"\n name=\"search\" minlength=\"3\" autocomplete=\"off\" [placeholder]=\"'Search'\" (keyup.esc)=\"resetSearch()\"\n (keyup.enter)=\"doSearch()\" (click)=\"setFocus()\">\n </div>\n <button mat-stroked-button class=\"kui-fulltext-search-button-mobile suffix-mobile\" (click)=\"doSearch()\"\n type=\"submit\">\n Search\n </button>\n </div>\n <!-- *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -->\n\n</div>\n\n<!-- full-text search menu - only for desktop/tablet versions -->\n<ng-template #fulltextSearchMenu>\n\n <div class=\"kui-search-menu\" [class.with-project-filter]=\"projectfilter\">\n <div class=\"kui-menu-content\">\n <mat-list class=\"kui-previous-search-list\">\n <div *ngFor=\"let item of prevSearch | kuiReverse; let i=index\">\n <mat-list-item *ngIf=\"i<10\">\n <h4 mat-line (click)=\"doPrevSearch(item)\" class=\"kui-previous-search-item\">\n <div class=\"kui-project-filter-label\" [class.not-empty]=\"item.projectIri\"\n *ngIf=\"projectfilter && !error && projects?.length > 0\">\n <span *ngIf=\"item.projectIri\">{{item.projectLabel}}</span>\n </div>\n <div class=\"kui-previous-search-query\" [class.fix-width]=\"projectfilter\">\n {{item.query}}\n </div>\n </h4>\n <button mat-icon-button (click)=\"resetPrevSearch(item)\">\n <mat-icon class=\"mat-list-close-icon\" aria-label=\"close\">close</mat-icon>\n </button>\n </mat-list-item>\n </div>\n </mat-list>\n </div>\n\n <div class=\"kui-menu-action\" *ngIf=\"prevSearch\">\n <mat-divider></mat-divider>\n <button mat-button color=\"primary\" class=\"center\" (click)=\"resetPrevSearch()\">Clear list\n </button>\n </div>\n </div>\n\n</ng-template>\n", styles: ["input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{display:none}input[type=search]{-moz-appearance:none;-webkit-appearance:none}.center{text-align:center}.kui-fulltext-search-panel{border-radius:4px;display:-webkit-box;display:flex;height:40px;position:relative;width:480px;z-index:100;background-color:#f9f9f9}.kui-fulltext-search-panel.active{box-shadow:0 1px 3px rgba(0,0,0,.5)}.kui-fulltext-search-panel.with-project-filter{width:calc(480px + 160px)}.kui-fulltext-search-panel .kui-project-filter-button{font-size:inherit;overflow:hidden;text-overflow:ellipsis;width:160px;margin:1px;border-radius:4px 0 0 4px}.kui-fulltext-search-panel .kui-fulltext-search{background-color:#f9f9f9;border-radius:4px;display:-webkit-inline-box;display:inline-flex;position:relative;z-index:10}.kui-fulltext-search-panel .kui-fulltext-search.with-project-filter{width:calc(480px + 160px);border-top-left-radius:0;border-bottom-left-radius:0}.kui-fulltext-search-panel .kui-fulltext-search .kui-fulltext-search-field{-webkit-box-flex:1;flex:1;width:calc(480px - 40px);margin:1px}.kui-fulltext-search-panel .kui-fulltext-search .kui-fulltext-search-field .kui-fulltext-search-input{border-style:none;font-size:14pt;height:38px;position:absolute;padding-left:12px;width:calc(100% - 40px)}.kui-fulltext-search-panel .kui-fulltext-search .kui-fulltext-search-field .kui-fulltext-search-input.with-project-filter{width:calc(100% - 40px - 160px)}.kui-fulltext-search-panel .kui-fulltext-search .kui-fulltext-search-field .kui-fulltext-search-input:active,.kui-fulltext-search-panel .kui-fulltext-search .kui-fulltext-search-field .kui-fulltext-search-input:focus{outline:0}.kui-fulltext-search-panel .kui-fulltext-search .kui-fulltext-search-button{background-color:#fff}.kui-fulltext-search-panel .kui-fulltext-search .suffix{margin:1px 0 1px -3px;border-radius:0 4px 4px 0}.kui-fulltext-search-panel .kui-fulltext-search .prefix{margin:1px 0 1px 3px;border-radius:4px 0 0 4px}.kui-fulltext-search-panel .kui-fulltext-search .prefix,.kui-fulltext-search-panel .kui-fulltext-search .suffix{border-style:none;color:rgba(41,41,41,.4);cursor:pointer;height:38px;outline:0;position:relative;width:39px}.kui-fulltext-search-panel .kui-fulltext-search .prefix.disabled,.kui-fulltext-search-panel .kui-fulltext-search .suffix.disabled{cursor:auto}.kui-fulltext-search-panel .kui-fulltext-search .prefix:active,.kui-fulltext-search-panel .kui-fulltext-search .suffix:active{color:#515151}.kui-search-menu{height:100%}.kui-search-menu .kui-menu-content{display:block}.kui-search-menu .kui-menu-content .mat-list{padding-bottom:8px}.kui-search-menu .kui-menu-content .mat-list .kui-previous-search-query{overflow:hidden;text-overflow:ellipsis}.kui-search-menu .kui-menu-content .mat-list .kui-previous-search-query.fix-width{width:calc(100% - 160px)}.kui-project-filter-button{height:38px!important;display:block;text-align:left}.kui-project-filter-button .placeholder{margin:0;padding:0;font-size:x-small}.kui-project-filter-button .icon,.kui-project-filter-button .label{display:inline;position:relative}.kui-project-filter-button .label{top:-12px;font-size:smaller;text-transform:capitalize}.kui-project-filter-button .icon{top:-6px;float:right}.kui-project-filter-button.kui-project-filter-button-mobile{height:100%!important}.kui-project-filter-button.kui-project-filter-button-mobile .icon,.kui-project-filter-button.kui-project-filter-button-mobile .label{top:0;font-size:inherit}@media (min-width:576px){.kui-fulltext-search-mobile,.kui-fulltext-search-mobile.with-project-filter,.kui-project-filter-mobile{display:none}}@media (max-width:576px){.kui-fulltext-search,.kui-project-filter{display:none}.kui-fulltext-search.with-project-filter{display:none!important}.kui-search-menu{display:none}.kui-search-menu.with-project-filter{display:none!important}.kui-fulltext-search-panel{height:100vh!important;background-color:rgba(220,218,218,.9);position:relative;width:100%!important;z-index:100;display:block;border-radius:0}.kui-fulltext-search-panel.with-project-filter{width:100%!important}.kui-fulltext-search-panel .kui-project-filter-mobile{height:54px;margin:0 2% 5%;padding-top:3%}.kui-fulltext-search-panel .kui-project-filter-mobile.mat-stroked-button{padding:0}.kui-fulltext-search-panel .kui-project-filter-mobile .kui-project-filter-button-mobile{width:calc(100% - 16px);margin:8px;height:100%;background-color:#b8b8b8;cursor:pointer}.kui-fulltext-search-panel .kui-project-filter-mobile .kui-project-filter-menu-mobile{width:calc(100% - 32px)!important;max-width:100%;margin:0 8px;height:100%!important}.kui-fulltext-search-panel .kui-fulltext-search-mobile{display:-webkit-box;display:flex;height:64px;margin-top:5%;margin-right:4%;margin-left:4%}.kui-fulltext-search-panel .kui-fulltext-search-mobile .kui-fulltext-search-field-mobile{width:80%;margin-right:2px}.kui-fulltext-search-panel .kui-fulltext-search-mobile .kui-fulltext-search-field-mobile .kui-fulltext-search-input-mobile{width:72%;height:64px;font-size:14pt;position:absolute;padding-left:12px;border-radius:5px;border:.6px solid #b8b8b8}.kui-fulltext-search-panel .kui-fulltext-search-mobile .kui-fulltext-search-field-mobile .kui-fulltext-search-input-mobile:active,.kui-fulltext-search-panel .kui-fulltext-search-mobile .kui-fulltext-search-field-mobile .kui-fulltext-search-input-mobile:focus{outline:0}.kui-fulltext-search-panel .kui-fulltext-search-mobile .kui-fulltext-search-button-mobile{background-color:#fff;width:20%;height:64px;padding:0;margin-left:2px;border-radius:5px;border:.8px solid #b8b8b8}.kui-fulltext-search-panel .kui-fulltext-search-mobile .suffix{margin:1px 0 1px -3px;border-radius:0 4px 4px 0}.kui-fulltext-search-panel .kui-fulltext-search-mobile .prefix,.kui-fulltext-search-panel .kui-fulltext-search-mobile .suffix{border-style:none;color:rgba(41,41,41,.4);cursor:pointer;height:100%;outline:0;position:relative;width:20%}.kui-fulltext-search-panel .kui-fulltext-search-mobile .prefix.disabled,.kui-fulltext-search-panel .kui-fulltext-search-mobile .suffix.disabled{cursor:auto}.kui-fulltext-search-panel .kui-fulltext-search-mobile .prefix:active,.kui-fulltext-search-panel .kui-fulltext-search-mobile .suffix:active{color:#515151}::ng-deep .cdk-overlay-pane .mat-menu-panel{box-shadow:none}::ng-deep .cdk-overlay-pane .mat-select-panel-wrap{margin-top:20%}::ng-deep .cdk-overlay-pane .mat-select-panel-wrap .mat-select-panel{max-height:100%!important}}"] }), __param(0, Inject(KnoraApiConnectionToken)), __metadata("design:paramtypes", [KnoraApiConnection, Overlay, Router, ViewContainerRef]) ], FulltextSearchComponent); /** * The search-panel contains the kui-fulltext-search and the kui-extended-search components. */ let SearchPanelComponent = class SearchPanelComponent { constructor(_overlay, _viewContainerRef) { this._overlay = _overlay; this._viewContainerRef = _viewContainerRef; /** * @param {string} route Route to navigate after search. This route path should contain a component for search results. */ this.route = '/search'; /** *@param {boolean} [projectfilter] If true it shows the selection of projects to filter by one of them */ this.projectfilter = false; /** * @param {boolean} [advanced] Adds the extended / advanced search to the panel */ this.advanced = false; /** * @param {boolean} [expert] Adds the expert search / gravsearch editor to the panel */ this.expert = false; } openPanelWithBackdrop(type) { this.showAdvanced = (type === 'advanced'); const config = new OverlayConfig({ hasBackdrop: true, backdropClass: 'cdk-overlay-transparent-backdrop', // backdropClass: 'cdk-overlay-dark-backdrop', positionStrategy: this.getOverlayPosition(), scrollStrategy: this._overlay.scrollStrategies.block() }); this.overlayRef = this._overlay.create(config); this.overlayRef.attach(new TemplatePortal(this.searchMenu, this._viewContainerRef)); this.overlayRef.backdropClick().subscribe(() => { this.overlayRef.detach(); }); } getOverlayPosition() { const positions = [ new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }), new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }) ]; const overlayPosition = this._overlay.position().flexibleConnectedTo(this.searchPanel).withPositions(positions).withLockedPosition(false); return overlayPosition; } closeMenu() { this.overlayRef.detach(); } }; SearchPanelComponent.ctorParameters = () => [ { type: Overlay }, { type: ViewContainerRef } ]; __decorate([ Input(), __metadata("design:type", String) ], SearchPanelComponent.prototype, "route", void 0); __decorate([ Input(), __metadata("design:type", Boolean) ], SearchPanelComponent.prototype, "projectfilter", void 0); __decorate([ Input(), __metadata("design:type", String) ], SearchPanelComponent.prototype, "filterbyproject", void 0); __decorate([ Input(), __metadata("design:type", Boolean) ], SearchPanelComponent.prototype, "advanced", void 0); __decorate([ Input(), __metadata("design:type", Boolean) ], SearchPanelComponent.prototype, "expert", void 0); __decorate([ ViewChild('fullSearchPanel', { static: false }), __metadata("design:type", ElementRef) ], SearchPanelComponent.prototype, "searchPanel", void 0); __decorate([ ViewChild('searchMenu', { static: false }), __metadata("design:type", TemplateRef) ], SearchPanelComponent.prototype, "searchMenu", void 0); SearchPanelComponent = __decorate([ Component({ selector: 'kui-search-panel', template: "<div class=\"kui-search-panel\" #fullSearchPanel cdkOverlayOrigin>\n\n <!-- DESKTOP VERSION -->\n <kui-fulltext-search class=\"kui-fulltext-search\" [route]=\"route\" [projectfilter]=\"projectfilter\"\n [filterbyproject]=\"filterbyproject\">\n </kui-fulltext-search>\n\n <!-- advanced search button: if advanced === true -->\n <button mat-button *ngIf=\"advanced && !expert\" color=\"primary\"\n (click)=\"openPanelWithBackdrop('advanced')\">advanced</button>\n\n <!-- expert search button: if expert === true -->\n <button mat-button *ngIf=\"!advanced && expert\" color=\"primary\"\n (click)=\"openPanelWithBackdrop('expert')\">expert</button>\n\n <!-- button to select advanced or expert search: if advanced AND expert === true; open menu to select -->\n <button mat-button *ngIf=\"advanced && expert\" [matMenuTriggerFor]=\"selectSearch\">\n <mat-icon>filter_list</mat-icon>\n </button>\n <mat-menu #selectSearch=\"matMenu\">\n <button mat-menu-item (click)=\"openPanelWithBackdrop('advanced')\">\n <span>Advanced search</span>\n </button>\n <button mat-menu-item (click)=\"openPanelWithBackdrop('expert')\">\n <span>Expert search</span>\n </button>\n </mat-menu>\n\n</div>\n\n<!-- full-text search menu -->\n<ng-template #searchMenu>\n <div class=\"kui-search-menu with-extended-search\" [class.with-project-filter]=\"projectfilter\">\n <div class=\"kui-menu-header\">\n <span class=\"kui-menu-title\">\n <h4 *ngIf=\"showAdvanced\">Advanced search</h4>\n <h4 *ngIf=\"!showAdvanced\">Expert search</h4>\n </span>\n <span class=\"fill-remaining-space\"></span>\n <span class=\"kui-menu-close\">\n <button mat-icon-button (click)=\"closeMenu()\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n </div>\n <div class=\"kui-menu-content\">\n <kui-extended-search *ngIf=\"showAdvanced\" [route]=\"route\" (toggleExtendedSearchForm)=\"closeMenu()\">\n </kui-extended-search>\n <kui-expert-search *ngIf=\"!showAdvanced\" [route]=\"route\" (toggleExpertSearchForm)=\"closeMenu()\">\n </kui-expert-search>\n </div>\n </div>\n</ng-template>\n", styles: [".advanced-btn{margin-left:10px}.kui-search-panel{display:-webkit-box;display:flex;position:relative;z-index:100}.extended-search-box{margin:12px}@media (max-width:576px){.kui-fulltext-search{height:100%;width:100%!important}}"] }), __metadata("design:paramtypes", [Overlay, ViewContainerRef]) ], SearchPanelComponent); // https://stackoverflow.com/questions/45661010/dynamic-nested-reactive-form-expressionchangedafterithasbeencheckederror const resolvedPromise = Promise.resolve(null); let SelectResourceClassComponent = class SelectResourceClassComponent { constructor(fb) { this.fb = fb; // event emitted to parent component once a resource class is selected by the user this.resourceClassSelectedEvent = new EventEmitter(); } // setter method for resource classes when being updated by parent component set resourceClasses(value) { this.resourceClassSelected = undefined; // reset on updates this._resourceClasses = value; } // getter method for resource classes (used in template) get resourceClasses() { return this._resourceClasses; } /** * Returns the Iri of the selected resource class. * * @returns the Iri of the selected resource class or false in case no resource class is selected. */ getResourceClassSelected() { if (this.resourceClassSelected !== undefined && this.resourceClassSelected !== null) { return this.resourceClassSelected; } else { return false; } } /** * Initalizes the FormGroup for the resource class selection. * The initial value is set to null. */ initForm() { // build a form for the resource class selection this.form = this.fb.group({ resourceClass: [null] // resource class selection is optional }); // store and emit Iri of the resource class when selected this.form.valueChanges.subscribe((data) => { this.resourceClassSelected = data.resourceClass; this.resourceClassSelectedEvent.emit(this.resourceClassSelected); }); } ngOnInit() { this.initForm(); // add form to the parent form group this.formGroup.addControl('resourceClass', this.form); } ngOnChanges() { if (this.form !== undefined) { // resource classes have been reinitialized // reset form resolvedPromise.then(() => { // remove this form from the parent form group this.formGroup.removeControl('resourceClass'); this.initForm(); // add form to the parent form group this.formGroup.addControl('resourceClass', this.form); }); } } }; SelectResourceClassComponent.ctorParameters = () => [ { type: FormBuilder, decorators: [{ type: Inject, args: [FormBuilder,] }] } ]; __decorate([ Input(), __metadata("design:type", FormGroup) ], SelectResourceClassComponent.prototype, "formGroup", void 0); __decorate([ Input(), __metadata("design:type", Array), __metadata("design:paramtypes", [Array]) ], SelectResourceClassComponent.prototype, "resourceClasses", null); __decorate([ Output(), __metadata("design:type", Object) ], SelectResourceClassComponent.prototype, "resourceClassSelectedEvent", void 0); SelectResourceClassComponent = __decorate([ Component({ selector: 'kui-select-resource-class', template: "<mat-form-field *ngIf=\"resourceClasses.length > 0\" class=\"large-field\">\n <mat-select placeholder=\"Select a Resource Class (optional)\" [formControl]=\"form.controls['resourceClass']\">\n <mat-option [value]=\"null\">no selection</mat-option>\n <!-- undo selection of a resource class -->\n <mat-option *ngFor=\"let resourceClass of resourceClasses\" [value]=\"resourceClass.id\">{{ resourceClass.label }}\n </mat-option>\n </mat-select>\n</mat-form-field>\n", styles: [""] }), __param(0, Inject(FormBuilder)), __metadata("design:paramtypes", [FormBuilder]) ], SelectResourceClassComponent); /** * The extended search allows you to filter by project, by source type (resource class), or by the metadata (properties) of source types. Each filter can be standalone or combined. The metadata field can be precisely filtered with criteria such as "contains", "like", "equals to", "exists" or in case of a date value with "before" or "after". In addition, for a metadata field that is connected to another source type, it's possible to filter by this second source type. If you are looking for the source type "Photograph" with the metadata field "Photographer", which is connected to source type "Person", you can search for photograph(s) taken by person(s) who is born before February 1970. The result of this request will be an intersection of the two source types. */ let ExtendedSearchComponent = class ExtendedSearchComponent { constructor(fb, knoraApiConnection, _route, _router, _cacheService, _gravSearchService) { this.fb = fb; this.knoraApiConnection = knoraApiConnection; this._route = _route; this._router = _router; this._cacheService = _cacheService; this._gravSearchService = _gravSearchService; /** * @param {boolean} toggleExtendedSearchForm Trigger toggle for extended search form. */ this.toggleExtendedSearchForm = new EventEmitter(); /** * @param {string} gravsearch Send the gravsearch query back. */ this.gravsearch = new EventEmitter(); // all available ontologies this.ontologies = []; // properties specified by the user this.activeProperties = []; // resource classes for the selected ontology this.resourceClasses = []; this.result = [new ReadResource()]; // form validation status this.formValid = false; } ngOnInit() { // parent form is empty, it gets passed to the child components this.form = this.fb.group({}); // if form status changes, re-run validation this.form.statusChanges.subscribe((data) => { this.formValid = this.validateForm(); // console.log(this.form); }); // initialize ontologies to be used for the ontologies selection in the search form this.initializeOntologies(); } /** * @ignore * Add a property to the search form. * @returns void */ addProperty() { this.activeProperties.push(true); } /** * @ignore * Remove the last property from the search form. * @returns void */ removeProperty() { this.activeProperties.splice(-1, 1); } /** * @ignore * Gets all available ontologies for the search form. * @returns void */ initializeOntologies() { this.knoraApiConnection.v2.onto.getOntologiesMetadata().subscribe((response) => { this.ontologies = response.ontologies; }, (error) => { console.error(error); }); } /** * @ignore * Once an ontology has been selected, gets its classes and properties. * The classes and properties will be made available to the user for selection. * * @param ontologyIri Iri of the ontology chosen by the user. * @returns void */ getResourceClassesAndPropertiesForOntology(ontologyIri) { // reset active resource class definition this.activeResourceClass = undefined; // reset specified properties this.activeProperties = []; this.activeOntology = ontologyIri; this._cacheService.getEntityDefinitionsForOntologies([ontologyIri]).subscribe((ontoInfo) => { this.resourceClasses = ontoInfo.getResourceClassesAsArray(true); this.properties = ontoInfo.getProperties(); }); } /** * @ignore * Once a resource class has been selected, gets its properties. * The properties will be made available to the user for selection. * * @param resourceClassIri * @returns void */ getPropertiesForResourceClass(resourceClassIri) { // reset specified properties this.activeProperties = []; // if the client undoes the selection of a resource class, use the active ontology as a fallback if (resourceClassIri === null) { this.getResourceClassesAndPropertiesForOntology(this.activeOntology); } else { this._cacheService.getResourceClassDefinitions([resourceClassIri]).subscribe((ontoInfo) => { this.properties = ontoInfo.getProperties(); this.activeResourceClass = ontoInfo.getResourceClasses()[resourceClassIri]; }); } } /** * @ignore * Validates form and returns its status (boolean). */ validateForm() { // check that either a resource class is selected or at least one property is specified return this.form.valid && (this.propertyComponents.length > 0 || (this.resourceClassComponent !== undefined && this.resourceClassComponent.getResourceClassSelected() !== false)); } /** * @ignore * Resets the form (selected resource class and specified properties) preserving the active ontology. */ resetForm() { if (this.activeOntology !== undefined) { this.getResourceClassesAndPropertiesForOntology(this.activeOntology); } } /** * @ignore * Creates a GravSearch query with the given form values and calls the extended search route. */ submit() { if (!this.formValid) return; // check that from is valid const resClassOption = this.resourceClassComponent.getResourceClassSelected(); let resClass; if (resClassOption !== false) { resClass = resClassOption; } const properties = this.propertyComponents.map((propComp) => { return propComp.getPropertySelectedWithValue(); }); const gravsearch = this._gravSearchService.createGravsearchQuery(properties, resClass, 0); if (this.route) { this._router.navigate([this.route + '/extended/', gravsearch], { relativeTo: this._route }); } else { this.gravsearch.emit(gravsearch); } // toggle extended search form this.toggleExtendedSearchForm.emit(true); } }; ExtendedSearchComponent.ctorParameters = () => [ { type: FormBuilder, decorators: [{ type: Inject, args: [FormBuilder,] }] }, { type: KnoraApiConnection, decorators: [{ type: Inject, args: [KnoraApiConnectionToken,] }] }, { type: ActivatedRoute }, { type: Router }, { type: OntologyCacheService }, { type: GravsearchGenerationService } ]; __decorate([ Input(), __metadata("design:type", Object) ], ExtendedSearchComponent.prototype, "route", void 0); __decorate([ Output(), __metadata("design:type", Object) ], ExtendedSearchComponent.prototype, "toggleExtendedSearchForm", void 0); __decorate([ Output(), __metadata("design:type", Object) ], ExtendedSearchComponent.prototype, "gravsearch", void 0); __decorate([ ViewChild('resourceClass', { static: false }), __metadata("design:type", SelectResourceClassComponent) ], ExtendedSearchComponent.prototype, "resourceClassComponent", void 0); __decorate([ ViewChildren('property'), __metadata("design:type", QueryList) ], ExtendedSearchComponent.prototype, "propertyComponents", void 0); ExtendedSearchComponent = __decorate([ Component({ selector: 'kui-extended-search', template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"kui-form-content\">\n\n <div>\n <kui-select-ontology *ngIf=\"ontologies.length > 0\" [formGroup]=\"form\" [ontologies]=\"ontologies\"\n (ontologySelected)=\"getResourceClassesAndPropertiesForOntology($event)\"></kui-select-ontology>\n </div>\n\n <div class=\"select-resource-class\" *ngIf=\"resourceClasses?.length > 0\">\n <kui-select-resource-class #resourceClass [formGroup]=\"form\" [resourceClasses]=\"resourceClasses\"\n (resourceClassSelectedEvent)=\"getPropertiesForResourceClass($event)\">\n </kui-select-resource-class>\n </div>\n\n <div class=\"select-property\" *ngIf=\"properties !== undefined\">\n <div *ngFor=\"let prop of activeProperties; let i = index\">\n\n <kui-select-property #property [activeResourceClass]=\"activeResourceClass\" [formGroup]=\"form\" [index]=\"i\"\n [properties]=\"properties\"></kui-select-property>\n\n </div>\n </div>\n\n\n <div class=\"select-property buttons\">\n <button mat-mini-fab class=\"property-button add-property-button\" color=\"primary\" type=\"button\"\n (click)=\"addProperty()\" [disabled]=\"activeOntology === undefined || activeProperties.length >= 4\">\n <mat-icon aria-label=\"add a property\">add</mat-icon>\n </button>\n\n <button mat-mini-fab class=\"property-button remove-property-button\" color=\"primary\" type=\"button\"\n (click)=\"removeProperty()\" [disabled]=\"activeProperties.length == 0\">\n <mat-icon aria-label=\"remove property\">remove</mat-icon>\n </button>\n </div>\n\n <!-- <div>\n <button mat-icon-button type=\"button\" (click)=\"resetForm()\" [disabled]=\"this.activeOntology === undefined\">\n <mat-icon aria-label=\"reset query form\">clear</mat-icon>\n </button>\n\n <button mat-icon-button type=\"submit\" [disabled]=\"!formValid\">\n <mat-icon aria-label=\"submit query\">send</mat-icon>\n </button>\n </div> -->\n\n <div class=\"kui-form-action\">\n <button class=\"reset\" mat-button type=\"button\" (click)=\"resetForm()\" [disabled]=\"this.activeOntology === undefined\">\n Reset\n </button>\n <span class=\"fill-remaining-space\"></span>\n <button class=\"extended-search-button\" mat-raised-button color=\"primary\" type=\"submit\" [disabled]=\"!formValid\">\n Search\n </button>\n </div>\n\n</form>\n", styles: [".select-resource-class{margin-left:8px}.select-property{margin-left:16px}.select-property .property-button{margin:0 12px 64px 0}"] }), __param(0, Inject(FormBuilder)), __param(1, Inject(KnoraApiConnectionToken)), __metadata("design:paramtypes", [FormBuilder, KnoraApiConnection, ActivatedRoute, Router, OntologyCacheService, GravsearchGenerationService]) ], ExtendedSearchComponent); let ExpertSearchComponent = class ExpertSearchComponent { constructor(knoraApiConfig, fb, _route, _router, _searchParamsService) { this.knoraApiConfig = knoraApiConfig; this.fb = fb; this._route = _route; this._router = _router; this._searchParamsService = _searchParamsService; /** * @param gravsearch Send the gravsearch query back. */ this.gravsearch = new EventEmitter(); /** * @param {boolean} toggleExtendedSearchForm Trigger toggle for extended search form. */ this.toggleExpertSearchForm = new EventEmitter(); } ngOnInit() { this.initForm(); } /** * @ignore * Initiate the form with predefined Gravsearch query as example. */ initForm() { this.expertSearchForm = this.fb.group({ gravquery: [ ` PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#> PREFIX incunabula: <${this.knoraApiConfig.apiUrl}/ontology/0803/incunabula/simple/v2#> CONSTRUCT { ?book knora-api:isMainResource true . ?book incunabula:title ?title . } WHERE { ?book a incunabula:book . ?book incunabula:title ?title . } `, Validators.required ] }); } /** * @ignore * Send the gravsearch query to the result view, either through the route or by emitting the gravsearch as an output. */ submitQuery() { const gravsearch = this.generateGravsearch(0); if (this.route) { this._router.navigate([this.route + '/extended/', gravsearch], { relativeTo: this._route }); } else { this.gravsearch.emit(gravsearch); } // toggle expert search form this.toggleExpertSearchForm.emit(true); } /** * @ignore * Generate the whole gravsearch query matching the query given by the form. */ generateGravsearch(offset = 0) { const queryTemplate = this.expertSearchForm.controls['gravquery'].value; // offset component of the Gravsearch query const offsetTemplate = ` OFFSET ${offset} `; // function that generates the same Gravsearch query with the given offset const generateGravsearchWithCustomOffset = (localOffset) => { const offsetCustomTemplate = ` OFFSET ${localOffset} `; return queryTemplate + offsetCustomTemplate; }; if (offset === 0) { // store the function so another Gravsearch query can be created with an increased offset this._searchParamsService.changeSearchParamsMsg(new ExtendedSearchParams(generateGravsearchWithCustomOffset)); } return queryTemplate + offsetTemplate; } /** * @ignore * Reset the form to the initial state. */ resetForm() { this.initForm(); } }; ExpertSearchComponent.ctorParameters = () => [ { type: KnoraApiConfig, decorators: [{ type: Inject, args: [KnoraApiConfigToken,] }] }, { type: FormBuilder }, { type: ActivatedRoute }, { type: Router }, { type: SearchParamsService } ]; __decorate([ Input(), __metadata("design:type", Object) ], ExpertSearchComponent.prototype, "route", void 0); __decorate([ Output(), __metadata("design:type", Object) ], ExpertSearchComponent.prototype, "gravsearch", void 0); __decorate([ Output(), __metadata("design:type", Object) ], ExpertSearchComponent.prototype, "toggleExpertSearchForm", void 0); ExpertSearchComponent = __decorate([ Component({ selector: 'kui-expert-search', template: "<div class=\"expert-search-container\">\n <!-- The integration in container like the accordeon expansion panel should be handled on app side\n <mat-expansion-panel [expanded]=\"true\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n Expert search\n </mat-panel-title>\n <mat-panel-description> </mat-panel-description>\n </mat-expansion-panel-header>\n -->\n <form [formGroup]=\"expertSearchForm\" class=\"expert-search-form kui-form-content\">\n <mat-form-field class=\"textarea-field large-field\">\n <textarea matInput formControlName=\"gravquery\" matTextareaAutosize matAutosizeMinRows=\"12\" matAutosizeMaxRows=\"24\"\n placeholder=\"Write your gravsearch query\"></textarea>\n </mat-form-field>\n\n <div class=\"kui-form-a