From: Bill Erickson Date: Mon, 2 Jul 2018 05:16:30 +0000 (-0400) Subject: LP#1775466 Typeahead is now combobox; entries X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=75d4dd232559e5c8b5b86619b95e66c5378bec9f;p=working%2FEvergreen.git LP#1775466 Typeahead is now combobox; entries Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts new file mode 100644 index 0000000000..1238d8a5fe --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts @@ -0,0 +1,25 @@ +import {Component, Input, Host, OnInit} from '@angular/core'; +import {ComboboxComponent} from './combobox.component'; + +@Component({ + selector: 'eg-combobox-entry', + template: '' +}) +export class ComboboxEntryComponent implements OnInit{ + + @Input() entryId: any; + @Input() entryLabel: string; + @Input() selected: boolean; + + constructor(@Host() private combobox: ComboboxComponent) {} + + ngOnInit() { + if (this.selected) { + this.combobox.startId = this.entryId; + } + this.combobox.addEntries( + [{id: this.entryId, label: this.entryLabel}]); + } +} + + diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html new file mode 100644 index 0000000000..879a02340c --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html @@ -0,0 +1,23 @@ + + + +{{r.label}} + + +
+ +
+ keyboard_arrow_up + keyboard_arrow_down +
+
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts new file mode 100644 index 0000000000..d929d62c18 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts @@ -0,0 +1,174 @@ +import {Component, OnInit, Input, Output, ViewChild, EventEmitter, ElementRef} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {map} from 'rxjs/operators/map'; +import {mapTo} from 'rxjs/operators/mapTo'; +import {debounceTime} from 'rxjs/operators/debounceTime'; +import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged'; +import {merge} from 'rxjs/operators/merge'; +import {filter} from 'rxjs/operators/filter'; +import {Subject} from 'rxjs/Subject'; +import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap'; +import {StoreService} from '@eg/core/store.service'; + +export interface ComboboxEntry { + id: any; + label: string; + freetext?: boolean; +} + +@Component({ + selector: 'eg-combobox', + templateUrl: './combobox.component.html', + styles: [` + .icons {margin-left:-18px} + .material-icons {font-size: 16px;font-weight:bold} + `] +}) +export class ComboboxComponent implements OnInit { + + selected: ComboboxEntry; + click$: Subject; + entrylist: ComboboxEntry[]; + freeTextId: number; + + @ViewChild('instance') instance: NgbTypeahead; + + // Placeholder text for selector input + @Input() placeholder = ''; + + @Input() persistKey: string; // TODO + + // Display all entries when the user clicks in the text filter + // box regardless of any text that already exists there. + @Input() clickShowsAll = true; + + @Input() allowFreeText = false; + + // If true select the first item in the list + @Input() selectFirst: boolean; + + // Entry ID of the default entry to select (optional) + // onChange() is NOT fired when applying the default value + @Input() startId: any; + + @Input() set entries(el: ComboboxEntry[]) { + this.addEntries(el); + } + + // Emitted when the value is changed via UI. + @Output() onChange: EventEmitter; + + // Useful for massaging the match string prior to comparison + // and display. Default version trims leading/trailing spaces. + formatDisplayString: (ComboboxEntry) => string; + + constructor( + private elm: ElementRef, + private store: StoreService, + ) { + this.entrylist = []; + this.click$ = new Subject(); + this.onChange = new EventEmitter(); + this.freeTextId = -1; + + this.formatDisplayString = (result: ComboboxEntry) => { + return result.label.trim(); + }; + } + + ngOnInit() { + } + + openMe($event) { + // Give the input a chance to focus then fire the click + // handler to force open the typeahead + this.elm.nativeElement.getElementsByTagName('input')[0].focus(); + setTimeout(() => this.click$.next('')); + } + + // Called by combobox-entry.component + addEntries(entries: ComboboxEntry[]) { + entries.forEach(entry => { + + if (this.entrylist.filter(e => e.id === entry.id).length) { + // avoid dupes + return; + } + + this.entrylist.push(entry); + + if (this.startId === entry.id) { + this.selected = entry; + } else if (this.selectFirst && this.entrylist.length === 1) { + this.selected = entry; + } + }); + } + + onBlur() { + + if (typeof this.selected === 'string' && this.selected !== '') { + // Free text entered which does not match a known entry + + if (this.allowFreeText) { + // translate it into a dummy ComboboxEntry + // and manually fire the onchange handler. + this.selected = { + id: this.freeTextId--, + label: this.selected, + freetext: true + } + this.selectorChanged( + {item: this.selected, preventDefault: () => true}); + } else { + // If free text is now allowed, clear the value when + // the user navigates away to avoid confusion. + this.selected = null; + } + } + } + + // Fired by the typeahead to inform us of a change. + // This only fires when an item in the list is selected, not when + // the value is cleared or free-text is used. + selectorChanged(selEvent: NgbTypeaheadSelectItemEvent) { + console.log('selector changed'); + this.onChange.emit(selEvent.item.id); + } + + filter = (text$: Observable): Observable => { + return text$.pipe( + debounceTime(200), + distinctUntilChanged(), + + merge( + // Inject a specifier indicating the source of the + // action is a user click instead of a text entry. + this.click$ + .pipe(filter(() => !this.instance.isPopupOpen())) + .pipe(map(nothing => { + if (this.clickShowsAll) { + return '_CLICK_'; + } else { + return nothing; + } + })) + ), + + map(term => { + if (term === '' || term === '_CLICK_') { + // Click events display all visible entrylist + return this.entrylist; + } + + // Filter entrylist whose labels substring-match the + // text entered. + return this.entrylist.filter(entry => + entry.label.toLowerCase().indexOf(term.toLowerCase()) > -1 + ); + }) + ); + } +} + + diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts index 99ea3987b8..83c616da3f 100644 --- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts +++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts @@ -1,4 +1,4 @@ -/** TODO PORT ME TO */ +/** TODO PORT ME TO */ import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {map} from 'rxjs/operators/map'; diff --git a/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts b/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts deleted file mode 100644 index 1c5afac2c7..0000000000 --- a/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts +++ /dev/null @@ -1,156 +0,0 @@ -import {Component, OnInit, Input, Output, ViewChild, EventEmitter, ElementRef} from '@angular/core'; -import {Observable} from 'rxjs/Observable'; -import {map} from 'rxjs/operators/map'; -import {mapTo} from 'rxjs/operators/mapTo'; -import {debounceTime} from 'rxjs/operators/debounceTime'; -import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged'; -import {merge} from 'rxjs/operators/merge'; -import {filter} from 'rxjs/operators/filter'; -import {Subject} from 'rxjs/Subject'; -import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap'; -import {StoreService} from '@eg/core/store.service'; - -export interface TypeaheadEntry { - id: any; - label: string; - freetext?: boolean; -} - -@Component({ - selector: 'eg-typeahead', - templateUrl: './typeahead.component.html', - styles: [` - .icons {margin-left:-18px} - .material-icons {font-size: 16px;font-weight:bold} - `] -}) -export class TypeaheadComponent implements OnInit { - - selected: TypeaheadEntry; - click$: Subject; - entrylist: TypeaheadEntry[]; - freeTextId: number; - - @ViewChild('instance') instance: NgbTypeahead; - - // Placeholder text for selector input - @Input() placeholder = ''; - - @Input() persistKey: string; // TODO - - // Display all entries when the user clicks in the text filter - // box regardless of any text that already exists there. - @Input() clickShowsAll = true; - - @Input() allowFreeText = false; - - // Entry ID of the default entry to select (optional) - // onChange() is NOT fired when applying the default value - @Input() startId: any; - - @Input() set entries(el: TypeaheadEntry[]) { - this.entrylist = el; - } - - // Emitted when the value is changed via UI. - @Output() onChange: EventEmitter; - - // Useful for massaging the match string prior to comparison - // and display. Default version trims leading/trailing spaces. - formatDisplayString: (TypeaheadEntry) => string; - - constructor( - private elm: ElementRef, - private store: StoreService, - ) { - this.entrylist = []; - this.click$ = new Subject(); - this.onChange = new EventEmitter(); - this.freeTextId = -1; - - this.formatDisplayString = (result: TypeaheadEntry) => { - return result.label.trim(); - }; - } - - ngOnInit() { - if (this.startId) { - this.selected = this.entrylist.filter( - e => e.id === this.startId)[0]; - } - } - - openMe($event) { - // Give the input a chance to focus then fire the click - // handler to force open the typeahead - this.elm.nativeElement.getElementsByTagName('input')[0].focus(); - setTimeout(() => this.click$.next('')); - } - - onBlur() { - - if (typeof this.selected === 'string' && this.selected !== '') { - // Free text entered which does not match a known entry - - if (this.allowFreeText) { - // translate it into a dummy TypeaheadEntry - // and manually fire the onchange handler. - this.selected = { - id: this.freeTextId--, - label: this.selected, - freetext: true - } - this.selectorChanged( - {item: this.selected, preventDefault: () => true}); - } else { - // If free text is now allowed, clear the value when - // the user navigates away to avoid confusion. - this.selected = null; - } - } - } - - // Fired by the typeahead to inform us of a change. - // This only fires when an item in the list is selected, not when - // the value is cleared or free-text is used. - selectorChanged(selEvent: NgbTypeaheadSelectItemEvent) { - console.log('selector changed'); - this.onChange.emit(selEvent.item.id); - } - - filter = (text$: Observable): Observable => { - return text$.pipe( - debounceTime(200), - distinctUntilChanged(), - - merge( - // Inject a specifier indicating the source of the - // action is a user click instead of a text entry. - this.click$ - .pipe(filter(() => !this.instance.isPopupOpen())) - .pipe(map(nothing => { - if (this.clickShowsAll) { - return '_CLICK_'; - } else { - return nothing; - } - })) - ), - - map(term => { - if (term === '' || term === '_CLICK_') { - // Click events display all visible entrylist - return this.entrylist; - } - - // Filter entrylist whose labels substring-match the - // text entered. - return this.entrylist.filter(entry => - entry.label.toLowerCase().indexOf(term.toLowerCase()) > -1 - ); - }) - ); - } -} - - diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index a22b4e0a4e..986a19a56b 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -1,7 +1,8 @@ import {NgModule, ModuleWithProviders} from '@angular/core'; import {EgCommonModule} from '@eg/common.module'; import {StaffBannerComponent} from './share/staff-banner.component'; -import {TypeaheadComponent} from '@eg/share/typeahead/typeahead.component'; +import {ComboboxComponent} from '@eg/share/combobox/combobox.component'; +import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component'; import {OrgSelectComponent} from '@eg/share/org-select/org-select.component'; import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive'; import {AccessKeyService} from '@eg/share/accesskey/accesskey.service'; @@ -21,7 +22,8 @@ import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; @NgModule({ declarations: [ StaffBannerComponent, - TypeaheadComponent, + ComboboxComponent, + ComboboxEntryComponent, OrgSelectComponent, AccessKeyDirective, AccessKeyInfoComponent, @@ -37,7 +39,8 @@ import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; exports: [ EgCommonModule, StaffBannerComponent, - TypeaheadComponent, + ComboboxComponent, + ComboboxEntryComponent, OrgSelectComponent, AccessKeyDirective, AccessKeyInfoComponent, diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index e560abc897..8f95b19d90 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -39,7 +39,7 @@
- Typeahead: + Typeahead: