LP#1775466 Typeahead is now combobox; entries
authorBill Erickson <berickxx@gmail.com>
Mon, 2 Jul 2018 05:16:30 +0000 (01:16 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 5 Sep 2018 14:05:23 +0000 (10:05 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts [deleted file]
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html

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 (file)
index 0000000..1238d8a
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, Input, Host, OnInit} from '@angular/core';
+import {ComboboxComponent} from './combobox.component';
+
+@Component({
+  selector: 'eg-combobox-entry',
+  template: '<ng-template></ng-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 (file)
index 0000000..879a023
--- /dev/null
@@ -0,0 +1,23 @@
+
+<!-- todo disabled -->
+<ng-template #displayTemplate let-r="result">
+{{r.label}}
+</ng-template>
+
+<div class="d-flex">
+  <input type="text" 
+    class="form-control"
+    [placeholder]="placeholder"
+    [(ngModel)]="selected" 
+    [ngbTypeahead]="filter"
+    [resultTemplate]="displayTemplate"
+    [inputFormatter]="formatDisplayString"
+    (click)="click$.next($event.target.value)"
+    (blur)="onBlur()"
+    (selectItem)="selectorChanged($event)"
+    #instance="ngbTypeahead"/>
+  <div class="d-flex flex-column icons" (click)="openMe($event)">
+    <span class="material-icons">keyboard_arrow_up</span>
+    <span class="material-icons">keyboard_arrow_down</span>
+  </div>
+</div>
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 (file)
index 0000000..d929d62
--- /dev/null
@@ -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<string>;
+    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<ComboboxEntry>;
+
+    // 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<string>();
+        this.onChange = new EventEmitter<ComboboxEntry>();
+        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<string>): Observable<ComboboxEntry[]> => {
+        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
+                );
+            })
+        );
+    }
+}
+
+
index 99ea398..83c616d 100644 (file)
@@ -1,4 +1,4 @@
-/** TODO PORT ME TO <eg-typeahead> */
+/** TODO PORT ME TO <eg-combobox> */
 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 (file)
index 1c5afac..0000000
+++ /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<string>;
-    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<TypeaheadEntry>;
-
-    // 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<string>();
-        this.onChange = new EventEmitter<TypeaheadEntry>();
-        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<string>): Observable<TypeaheadEntry[]> => {
-        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
-                );
-            })
-        );
-    }
-}
-
-
index a22b4e0..986a19a 100644 (file)
@@ -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,
index e560abc..8f95b19 100644 (file)
@@ -39,7 +39,7 @@
    <button class="btn btn-info" (click)="testToast()">Test Toast Message</button>
   </div>
   <div class="col-lg-4">
-    Typeahead: <eg-typeahead [entries]="taEntries" [clickShowsAll]="true"></eg-typeahead>
+    Typeahead: <eg-combobox [entries]="taEntries" [clickShowsAll]="true"></eg-combobox>
   </div>
 </div>
 <!-- /Progress Dialog Experiments ----------------------------- -->