diff --git a/src/app/components/components/chips/chips.component.html b/src/app/components/components/chips/chips.component.html index ac7431d26a..20fb514f53 100644 --- a/src/app/components/components/chips/chips.component.html +++ b/src/app/components/components/chips/chips.component.html @@ -1,54 +1,29 @@ Chips & Autocomplete - Small blocks for multiple items + Autocomplete with chips and no custom inputs - -

Basic Demo

- - - Demo - - - - Code -

HTML:

- - - ]]> - -

Typescript:

- - - -
-
-
-
- - -

Autocomplete Demo

- - - Demo - - - - Code + + + Demo +
+
Type and select a preset option:
+ +
+
+ + Code +

HTML:

+ ]]>

Typescript:

Autocomplete Demo 'need more?', ]; + itemsRequireMatch: string[] = this.items.slice(0, 6); + + toggleReadOnly(): void { + this.readOnly = !this.readOnly; + } } ]]> -
-
-
+ + + + + + +
- -

Autocomplete and requireMatch Demo

- - - Demo - - - - Code + Autocomplete with custom inputs + Autocomplete demo allowing custom inputs + + + + Demo +
+
Type and select option or enter custom text and press enter:
+ +
+
+ + Code +

HTML:

+ ]]>

Typescript:

Autocomplete and requireMatch Demo 'need more?', ]; - itemsRequireMatch: string[] = this.items.slice(0, 6); - - toggleReadOnly(): void { - this.readOnly = !this.readOnly; - } } ]]> -
-
-
+ + + +
+ + Custom chips + Demo allowing custom inputs for tags - - - + + + Demo +
+
Type any test and press enter:
+ +
+
+ + Code + +

HTML:

+ + + ]]> + +

Typescript:

