src/pill-input/pill-input.component.ts
Internal component used to render pills and the pill text input. There is a sizeable chunk of logic here handling focus and keyboard state around pills.
| selector | ibm-pill-input |
Properties |
|
Methods |
|
Inputs |
Outputs |
HostBindings |
constructor(elementRef: ElementRef)
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:138
|
||||||||
|
instaniates a pill-input
Parameters :
|
disabled
|
is the input disabled. true/false
Default value: |
|
Defined in src/pill-input/pill-input.component.ts:114
|
|
displayValue
|
value to display when something is selected |
|
Defined in src/pill-input/pill-input.component.ts:109
|
|
pillDirection
|
the direction of the pills
Type:
Default value: |
|
Defined in src/pill-input/pill-input.component.ts:116
|
|
pills
|
array of selected items
Type: |
|
Defined in src/pill-input/pill-input.component.ts:105
|
|
placeholder
|
value to display when nothing is selected |
|
Defined in src/pill-input/pill-input.component.ts:107
|
|
size
|
Type:
Default value: |
|
Defined in src/pill-input/pill-input.component.ts:112
|
|
type
|
"single" or "multi" for single or multi select lists
Type:
Default value: |
|
Defined in src/pill-input/pill-input.component.ts:111
|
|
blur
|
emitted when the component looses focus $event type: EventEmitter
|
|
Defined in src/pill-input/pill-input.component.ts:126
|
|
focus
|
emitted when the component is focused $event type: EventEmitter
|
|
Defined in src/pill-input/pill-input.component.ts:124
|
|
search
|
emitted when the user types into an input $event type: EventEmitter
|
|
Defined in src/pill-input/pill-input.component.ts:120
|
|
submit
|
emitted when the user presses enter and a value is present $event type: EventEmitter
|
|
Defined in src/pill-input/pill-input.component.ts:122
|
|
updatePills
|
empty event to trigger an update of the selected items $event type: EventEmitter
|
|
Defined in src/pill-input/pill-input.component.ts:118
|
|
| style.width.% |
style.width.%:
|
Default value : 100
|
|
Defined in src/pill-input/pill-input.component.ts:138
|
| Private checkPlaceholderVisibility |
checkPlaceholderVisibility()
|
|
Defined in src/pill-input/pill-input.component.ts:389
|
|
checks weather the placeholder should be displayed or not.
Returns :
void
|
| Public clearInputText | ||||||||
clearInputText(toSkip: )
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:307
|
||||||||
|
clears the content of inputs
Parameters :
Returns :
void
|
| Public doResize |
doResize()
|
|
Defined in src/pill-input/pill-input.component.ts:296
|
|
sets the height of the input wrapper to the correct height for all selected pills
Returns :
void
|
| Public empty | ||||||||
empty(array: Array
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:223
|
||||||||
|
Helper method to check if an array is empty
Parameters :
Returns :
boolean
|
| Public focusInput | ||||||||
focusInput(ev: )
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:253
|
||||||||
|
focuses the correct input and handles clearing any unnecessary values from any other input
Parameters :
Returns :
void
|
| Public getInputText | ||||||||
getInputText(ev: )
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:322
|
||||||||
|
returns the text from an event, the textContent of the first filled pillInput, or an empty string
Parameters :
Returns :
string
|
| ngAfterViewInit |
ngAfterViewInit()
|
|
Defined in src/pill-input/pill-input.component.ts:190
|
|
Binds events on the view.
Returns :
void
null |
| ngOnChanges | ||||||||
ngOnChanges(changes: )
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:148
|
||||||||
|
Updates the pills, and subscribes to their
Parameters :
Returns :
void
|
| onKeydown | ||||||||
onKeydown(ev: KeyboardEvent)
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:343
|
||||||||
|
handles deleting pills on backspace, submitting user input on enter, and navigating the pill list with arrow left/right
Parameters :
Returns :
void
|
| onKeyup | ||||||||
onKeyup(ev: KeyboardEvent)
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:380
|
||||||||
|
handles emmiting the search event
Parameters :
Returns :
void
|
| Public setFocus | ||||||||
setFocus(state: boolean)
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:238
|
||||||||
|
sets focus to state
Parameters :
Returns :
void
|
| Private setSelection | ||||||||
setSelection(target: )
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:402
|
||||||||
|
selects all the text in a given node
Parameters :
Returns :
void
|
| Public showMore | ||||||||
showMore(ev: )
|
||||||||
|
Defined in src/pill-input/pill-input.component.ts:286
|
||||||||
|
toggles the expanded state of the input wrapper to show all pills
Parameters :
Returns :
void
|
| Public expanded |
expanded:
|
Default value : false
|
|
Defined in src/pill-input/pill-input.component.ts:103
|
|
sets the expanded state (hide/show all selected pills) |
| Public expandedHeight |
expandedHeight:
|
Default value : 0
|
|
Defined in src/pill-input/pill-input.component.ts:97
|
|
height of the expanded input |
| Public focusActive |
focusActive:
|
Default value : false
|
|
Defined in src/pill-input/pill-input.component.ts:95
|
|
are we focused? needed because we have a lot of inputs that could steal focus and we need to set visual focus on the wrapper |
| inputWrapper |
inputWrapper:
|
Decorators : ViewChild
|
|
Defined in src/pill-input/pill-input.component.ts:130
|
|
ViewChild for the overall wrapper |
| Public numberMore |
numberMore:
|
Default value : 0
|
|
Defined in src/pill-input/pill-input.component.ts:99
|
|
number of pills hidden by overflow |
| pillInputs |
pillInputs:
|
Type : QueryList<any>
|
Decorators : ViewChildren
|
|
Defined in src/pill-input/pill-input.component.ts:134
|
|
List of inputs |
| pillInstances |
pillInstances:
|
Type : QueryList<Pill>
|
Decorators : ViewChildren
|
|
Defined in src/pill-input/pill-input.component.ts:136
|
|
list of instantiated pills |
| pillWrapper |
pillWrapper:
|
Decorators : ViewChild
|
|
Defined in src/pill-input/pill-input.component.ts:128
|
|
ViewChild of the pill wrapper |
| Public showPlaceholder |
showPlaceholder:
|
Default value : true
|
|
Defined in src/pill-input/pill-input.component.ts:101
|
|
should we show the placeholder value? |
| singleInput |
singleInput:
|
Decorators : ViewChild
|
|
Defined in src/pill-input/pill-input.component.ts:132
|
|
ViewChild for the single input input box |
import {
Component,
Input,
Output,
EventEmitter,
ElementRef,
ViewChild,
ViewChildren,
QueryList,
OnChanges,
AfterViewInit,
HostBinding
} from "@angular/core";
import { Pill } from "./pill.component";
import { ListItem } from "./../dropdown/list-item.interface";
/**
* Internal component used to render pills and the pill text input.
* There is a sizeable chunk of logic here handling focus and keyboard state around pills.
*
* @export
* @class PillInput
* @implements {OnChanges}
* @implements {AfterViewInit}
*/
@Component({
selector: "ibm-pill-input",
template: `
<div
#inputWrapper
*ngIf="type === 'multi'"
role="textbox"
class="pill_input_wrapper"
[ngClass]="{
'expand-overflow': expanded,
focus: focusActive,
disabled: disabled
}"
style="overflow: hidden;"
(click)="focusInput($event)">
<span
*ngIf="showPlaceholder"
class="input_placeholder">
{{ placeholder }}
</span>
<div
#pillWrapper
[ngClass]="{
'input_pills--column': pillDirection === 'column',
'input_pills': pillDirection === 'row'
}">
<div style="display: flex" *ngFor="let pill of pills; let last = last">
<ibm-pill
[item]="pill">
{{ pill.content }}
</ibm-pill>
<div
#pillInput
*ngIf="!last"
class="input"
contenteditable
(keydown)="onKeydown($event)"
(keyup)="onKeyup($event)"></div>
</div>
<div
#pillInput
class="input"
contenteditable
(keydown)="onKeydown($event)"
(keyup)="onKeyup($event)"></div>
</div>
<button
*ngIf="!expanded && numberMore > 0"
class="btn--link"
href=""
(click)="showMore($event)">{{ numberMore }} more</button>
<button
*ngIf="expanded && numberMore > 0"
class="btn--link"
href=""
(click)="showMore($event)">Hide</button>
</div>
<input
#singleInput
*ngIf="type === 'single'"
type="text"
[disabled]="disabled"
[placeholder]="placeholder"
(keydown)="onKeydown($event)"
(keyup)="onKeyup($event)"/>`
})
export class PillInput implements OnChanges, AfterViewInit {
/** are we focused? needed because we have a lot of inputs that could steal focus and we need to set visual focus on the wrapper */
public focusActive = false;
/** height of the expanded input */
public expandedHeight = 0;
/** number of pills hidden by overflow */
public numberMore = 0;
/** should we show the placeholder value? */
public showPlaceholder = true;
/** sets the expanded state (hide/show all selected pills) */
public expanded = false;
/** array of selected items */
@Input() pills: Array<ListItem> = [];
/** value to display when nothing is selected */
@Input() placeholder = "";
/** value to display when something is selected */
@Input() displayValue = "";
/** "single" or "multi" for single or multi select lists */
@Input() type: "single" | "multi" = "single";
@Input() size: "sm" | "md" | "lg" = "md";
/** is the input disabled. true/false */
@Input() disabled = false;
/** the direction of the pills */
@Input() pillDirection: "row" | "column" = "row";
/** empty event to trigger an update of the selected items */
@Output() updatePills = new EventEmitter();
/** emitted when the user types into an input */
@Output() search = new EventEmitter();
/** emitted when the user presses enter and a value is present */
@Output() submit = new EventEmitter();
/** emitted when the component is focused */
@Output() focus = new EventEmitter();
/** emitted when the component looses focus */
@Output() blur = new EventEmitter();
/** ViewChild of the pill wrapper */
@ViewChild("pillWrapper") pillWrapper;
/** ViewChild for the overall wrapper */
@ViewChild("inputWrapper") inputWrapper;
/** ViewChild for the single input input box */
@ViewChild("singleInput") singleInput;
/** List of inputs */
@ViewChildren("pillInput") pillInputs: QueryList<any>;
/** list of instantiated pills */
@ViewChildren(Pill) pillInstances: QueryList<Pill>;
// needed since matter doesn't/can't account for the host element
@HostBinding("style.width.%") width = "100";
/** instaniates a pill-input */
constructor(private elementRef: ElementRef) {}
/**
* Updates the pills, and subscribes to their `remove` events.
* Updates the displayValue and checks if it should be displayed.
* @param changes
*/
ngOnChanges(changes) {
if (changes.pills) {
this.pills = changes.pills.currentValue;
if (this.pillDirection === "column") {
this.numberMore = this.pills.length - 1;
}
setTimeout(() => {
if (this.pillInstances) {
this.numberMore = 0;
let pills = this.elementRef.nativeElement.querySelectorAll(".pill");
if (pills.length > 1) {
for (let pill of pills) {
if (pill.offsetTop > 30) {
this.numberMore++;
}
}
}
this.pillInstances.forEach(item => {
item.remove.subscribe(() => {
this.updatePills.emit();
this.doResize();
if (this.numberMore === 0) { this.expanded = false; }
});
});
this.doResize();
}
});
}
if (changes.displayValue) {
if (this.type === "single" && this.singleInput) {
this.singleInput.nativeElement.value = changes.displayValue.currentValue;
}
this.checkPlaceholderVisibility();
}
}
/**
* Binds events on the view.
* @returns null
* @memberof PillInput
*/
ngAfterViewInit() {
if (this.inputWrapper) {
this.inputWrapper.nativeElement.scrollTop = 0;
}
// TODO: move these to methods and late bind/eager unbind
if (this.disabled) { return; }
// collapse input on outside click
document.addEventListener("click", ev => {
if (this.elementRef.nativeElement.contains(ev.target)) {
this.setFocus(true);
} else {
this.setFocus(false);
}
this.checkPlaceholderVisibility();
});
// keyup here because we want to get the element the event ends on
// **not** the element the event starts from (that would be keydown)
document.addEventListener("keyup", ev => {
if (!this.elementRef.nativeElement.contains(ev.target)) {
this.setFocus(false);
} else {
this.setFocus(true);
}
this.checkPlaceholderVisibility();
});
this.clearInputText();
}
/**
* Helper method to check if an array is empty
* @param {Array<any>} array
*/
public empty(array: Array<any>) {
if (!array) {
return true;
}
if (array.length === 0) {
return true;
}
return false;
}
/**
* sets focus to state
*
* @param {boolean} state
*/
public setFocus(state: boolean) {
this.focusActive = state;
if (this.focusActive) {
this.focus.emit();
} else {
this.blur.emit();
}
}
/**
* focuses the correct input and handles clearing any unnecessary values from any other input
*
* @param ev
*/
public focusInput(ev) {
if (this.disabled) { return; }
this.setFocus(true);
if (this.numberMore > 0 || this.pillDirection === "column") {
this.expandedHeight = this.pillWrapper.nativeElement.offsetHeight;
this.expanded = true;
}
if (this.pillInputs.find(input => input.nativeElement === ev.target)) {
if (ev.target.textContent === "") {
ev.target.textContent = "";
}
this.clearInputText(ev.target);
this.setSelection(ev.target);
} else if (this.getInputText()) {
this.pillInputs.forEach(input => {
if (input.nativeElement.textContent.trim() !== "") {
this.setSelection(input.nativeElement);
}
});
} else {
if (this.pillInputs.last.nativeElement.textContent === "") {
this.pillInputs.last.nativeElement.textContent = "";
}
this.setSelection(this.pillInputs.last.nativeElement);
}
this.inputWrapper.nativeElement.scrollTop = 0;
}
/**
* toggles the expanded state of the input wrapper to show all pills
*
* @param ev
*/
public showMore(ev) {
ev.stopPropagation();
ev.preventDefault();
this.expanded = !this.expanded;
this.doResize();
}
/**
* sets the height of the input wrapper to the correct height for all selected pills
*/
public doResize() {
if (this.expanded) {
this.expandedHeight = this.pillWrapper.nativeElement.offsetHeight;
}
}
/**
* clears the content of inputs
*
* @param toSkip input element to skip clearing
*/
public clearInputText(toSkip = null) {
if (this.pillInputs) {
this.pillInputs.forEach(input => {
if (!toSkip || input.nativeElement !== toSkip) {
input.nativeElement.textContent = "";
}
});
}
}
/**
* returns the text from an event, the textContent of the first filled pillInput, or an empty string
*
* @param ev optional event to pull text from
*/
public getInputText(ev = null): string {
if (this.type === "multi") {
if (ev) {
return ev.target.textContent.trim();
}
if (this.pillInputs) {
let text = this.pillInputs.find(input => input.nativeElement.textContent.trim() !== "");
return text ? text.nativeElement.textContent.trim() : "";
}
}
if (this.type === "single" && ev) {
return ev.target.value.trim();
}
return "";
}
/**
* handles deleting pills on backspace, submitting user input on enter, and navigating the pill list with arrow left/right
*
* @param ev
*/
onKeydown(ev: KeyboardEvent) {
if (ev.key === "Escape") {
this.expanded = false;
} else if (ev.key === "Backspace" && ev.target["textContent"] === "" && !this.empty(this.pills)) {
// stop the window from navigating backwards
ev.preventDefault();
let inputIndex = this.pillInputs.toArray().findIndex(input => input.nativeElement === ev.target);
if (inputIndex > -1) {
this.pills[inputIndex].selected = false;
// - 1 because the last one doesn't get removed, so the focus doesn't leave
if (inputIndex < this.pillInputs.length - 1) {
this.pillInputs.toArray()[inputIndex + 1].nativeElement.focus();
}
}
this.updatePills.emit();
} else if (ev.key === "Enter") {
ev.preventDefault();
if (this.getInputText()) {
let inputIndex = this.pillInputs.toArray().findIndex(input => input.nativeElement.textContent.trim() !== "");
this.submit.emit({
after: this.pills[inputIndex],
value: this.getInputText()
});
this.clearInputText();
}
} else if (ev.key === "ArrowLeft") {
let index = this.pillInputs.toArray().findIndex(input => input.nativeElement === ev.target);
let prevInput = this.pillInputs.toArray()[index - 1];
if (prevInput) { prevInput.nativeElement.focus(); }
} else if (ev.key === "ArrowRight") {
let index = this.pillInputs.toArray().findIndex(input => input.nativeElement === ev.target);
let nextInput = this.pillInputs.toArray()[index + 1];
if (nextInput) { nextInput.nativeElement.focus(); }
}
}
/** handles emmiting the search event */
onKeyup(ev: KeyboardEvent) {
this.doResize();
this.clearInputText(ev.target);
this.search.emit(this.getInputText(ev));
}
/**
* checks weather the placeholder should be displayed or not.
*/
private checkPlaceholderVisibility(): void {
if (this.type === "single") {
setTimeout(() => this.showPlaceholder = !this.displayValue && !this.focusActive && !this.getInputText());
} else {
setTimeout(() => this.showPlaceholder = this.empty(this.pills) && !this.focusActive && !this.getInputText());
}
}
/**
* selects all the text in a given node
*
* @param target node to set the selection on
*/
private setSelection(target) {
let selectionRange = document.createRange();
let selection = window.getSelection();
selectionRange.selectNodeContents(target);
selection.removeAllRanges();
selection.addRange(selectionRange);
target.focus();
}
}