Skip to content

Commit 2a1306f

Browse files
committed
Added support for multiple select options
1 parent b4e83ec commit 2a1306f

File tree

5 files changed

+115
-27
lines changed

5 files changed

+115
-27
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ To run the demo app:
8080
$ npm run demo
8181
```
8282

83-
To compile the demo app without running the app:
83+
To compile the demo app without running it:
8484

8585
```bash
8686
$ npm run tsc-demo

components/dropdown/dropdown.service.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import {EventEmitter, ElementRef} from 'angular2/core';
33
const DISABLED = 'disabled';
44
const OUTSIDECLICK = 'outsideClick';
55

6-
const KEYCODE = {
6+
export const KEYCODE = {
77
LEFT: 37,
88
UP: 38,
99
RIGHT: 39,
1010
DOWN: 40,
1111

1212
ESCAPE: 27,
13-
ENTER: 13
13+
ENTER: 13,
14+
15+
BACKSPACE: 8
1416
};
1517

1618
export class DropdownService {
@@ -154,15 +156,14 @@ export class DropdownService {
154156
this.selectPreviousItem();
155157
break;
156158
case KEYCODE.ENTER:
157-
if (!this.selectedItem.hasAttribute("suiDropdown")) {
159+
if (this.selectedItem && !this.selectedItem.hasAttribute("suiDropdown")) {
158160
(<HTMLElement> this.selectedItem).click();
159-
160-
this.isOpen = false;
161+
this.selectedItem = null;
161162
break;
162163
}
163164
//Fall through on purpose! (So enter on a nested dropdown acts as right arrow)
164165
case KEYCODE.RIGHT:
165-
if (this.selectedItem.hasAttribute("suiDropdown")) {
166+
if (this.selectedItem && this.selectedItem.hasAttribute("suiDropdown")) {
166167
(<HTMLElement> this.selectedItem).click();
167168
this.selectedItem = this.selectedItem.querySelector(`.${this.itemClass}:not(.${this.itemDisabledClass})`);
168169
}

components/search/search.component.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {Dropdown, DropdownMenu} from '../dropdown';
55
@Component({
66
selector: 'sui-search',
77
directives: [DropdownMenu],
8-
inputs: ['placeholder', 'options', 'optionsField', 'searchDelay', 'icon'],
8+
inputs: ['placeholder', 'options', 'optionsField', 'searchDelay', 'icon', 'isDisabled'],
99
outputs: ['selectedOptionChange'],
1010
host: {
1111
'[class.visible]': 'isOpen',
@@ -40,6 +40,7 @@ export class Search extends Dropdown implements AfterViewInit {
4040

4141
public selectedOption:any;
4242
public selectedOptionChange:EventEmitter<any> = new EventEmitter(false);
43+
public onItemSelected:EventEmitter<any> = new EventEmitter(false);
4344

4445
protected _options:Array<any> = [];
4546
protected _optionsLookup:((query:string) => Promise<any>);
@@ -74,7 +75,6 @@ export class Search extends Dropdown implements AfterViewInit {
7475
this._queryTimer = setTimeout(() => {
7576
this.search(() => {
7677
this.isOpen = true;
77-
this._loading = false;
7878
});
7979
}, this.searchDelay);
8080
return;
@@ -99,6 +99,7 @@ export class Search extends Dropdown implements AfterViewInit {
9999
if (this._optionsLookup) {
100100
if (this._resultsCache[this._query]) {
101101
this._results = this._resultsCache[this._query];
102+
this._loading = false;
102103
callback();
103104
return;
104105
}
@@ -110,6 +111,7 @@ export class Search extends Dropdown implements AfterViewInit {
110111
return;
111112
}
112113
this._results = this.options.filter((o:string) => this.deepValue(o, this.optionsField).slice(0, this.query.length).toLowerCase() == this.query.toLowerCase());
114+
this._loading = false;
113115
callback();
114116
}
115117

@@ -128,8 +130,9 @@ export class Search extends Dropdown implements AfterViewInit {
128130

129131
public select(result:any):void {
130132
this.selectedOption = result;
131-
this.selectedOptionChange.emit(this.selectedOption);
132-
this._query = this.deepValue(this.selectedOption, this.optionsField);
133+
this.selectedOptionChange.emit(result);
134+
this.onItemSelected.emit(result);
135+
this._query = this.deepValue(result, this.optionsField);
133136
this.isOpen = false;
134137
}
135138

components/select/select.component.ts

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,36 @@ import {Component, Directive, Provider, Input, ViewChild, HostBinding, ElementRe
22
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from 'angular2/common';
33
import {Search, SearchValueAccessor} from '../search';
44
import {DropdownMenu} from '../dropdown';
5+
import {KEYCODE} from '../dropdown/dropdown.service';
56

67
@Component({
78
selector: 'sui-select',
89
directives: [DropdownMenu],
9-
inputs: ['placeholder', 'options', 'optionsField', 'isSearchable', 'searchDelay', 'isDisabled'],
10+
inputs: ['placeholder', 'options', 'optionsField', 'isSearchable', 'searchDelay', 'isDisabled', 'allowMultiple'],
1011
outputs: ['selectedOptionChange'],
1112
host: {
1213
'[class.visible]': 'isOpen',
1314
'[class.disabled]': 'isDisabled'
1415
},
1516
template: `
1617
<i class="dropdown icon"></i>
17-
<input *ngIf="isSearchable" class="search" type="text" autocomplete="off" [(ngModel)]="query">
18-
<div *ngIf="!selectedOption" class="default text" [class.filtered]="query" (click)="focus()">{{ placeholder }}</div>
18+
<!-- Multi-select labels -->
19+
<a *ngFor="#selected of selectedOptions; #i = index" class="ui label" (click)="selectedOptionClick($event)">
20+
<content [innerHTML]="selectedOptionsHTML[i]"></content>
21+
<i class="delete icon" (click)="deselectOption(selected); selectedOptionClick($event)"></i>
22+
</a>
23+
<!-- Search input box -->
24+
<input *ngIf="isSearchable" class="search" type="text" autocomplete="off" [(ngModel)]="query" (keydown)="searchKeyDown($event)">
25+
<!-- Single-select label -->
26+
<div *ngIf="!selectedOption" class="default text" [class.filtered]="query">{{ placeholder }}</div>
1927
<div *ngIf="selectedOption" class="text" [class.filtered]="query" [innerHTML]="selectedOptionHTML"></div>
28+
<!-- Select dropdown menu -->
2029
<div class="menu" suiDropdownMenu>
2130
<ng-content></ng-content>
2231
<div *ngIf="!results.length" class="message">No Results</div>
2332
</div>
24-
`
33+
`,
34+
styles: [`:host input.search { width: 12em !important; }`]
2535
})
2636
export class Select extends Search {
2737
@ViewChild(DropdownMenu) protected _menu:DropdownMenu;
@@ -32,12 +42,17 @@ export class Select extends Search {
3242

3343
@HostBinding('class.search')
3444
public isSearchable:boolean = false;
45+
@HostBinding('class.multiple')
46+
public allowMultiple:boolean = false;
3547
protected searchDelay:number = 0;
3648
@HostBinding('class.loading')
3749
protected _loading:boolean = false;
3850
public placeholder:string = "Select one";
3951
public selectedOptionHTML:string;
4052

53+
public selectedOptions:any = [];
54+
public selectedOptionsHTML:Array<string> = [];
55+
4156
@HostBinding('class.active')
4257
public get isOpen():boolean {
4358
return this._service.isOpen;
@@ -48,27 +63,33 @@ export class Select extends Search {
4863
}
4964

5065
protected get results():Array<any> {
66+
var results = this.options;
5167
if (this.isSearchable || this._optionsLookup) {
52-
return this._results;
68+
results = this._results;
69+
}
70+
if (this.allowMultiple) {
71+
results = results.filter(r => (this.selectedOptions || []).indexOf(r) == -1);
5372
}
54-
return this.options;
73+
return results;
5574
}
5675

5776
constructor(private el:ElementRef) {
5877
super(el);
5978
this._allowEmptyQuery = true;
6079

80+
this._service.autoClose = "outsideClick";
81+
6182
this._service.itemClass = "item";
6283
this._service.itemSelectedClass = "selected";
6384

6485
this._service.isOpenChange.subscribe((isOpen:boolean) => {
6586
if (isOpen) {
66-
if (this.isSearchable) {
87+
if (this.isSearchable && !this._service.selectedItem) {
6788
this._service.selectNextItem();
6889
}
6990
}
7091
else {
71-
if (this.query) {
92+
if (this.query && !this.allowMultiple) {
7293
if (this._service.selectedItem) {
7394
(<HTMLElement> this._service.selectedItem).click();
7495
return;
@@ -79,18 +100,62 @@ export class Select extends Search {
79100
});
80101
}
81102

82-
public selectOption(result:any, valueHTML:string):void {
83-
super.select(result);
103+
public selectOption(option:any, valueHTML:string):void {
104+
if (!this.allowMultiple) {
105+
super.select(option);
106+
this.selectedOptionHTML = valueHTML;
107+
}
108+
else {
109+
this.selectedOptions = this.selectedOptions || [];
110+
this.selectedOptions.push(option);
111+
this.selectedOptionsHTML.push(valueHTML);
112+
113+
this.selectedOptionChange.emit(this.selectedOptions);
114+
this.onItemSelected.emit(option);
115+
}
116+
if (this.isSearchable) {
117+
this.focusFirstItem();
118+
this.focusSearch();
119+
}
84120
this._query = "";
85-
this.selectedOptionHTML = valueHTML;
121+
if (this.isSearchable) {
122+
this.search(() => {});
123+
}
124+
}
125+
126+
public deselectOption(option:any) {
127+
var index = this.selectedOptions.indexOf(option);
128+
this.selectedOptions.splice(index, 1);
129+
this.selectedOptionsHTML.splice(index, 1);
130+
this.selectedOptionChange.emit(this.selectedOptions);
131+
}
132+
133+
//noinspection JSMethodCanBeStatic
134+
private selectedOptionClick(event) {
135+
event.stopPropagation();
86136
}
87137

88-
private focus() {
138+
private focusSearch() {
89139
if (this.isSearchable) {
90140
this._service.dropdownElement.nativeElement.querySelector("input").focus();
91141
}
92142
}
93143

144+
private focusFirstItem() {
145+
setTimeout(() => {
146+
this._service.selectedItem = null;
147+
this._service.selectNextItem();
148+
})
149+
}
150+
151+
public writeValue(value:any) {
152+
if (this.allowMultiple) {
153+
this.selectedOptions = value;
154+
return;
155+
}
156+
this.selectedOption = value;
157+
}
158+
94159
@HostListener('click', ['$event'])
95160
public click(event:MouseEvent):boolean {
96161
event.stopPropagation();
@@ -100,6 +165,7 @@ export class Select extends Search {
100165
this.search(() => {
101166
this._loading = false;
102167
this.isOpen = true;
168+
this.focusSearch();
103169
});
104170
}
105171
else if ((<Element> event.target).tagName != "INPUT") {
@@ -108,6 +174,16 @@ export class Select extends Search {
108174
}
109175
return false;
110176
}
177+
178+
public searchKeyDown(event) {
179+
if (event.which == KEYCODE.BACKSPACE && !this._query) {
180+
var selectedOptions = this.selectedOptions || [];
181+
var lastSelectedOption = selectedOptions[selectedOptions.length - 1];
182+
if (lastSelectedOption) {
183+
this.deselectOption(lastSelectedOption);
184+
}
185+
}
186+
}
111187
}
112188

113189
@Component({
@@ -122,8 +198,9 @@ export class SelectOption {
122198

123199
constructor(private host:Select, private el:ElementRef) { }
124200

125-
@HostListener('click')
126-
public click():boolean {
201+
@HostListener('click', ['$event'])
202+
public click(event):boolean {
203+
event.stopPropagation();
127204
this.host.selectOption(this.value, this.el.nativeElement.innerHTML);
128205
return false;
129206
}

demo/app/components/test.page.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,21 @@ import {TEMPLATE_DIRECTIVES} from "../../../components/template";
2020
<div class="ui dividing right rail"></div>
2121
<h2 class="ui dividing header">Examples</h2>
2222
<div class="ui segment">
23-
<sui-select [placeholder]="placeholder" [options]="options" optionsField="test" [(ngModel)]="selected" [isSearchable]="true" [isDisabled]="isDisabled" #select>
23+
<sui-select [placeholder]="placeholder" [options]="options" optionsField="test" [(ngModel)]="selected" [isSearchable]="true" #select>
2424
<sui-select-option *ngFor="#result of select.results" [value]="result"><i class="af flag"></i>{{ result.test }}</sui-select-option>
2525
</sui-select>
2626
</div>
2727
<div class="ui segment">
28-
<sui-checkbox [(ngModel)]="isDisabled">Disabled?</sui-checkbox>
2928
<p>Selected option: {{ selected | json }}</p>
3029
</div>
30+
<div class="ui segment">
31+
<sui-select class="fluid" [options]="options" optionsField="test" [(ngModel)]="selectedItems" [isSearchable]="false" [allowMultiple]="true" #multiSelect>
32+
<sui-select-option *ngFor="#result of multiSelect.results" [value]="result"><i class="af flag"></i>{{ result.test }}</sui-select-option>
33+
</sui-select>
34+
</div>
35+
<div class="ui segment">
36+
<p>Selected items: {{ selectedItems | json }}</p>
37+
</div>
3138
</div>
3239
`
3340
})

0 commit comments

Comments
 (0)