diff --git a/demo/pages/elements/dropdown/DropdownDemo.ts b/demo/pages/elements/dropdown/DropdownDemo.ts index 97e290ce2..abd2df823 100644 --- a/demo/pages/elements/dropdown/DropdownDemo.ts +++ b/demo/pages/elements/dropdown/DropdownDemo.ts @@ -58,13 +58,38 @@ export class DropdownDemoComponent { public CustomClassTpl: string = CustomClassTpl; public CrazyLargeTpl: string = CrazyLargeTpl; - public items: string[] = []; - - constructor() { - for (let i = 0; i < 50; i++) { - this.items.push('ITEM!'); - } - } + public MOCK_WORDS: string[] = [ + 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', + 'adipiscing', 'elit', 'curabitur', 'vel', 'hendrerit', 'libero', + 'eleifend', 'blandit', 'nunc', 'ornare', 'odio', 'ut', + 'orci', 'gravida', 'imperdiet', 'nullam', 'purus', 'lacinia', + 'a', 'pretium', 'quis', 'congue', 'praesent', 'sagittis', + 'laoreet', 'auctor', 'mauris', 'non', 'velit', 'eros', + 'dictum', 'proin', 'accumsan', 'sapien', 'nec', 'massa', + 'volutpat', 'venenatis', 'sed', 'eu', 'molestie', 'lacus', + 'quisque', 'porttitor', 'ligula', 'dui', 'mollis', 'tempus', + 'at', 'magna', 'vestibulum', 'turpis', 'ac', 'diam', + 'tincidunt', 'id', 'condimentum', 'enim', 'sodales', 'in', + 'hac', 'habitasse', 'platea', 'dictumst', 'aenean', 'neque', + 'fusce', 'augue', 'leo', 'eget', 'semper', 'mattis', + 'tortor', 'scelerisque', 'nulla', 'interdum', 'tellus', 'malesuada', + 'rhoncus', 'porta', 'sem', 'aliquet', 'et', 'nam', + 'suspendisse', 'potenti', 'vivamus', 'luctus', 'fringilla', 'erat', + 'donec', 'justo', 'vehicula', 'ultricies', 'varius', 'ante', + 'primis', 'faucibus', 'ultrices', 'posuere', 'cubilia', 'curae', + 'etiam', 'cursus', 'aliquam', 'quam', 'dapibus', 'nisl', + 'feugiat', 'egestas', 'class', 'aptent', 'taciti', 'sociosqu', + 'ad', 'litora', 'torquent', 'per', 'conubia', 'nostra', + 'inceptos', 'himenaeos', 'phasellus', 'nibh', 'pulvinar', 'vitae', + 'urna', 'iaculis', 'lobortis', 'nisi', 'viverra', 'arcu', + 'morbi', 'pellentesque', 'metus', 'commodo', 'ut', 'facilisis', + 'felis', 'tristique', 'ullamcorper', 'placerat', 'aenean', 'convallis', + 'sollicitudin', 'integer', 'rutrum', 'duis', 'est', 'etiam', + 'bibendum', 'donec', 'pharetra', 'vulputate', 'maecenas', 'mi', + 'fermentum', 'consequat', 'suscipit', 'aliquam', 'habitant', 'senectus', + 'netus', 'fames', 'quisque', 'euismod', 'curabitur', 'lectus', + 'elementum', 'tempor', 'risus', 'cras' + ]; public clickMe(data: string): void { console.log('CLICKED!', data); // tslint:disable-line diff --git a/src/elements/dropdown/Dropdown.scss b/src/elements/dropdown/Dropdown.scss index e12bf7dba..34d17247d 100644 --- a/src/elements/dropdown/Dropdown.scss +++ b/src/elements/dropdown/Dropdown.scss @@ -81,8 +81,8 @@ novo-dropdown-container { color: darken($light, 55%); } &.active { + background: lighten($light, 10%); color: darken($light, 55%); - font-weight: 500; } &.disabled { color: $light; diff --git a/src/elements/dropdown/Dropdown.ts b/src/elements/dropdown/Dropdown.ts index 72b2beb90..1bdce65d5 100644 --- a/src/elements/dropdown/Dropdown.ts +++ b/src/elements/dropdown/Dropdown.ts @@ -1,5 +1,5 @@ // NG2 -import { Component, ElementRef, EventEmitter, OnInit, OnDestroy, Input, Output, ViewChild, DoCheck, Renderer, HostListener } from '@angular/core'; +import { Component, ElementRef, EventEmitter, OnInit, AfterContentInit, OnDestroy, Input, Output, ViewChild, DoCheck, Renderer, HostListener, ContentChildren, QueryList } from '@angular/core'; // APP import { OutsideClick } from '../../utils/outside-click/OutsideClick'; import { KeyCodes } from '../../utils/key-codes/KeyCodes'; @@ -102,6 +102,11 @@ export class NovoDropdownElement extends OutsideClick implements OnInit, OnDestr clickHandler: any; closeHandler: any; parentScrollElement: Element; + private _items: QueryList; + private _textItems: string[]; + private activeIndex: number = -1; + private filterTerm: string = ''; + private filterTermTimeout: any; constructor(element: ElementRef) { super(element); @@ -119,7 +124,15 @@ export class NovoDropdownElement extends OutsideClick implements OnInit, OnDestr }); } - ngOnInit() { + public set items(items: QueryList) { + this._items = items; + // Get the innertext of all the items to allow for searching + this._textItems = items.map((item: NovoItemElement) => { + return item.element.nativeElement.innerText; + }); + } + + public ngOnInit(): void { // Add a click handler to the button to toggle the menu let button = this.element.nativeElement.querySelector('button'); button.addEventListener('click', this.clickHandler); @@ -128,7 +141,7 @@ export class NovoDropdownElement extends OutsideClick implements OnInit, OnDestr } } - ngOnDestroy() { + public ngOnDestroy(): void { // Remove listener let button = this.element.nativeElement.querySelector('button'); if (button) { @@ -171,36 +184,108 @@ export class NovoDropdownElement extends OutsideClick implements OnInit, OnDestr @HostListener('keydown', ['$event']) public onKeyDown(event: KeyboardEvent): void { - // Close with ESC/Enter - if (this.active && (event.keyCode === KeyCodes.ESC || event.keyCode === KeyCodes.ENTER)) { + Helpers.swallowEvent(event); + + if (this.active && event.keyCode === KeyCodes.ESC) { + // active & esc hit -- close this.toggleActive(); + } else if (event.keyCode === KeyCodes.ENTER) { + // enter -- perform the "click" + this._items.toArray()[this.activeIndex].onClick(); + } else if (event.keyCode === KeyCodes.DOWN) { + // down - navigate through the list ignoring disabled ones + if (this.activeIndex !== -1) { + this._items.toArray()[this.activeIndex].active = false; + } + this.activeIndex++; + if (this.activeIndex === this._items.length) { + this.activeIndex = 0; + } + while (this._items.toArray()[this.activeIndex].disabled) { + this.activeIndex++; + if (this.activeIndex === this._items.length) { + this.activeIndex = 0; + } + } + this._items.toArray()[this.activeIndex].active = true; + this.scrollToActive(); + } else if (event.keyCode === KeyCodes.UP) { + // up -- navigate through the list ignoring disabled ones + if (this.activeIndex !== -1) { + this._items.toArray()[this.activeIndex].active = false; + } + this.activeIndex--; + if (this.activeIndex < 0) { + this.activeIndex = this._items.length - 1; + } + while (this._items.toArray()[this.activeIndex].disabled) { + this.activeIndex--; + if (this.activeIndex < 0) { + this.activeIndex = this._items.length - 1; + } + } + this._items.toArray()[this.activeIndex].active = true; + this.scrollToActive(); + } else if ((event.keyCode >= 65 && event.keyCode <= 90) || (event.keyCode >= 96 && event.keyCode <= 105) || (event.keyCode >= 48 && event.keyCode <= 57) || event.keyCode === KeyCodes.SPACE) { + // A-Z, 0-9, space -- filter the list and scroll to active filter + // filter has hard reset after 2s + clearTimeout(this.filterTermTimeout); + this.filterTermTimeout = setTimeout(() => { this.filterTerm = ''; }, 2000); + let char = String.fromCharCode(event.keyCode); + this.filterTerm = this.filterTerm.concat(char); + let index = this._textItems.findIndex((value: string) => { + return value.toLocaleLowerCase().indexOf(this.filterTerm.toLowerCase()) !== -1; + }); + if (index !== -1) { + if (this.activeIndex !== -1) { + this._items.toArray()[this.activeIndex].active = false; + } + this.activeIndex = index; + this._items.toArray()[this.activeIndex].active = true; + this.scrollToActive(); + } + } else if ([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(event.keyCode)) { + // backspace, delete -- remove partial filters + clearTimeout(this.filterTermTimeout); + this.filterTermTimeout = setTimeout(() => { this.filterTerm = ''; }, 2000); + this.filterTerm = this.filterTerm.slice(0, -1); } } -} -@Component({ - selector: 'list', - template: '' -}) -export class NovoListElement { + private scrollToActive(): void { + let container = this.element.nativeElement.querySelector('novo-dropdown-container'); + let item = this._items.toArray()[this.activeIndex]; + if (container && item) { + container.scrollTop = item.element.nativeElement.offsetTop; + } else { + // Append to body + container = document.querySelector('body > novo-dropdown-container'); + if (container && item) { + container.scrollTop = item.element.nativeElement.offsetTop; + } + } + } } @Component({ selector: 'item', template: '', host: { - '[class.disabled]': 'disabled' + '[class.disabled]': 'disabled', + '[class.active]': 'active' } }) export class NovoItemElement { - @Input() disabled: boolean; - @Input() keepOpen: boolean = false; - @Output() action: EventEmitter = new EventEmitter(); + @Input() public disabled: boolean; + @Input() public keepOpen: boolean = false; + @Output() public action: EventEmitter = new EventEmitter(); - constructor(private dropdown: NovoDropdownElement) { } + public active: boolean = false; + + constructor(private dropdown: NovoDropdownElement, public element: ElementRef) { } - @HostListener('click', ['$event']) - public onClick(event: MouseEvent): void { + @HostListener('click', []) + public onClick(): void { // Poor man's disable if (!this.disabled) { // Close if keepOpen is false @@ -213,6 +298,20 @@ export class NovoItemElement { } } +@Component({ + selector: 'list', + template: '' +}) +export class NovoListElement implements AfterContentInit { + @ContentChildren(NovoItemElement) public items: QueryList; + + constructor(private dropdown: NovoDropdownElement) { } + + public ngAfterContentInit(): void { + this.dropdown.items = this.items; + } +} + @Component({ selector: 'dropdown-item-header', template: '', diff --git a/src/elements/table/extras/dropdown-cell/DropdownCell.ts b/src/elements/table/extras/dropdown-cell/DropdownCell.ts index e7ad1101c..f29d5dfd5 100644 --- a/src/elements/table/extras/dropdown-cell/DropdownCell.ts +++ b/src/elements/table/extras/dropdown-cell/DropdownCell.ts @@ -20,7 +20,7 @@ export interface INovoDropdownCellConfig { {{ config.category }} - + {{ option.label || option }}