+ + + +
+
+
TdChipsComponent diff --git a/src/platform/core/chips/_chips-theme.scss b/src/platform/core/chips/_chips-theme.scss index 011ddaee5e..0b839a7557 100644 --- a/src/platform/core/chips/_chips-theme.scss +++ b/src/platform/core/chips/_chips-theme.scss @@ -1,5 +1,4 @@ @import '~@angular/material/core/theming/theming'; -@import '~@angular/material/core/style/variables'; @mixin td-chips-theme($theme) { $primary: map-get($theme, primary); @@ -8,37 +7,25 @@ $background: map-get($theme, background); $foreground: map-get($theme, foreground); - // Gradient for showing the dashed line when the input is disabled. - $md-input-underline-disabled-background-image: linear-gradient(to right, - rgba(0, 0, 0, 0.26) 0%, rgba(0, 0, 0, 0.26) 33%, transparent 0%); - - // chip - .td-chip { - background: md-color($background, status-bar); - color: md-color($foreground, text); - md-icon { - color: md-color($foreground, hint-text); - &:hover { - color: md-color($foreground, icon); - } - } - } // chips td-chips { - .mat-input-underline { - border-top: 1px solid md-color($foreground, hint-text); - &.mat-disabled { - background-image: $md-input-underline-disabled-background-image; - } - .mat-input-ripple { - background-color: md-color($primary); - transition: transform $swift-ease-out-duration $swift-ease-out-timing-function, - opacity $swift-ease-out-duration $swift-ease-out-timing-function; - &.mat-accent { - background-color: md-color($accent); + // chip + .mat-basic-chip { + background: md-color($background, status-bar); + color: md-color($foreground, text); + &:focus:not(.td-chip-disabled) { + background: md-color($primary); + &, md-icon { + color: mat-color($primary, default-contrast); + md-icon:hover { + color: md-color($foreground, icon); + } } - &.mat-warn { - background-color: md-color($warn); + } + md-icon { + color: md-color($foreground, hint-text); + &:hover { + color: md-color($foreground, icon); } } } diff --git a/src/platform/core/chips/autocomplete/autocomplete.component.html b/src/platform/core/chips/autocomplete/autocomplete.component.html deleted file mode 100644 index ebfbbaabac..0000000000 --- a/src/platform/core/chips/autocomplete/autocomplete.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
- - - - - - - - -
diff --git a/src/platform/core/chips/autocomplete/autocomplete.component.scss b/src/platform/core/chips/autocomplete/autocomplete.component.scss deleted file mode 100644 index 9ade72b011..0000000000 --- a/src/platform/core/chips/autocomplete/autocomplete.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} \ No newline at end of file diff --git a/src/platform/core/chips/autocomplete/autocomplete.component.ts b/src/platform/core/chips/autocomplete/autocomplete.component.ts deleted file mode 100644 index 383f575dcd..0000000000 --- a/src/platform/core/chips/autocomplete/autocomplete.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Component, Input, Output, forwardRef } from '@angular/core'; -import { EventEmitter } from '@angular/core'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; - -const noop: () => void = () => { - // empty method -}; - -export const TD_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TdAutoCompleteComponent), - multi: true, -}; - -@Component({ - providers: [ TD_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR ], - selector: 'td-autocomplete', - styleUrls: ['./autocomplete.component.scss' ], - templateUrl: './autocomplete.component.html', -}) -export class TdAutoCompleteComponent implements ControlValueAccessor { - - private _value: any = ''; - /** Callback registered via registerOnTouched (ControlValueAccessor) */ - private _onTouchedCallback: () => void = noop; - /** Callback registered via registerOnChange (ControlValueAccessor) */ - private _onChangeCallback: (_: any) => void = noop; - - listName: string = this.randomName(); - - @Input('name') name: string; - @Input('dividerColor') dividerColor: 'primary' | 'accent' | 'warn' = 'primary'; - @Input('placeholder') placeholder: string; - @Input('searchItems') searchItems: string[] = []; - @Input('readOnly') readOnly: boolean = false; - @Input('required') required: boolean = false; - @Input('disabled') disabled: boolean = false; - @Input('autoFocus') autoFocus: boolean = false; - @Input('max') max: string | number; - @Input('maxLength') maxLength: number; - @Input('min') min: string | number; - @Input('minLength') minLength: number; - - get value(): any { return this._value; }; - @Input() set value(v: any) { - if (v !== this._value) { - this._value = v; - this._onChangeCallback(v); - } - } - - @Output('itemSelect') itemSelect: EventEmitter = new EventEmitter(); - @Output('focus') focus: EventEmitter = new EventEmitter(); - @Output('blur') blur: EventEmitter = new EventEmitter(); - - clear(): boolean { - this.writeValue(''); - return true; - } - - randomName(): string { - let text: string = ''; - let possible: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i: number = 0; i < 7; i++ ) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - - return text; - } - - handleItemSelect(): void { - this.itemSelect.emit(this._value); - } - - handleFocus(): void { - this.focus.emit(true); - } - - handleBlur(): void { - this.blur.emit(false); - } - - /** - * Implemented as part of ControlValueAccessor. - * TODO: internal - */ - writeValue(value: any): void { - this._value = value; - } - - /** - * Implemented as part of ControlValueAccessor. - * TODO: internal - */ - registerOnChange(fn: any): void { - this._onChangeCallback = fn; - } - - /** - * Implemented as part of ControlValueAccessor. - * TODO: internal - */ - registerOnTouched(fn: any): void { - this._onTouchedCallback = fn; - } - -} diff --git a/src/platform/core/chips/chip.component.html b/src/platform/core/chips/chip.component.html deleted file mode 100644 index b11973a6bb..0000000000 --- a/src/platform/core/chips/chip.component.html +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/platform/core/chips/chip.component.scss b/src/platform/core/chips/chip.component.scss deleted file mode 100644 index 8bf421a18b..0000000000 --- a/src/platform/core/chips/chip.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import '../common/styles/mixins'; -:host { - display: block; - @include rtl(float, left, right); -} diff --git a/src/platform/core/chips/chip.component.ts b/src/platform/core/chips/chip.component.ts deleted file mode 100644 index 91fb2b812a..0000000000 --- a/src/platform/core/chips/chip.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'td-chip', - styleUrls: ['./chip.component.scss' ], - templateUrl: './chip.component.html', -}) -export class TdChipComponent { - -} diff --git a/src/platform/core/chips/chips.component.html b/src/platform/core/chips/chips.component.html index d865990a16..7207d9caa8 100644 --- a/src/platform/core/chips/chips.component.html +++ b/src/platform/core/chips/chips.component.html @@ -1,21 +1,34 @@
- - - {{chip}} - cancel - - - + + + + {{chip}} + + cancel + + + + + + + + + {{item}} + + +
{ // empty method @@ -18,7 +21,7 @@ export const TD_CHIPS_CONTROL_VALUE_ACCESSOR: any = { styleUrls: ['./chips.component.scss' ], templateUrl: './chips.component.html', }) -export class TdChipsComponent implements ControlValueAccessor, DoCheck { +export class TdChipsComponent implements ControlValueAccessor, DoCheck, OnInit { /** * Implemented as part of ControlValueAccessor. @@ -27,6 +30,10 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { private _length: number = 0; private _requireMatch: boolean = false; + private _readOnly: boolean = false; + + @ViewChild(MdInputDirective) _inputChild: MdInputDirective; + @ViewChildren(MdChip) _chipsChildren: QueryList; /** * Boolean value that specifies if the input is valid against the provieded list. @@ -37,6 +44,21 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { */ focused: boolean = false; + /** + * FormControl for the mdInput element. + */ + inputControl: FormControl = new FormControl(); + + /** + * Subject to control what items to render in the autocomplete + */ + subject: Subject = new Subject(); + + /** + * Observable of items to render in the autocomplete + */ + filteredItems: Observable = this.subject.asObservable(); + /** * items?: string[] * Enables Autocompletion with the provided list of strings. @@ -58,9 +80,20 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { /** * readOnly?: boolean - * Disables the chip input and removal. + * Disables the chips input and chip removal icon. */ - @Input('readOnly') readOnly: boolean = false; + @Input('readOnly') + set readOnly(readOnly: boolean) { + this._readOnly = readOnly; + if (readOnly) { + this.inputControl.disable(); + } else { + this.inputControl.enable(); + } + } + get readOnly(): boolean { + return this._readOnly; + } /** * placeholder?: string @@ -89,10 +122,26 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { if (v !== this._value) { this._value = v; this._length = this._value ? this._value.length : 0; + if (this._value) { + this._filter(this.inputControl.value); + } } } get value(): any { return this._value; }; + ngOnInit(): void { + this.inputControl.valueChanges + .debounceTime(100) + .subscribe((value: string) => { + this.matches = true; + this._filter(value); + }); + // filter the autocomplete options after everything is rendered + Observable.timer().subscribe(() => { + this._filter(this.inputControl.value); + }); + } + ngDoCheck(): void { // Throw onChange event only if array changes size. if (this._value && this._value.length !== this._length) { @@ -103,14 +152,10 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { /** * Returns a list of filtered items. - * Removes the ones that have been added as value. */ - get filteredItems(): string[] { - if (!this._value) { - return []; - } + filter(val: string): string[] { return this.items.filter((item: string) => { - return this._value.indexOf(item) < 0; + return val ? item.indexOf(val) > -1 : true; }); } @@ -118,18 +163,22 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { * Method that is executed when trying to create a new chip from the autocomplete. * returns 'true' if successful, 'false' if it fails. */ - addItem(value: string): boolean { + addChip(value: string): boolean { if (value.trim() === '' || this._value.indexOf(value) > -1) { + this.matches = false; return false; } if (this.items && this.requireMatch) { if (this.items.indexOf(value) < 0) { + this.matches = false; return false; } } this._value.push(value); this.add.emit(value); this.onChange(this._value); + this.inputControl.setValue(''); + this.matches = true; return true; } @@ -137,7 +186,7 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { * Method that is executed when trying to remove a chip. * returns 'true' if successful, 'false' if it fails. */ - removeItem(value: string): boolean { + removeChip(value: string): boolean { let index: number = this._value.indexOf(value); if (index < 0) { return false; @@ -145,6 +194,7 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { this._value.splice(index, 1); this.remove.emit(value); this.onChange(this._value); + this.inputControl.setValue(''); return true; } @@ -155,10 +205,87 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { handleBlur(): boolean { this.focused = false; + this.matches = true; this.onTouched(); return true; } + /** + * Programmatically focus the input. Since its the component entry point + */ + focus(): void { + this._inputChild.focus(); + } + + /** + * Passes relevant input key presses. + */ + _inputKeydown(event: KeyboardEvent): void { + switch (event.keyCode) { + case LEFT_ARROW: + case DELETE: + case BACKSPACE: + /** Check to see if input is empty when pressing left arrow to move to the last chip */ + if (!this._inputChild.value) { + this._focusLastChip(); + event.preventDefault(); + } + break; + case RIGHT_ARROW: + /** Check to see if input is empty when pressing right arrow to move to the first chip */ + if (!this._inputChild.value) { + this._focusFirstChip(); + event.preventDefault(); + } + break; + default: + // default + } + } + + /** + * Passes relevant chip key presses. + */ + _chipKeydown(event: KeyboardEvent, index: number): void { + switch (event.keyCode) { + case DELETE: + case BACKSPACE: + /** Check to see if not in [readOnly] state to delete a chip */ + if (!this.readOnly) { + /** + * Checks if deleting last single chip, to focus input afterwards + * Else check if its not the last chip of the list to focus the next one. + */ + if (index === (this._totalChips - 1) && index === 0) { + this.focus(); + } else if (index < (this._totalChips - 1)) { + this._focusChip(index + 1); + } + this.removeChip(this.value[index]); + } + break; + case LEFT_ARROW: + /** Check to see if left arrow was pressed while focusing the first chip to focus input next */ + if (index === 0) { + this.focus(); + event.stopPropagation(); + } + break; + case RIGHT_ARROW: + /** Check to see if right arrow was pressed while focusing the last chip to focus input next */ + if (index === (this._totalChips - 1)) { + this.focus(); + event.stopPropagation(); + } + break; + case ESCAPE: + this.focus(); + break; + default: + // default + } + } + /** * Implemented as part of ControlValueAccessor. */ @@ -177,4 +304,44 @@ export class TdChipsComponent implements ControlValueAccessor, DoCheck { onChange = (_: any) => noop; onTouched = () => noop; + /** + * + * Method to filter the options for the autocomplete + */ + private _filter(value: string): void { + let items: string[] = this.filter(value); + items = items.filter((filteredItem: string) => { + return this._value && filteredItem ? this._value.indexOf(filteredItem) < 0 : true; + }); + this.subject.next(items); + } + + /** + * Get total of chips + */ + private get _totalChips(): number { + let chips: MdChip[] = this._chipsChildren.toArray(); + return chips.length; + } + + /** + * Method to focus a desired chip by index + */ + private _focusChip(index: number): void { + /** check to see if index exists in the array before focusing */ + if (index > -1 && this._totalChips > index) { + this._chipsChildren.toArray()[index].focus(); + } + } + + /** Method to focus first chip */ + private _focusFirstChip(): void { + this._focusChip(0); + } + + /** MEthod to focus last chip */ + private _focusLastChip(): void { + this._focusChip(this._totalChips - 1); + } + } diff --git a/src/platform/core/chips/chips.module.ts b/src/platform/core/chips/chips.module.ts index 42f997d47a..f9818e5e1c 100644 --- a/src/platform/core/chips/chips.module.ts +++ b/src/platform/core/chips/chips.module.ts @@ -1,32 +1,27 @@ import { NgModule, ModuleWithProviders } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import { MdInputModule, MdIconModule } from '@angular/material'; +import { MdInputModule, MdIconModule, MdAutocompleteModule, MdChipsModule } from '@angular/material'; import { TdChipsComponent } from './chips.component'; -import { TdChipComponent } from './chip.component'; -import { TdAutoCompleteComponent } from './autocomplete/autocomplete.component'; - export { TdChipsComponent } from './chips.component'; @NgModule({ imports: [ - FormsModule, + ReactiveFormsModule, CommonModule, MdInputModule, MdIconModule, + MdChipsModule, + MdAutocompleteModule, ], declarations: [ TdChipsComponent, - TdChipComponent, - TdAutoCompleteComponent, ], exports: [ TdChipsComponent, - TdChipComponent, - TdAutoCompleteComponent, ], }) export class CovalentChipsModule { diff --git a/src/platform/core/common/styles/_input.scss b/src/platform/core/common/styles/_input.scss index 04017e7dfe..39857b8267 100644 --- a/src/platform/core/common/styles/_input.scss +++ b/src/platform/core/common/styles/_input.scss @@ -13,6 +13,14 @@ md-input-container { } .mat-input-underline { border-top-color: md-color($warn); + .mat-input-ripple { + background-color: md-color($warn); + } } } +} + +// Proper placeholder font size +.mat-input-placeholder-wrapper { + font-size: 16px; } \ No newline at end of file