rl-ngx-tag-input
Version:
Tag input component for Angular
440 lines (437 loc) • 20.6 kB
JavaScript
import { Component, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Input, Output, ViewChild, NgModule } from '@angular/core';
import { NG_VALUE_ACCESSOR, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { tap, filter } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
import { CommonModule } from '@angular/common';
var KEYS = {
backspace: 8,
comma: 188,
downArrow: 40,
enter: 13,
esc: 27,
space: 32,
upArrow: 38
};
function isBlank(obj) {
return obj === undefined || obj === null;
}
var TagInputComponent = /** @class */ (function () {
function TagInputComponent(fb, elementRef) {
this.fb = fb;
this.elementRef = elementRef;
this.addOnBlur = true;
this.addOnComma = true;
this.addOnEnter = true;
this.addOnPaste = true;
this.addOnSpace = false;
this.allowDuplicates = false;
this.allowedTagsPattern = /.+/;
this.autocomplete = false;
this.autocompleteItems = [];
this.autocompleteMustMatch = true;
this.autocompleteSelectFirstItem = true;
this.pasteSplitPattern = ',';
this.placeholder = 'Add a tag';
this.addTag = new EventEmitter();
this.removeTag = new EventEmitter();
this.canShowAutoComplete = false;
this.autocompleteResults = [];
this.tagsList = [];
this.onChange = function () { };
this.onTouched = function () { };
}
Object.defineProperty(TagInputComponent.prototype, "tagInputField", {
get: function () {
return this.tagInputForm.get('tagInputField');
},
enumerable: true,
configurable: true
});
Object.defineProperty(TagInputComponent.prototype, "inputValue", {
get: function () {
return this.tagInputField.value;
},
enumerable: true,
configurable: true
});
TagInputComponent.prototype.onDocumentClick = function (event, target) {
if (!target) {
return;
}
if (!this.elementRef.nativeElement.contains(target)) {
this.canShowAutoComplete = false;
}
};
TagInputComponent.prototype.ngOnInit = function () {
var _this = this;
this.splitRegExp = new RegExp(this.pasteSplitPattern);
this.tagInputForm = this.fb.group({
tagInputField: ''
});
this.tagInputSubscription = this.tagInputField.valueChanges
.pipe(tap(function (value) {
_this.autocompleteResults = _this.autocompleteItems.filter(function (item) {
return item.toLowerCase().indexOf(value.toLowerCase()) > -1 && _this._isTagUnique(item);
});
}))
.subscribe();
};
TagInputComponent.prototype.onKeydown = function (event) {
var key = event.keyCode;
switch (key) {
case KEYS.backspace:
this._handleBackspace();
break;
case KEYS.enter:
if (this.addOnEnter && !this.showAutocomplete()) {
this._addTags([this.inputValue]);
event.preventDefault();
}
break;
case KEYS.comma:
if (this.addOnComma) {
this._addTags([this.inputValue]);
event.preventDefault();
}
break;
case KEYS.space:
if (this.addOnSpace) {
this._addTags([this.inputValue]);
event.preventDefault();
}
break;
default:
break;
}
};
TagInputComponent.prototype.onInputBlurred = function (event) {
if (this.addOnBlur) {
this._addTags([this.inputValue]);
}
this.isFocused = false;
};
TagInputComponent.prototype.onInputFocused = function () {
var _this = this;
this.isFocused = true;
setTimeout(function () { return _this.canShowAutoComplete = true; });
};
TagInputComponent.prototype.onInputPaste = function (event) {
var _this = this;
var clipboardData = event.clipboardData || (event.originalEvent && event.originalEvent.clipboardData);
var pastedString = clipboardData.getData('text/plain');
var tags = this._splitString(pastedString);
this._addTags(tags);
setTimeout(function () { return _this._resetInput(); });
};
TagInputComponent.prototype.onAutocompleteSelect = function (selectedItem) {
this._addTags([selectedItem]);
this.tagInputElement.nativeElement.focus();
};
TagInputComponent.prototype.onAutocompleteEnter = function () {
if (this.addOnEnter && this.showAutocomplete() && !this.autocompleteMustMatch) {
this._addTags([this.inputValue]);
}
};
TagInputComponent.prototype.showAutocomplete = function () {
return (this.autocomplete &&
this.autocompleteItems &&
this.autocompleteItems.length > 0 &&
this.canShowAutoComplete &&
this.inputValue.length > 0);
};
TagInputComponent.prototype._splitString = function (tagString) {
tagString = tagString.trim();
var tags = tagString.split(this.splitRegExp);
return tags.filter(function (tag) { return !!tag; });
};
TagInputComponent.prototype._isTagValid = function (tagString) {
return this.allowedTagsPattern.test(tagString) &&
this._isTagUnique(tagString);
};
TagInputComponent.prototype._isTagUnique = function (tagString) {
return this.allowDuplicates ? true : this.tagsList.indexOf(tagString) === -1;
};
TagInputComponent.prototype._isTagAutocompleteItem = function (tagString) {
return this.autocompleteItems.indexOf(tagString) > -1;
};
TagInputComponent.prototype._emitTagAdded = function (addedTags) {
var _this = this;
addedTags.forEach(function (tag) { return _this.addTag.emit(tag); });
};
TagInputComponent.prototype._emitTagRemoved = function (removedTag) {
this.removeTag.emit(removedTag);
};
TagInputComponent.prototype._addTags = function (tags) {
var _this = this;
var validTags = tags.map(function (tag) { return tag.trim(); })
.filter(function (tag) { return _this._isTagValid(tag); })
.filter(function (tag, index, tagArray) { return tagArray.indexOf(tag) === index; })
.filter(function (tag) { return (_this.showAutocomplete() && _this.autocompleteMustMatch) ? _this._isTagAutocompleteItem(tag) : true; });
this.tagsList = this.tagsList.concat(validTags);
this._resetSelected();
this._resetInput();
this.onChange(this.tagsList);
this._emitTagAdded(validTags);
};
TagInputComponent.prototype._removeTag = function (tagIndexToRemove) {
var removedTag = this.tagsList[tagIndexToRemove];
this.tagsList.splice(tagIndexToRemove, 1);
this._resetSelected();
this.onChange(this.tagsList);
this._emitTagRemoved(removedTag);
};
TagInputComponent.prototype._handleBackspace = function () {
if (!this.inputValue.length && this.tagsList.length) {
if (!isBlank(this.selectedTag)) {
this._removeTag(this.selectedTag);
}
else {
this.selectedTag = this.tagsList.length - 1;
}
}
};
TagInputComponent.prototype._resetSelected = function () {
this.selectedTag = null;
};
TagInputComponent.prototype._resetInput = function () {
this.tagInputField.setValue('');
};
TagInputComponent.prototype.writeValue = function (value) {
this.tagsList = value;
};
TagInputComponent.prototype.registerOnChange = function (fn) {
this.onChange = fn;
};
TagInputComponent.prototype.registerOnTouched = function (fn) {
this.onTouched = fn;
};
TagInputComponent.prototype.ngOnDestroy = function () {
this.tagInputSubscription.unsubscribe();
};
return TagInputComponent;
}());
TagInputComponent.decorators = [
{ type: Component, args: [{
selector: 'rl-tag-input',
template: "\n <rl-tag-input-item\n [text]=\"tag\"\n [index]=\"index\"\n [selected]=\"selectedTag === index\"\n (tagRemoved)=\"_removeTag($event)\"\n *ngFor=\"let tag of tagsList; let index = index\">\n </rl-tag-input-item>\n <form [formGroup]=\"tagInputForm\" class=\"ng2-tag-input-form\">\n <input\n class=\"ng2-tag-input-field\"\n type=\"text\"\n #tagInputElement\n formControlName=\"tagInputField\"\n [placeholder]=\"placeholder\"\n (paste)=\"onInputPaste($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onInputBlurred($event)\"\n (focus)=\"onInputFocused()\">\n\n <div *ngIf=\"showAutocomplete()\" class=\"rl-tag-input-autocomplete-container\">\n <rl-tag-input-autocomplete\n [items]=\"autocompleteResults\"\n [selectFirstItem]=\"autocompleteSelectFirstItem\"\n (itemSelected)=\"onAutocompleteSelect($event)\"\n (enterPressed)=\"onAutocompleteEnter($event)\">\n </rl-tag-input-autocomplete>\n </div>\n </form>\n ",
styles: ["\n :host {\n font-family: \"Roboto\", \"Helvetica Neue\", sans-serif;\n font-size: 16px;\n display: block;\n box-shadow: 0 1px #ccc;\n padding: 8px 0 6px 0;\n will-change: box-shadow;\n transition: box-shadow 0.12s ease-out;\n }\n\n :host .ng2-tag-input-form {\n display: inline;\n }\n\n :host .ng2-tag-input-field {\n font-family: \"Roboto\", \"Helvetica Neue\", sans-serif;\n font-size: 16px;\n display: inline-block;\n width: auto;\n box-shadow: none;\n border: 0;\n padding: 8px 0;\n }\n\n :host .ng2-tag-input-field:focus {\n outline: 0;\n }\n\n :host .rl-tag-input-autocomplete-container {\n position: relative;\n z-index: 10;\n }\n\n :host.ng2-tag-input-focus {\n box-shadow: 0 2px #0d8bff;\n }\n "],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(function () { return TagInputComponent; }), multi: true },
]
},] },
];
TagInputComponent.ctorParameters = function () { return [
{ type: FormBuilder, },
{ type: ElementRef, },
]; };
TagInputComponent.propDecorators = {
"isFocused": [{ type: HostBinding, args: ['class.ng2-tag-input-focus',] },],
"addOnBlur": [{ type: Input },],
"addOnComma": [{ type: Input },],
"addOnEnter": [{ type: Input },],
"addOnPaste": [{ type: Input },],
"addOnSpace": [{ type: Input },],
"allowDuplicates": [{ type: Input },],
"allowedTagsPattern": [{ type: Input },],
"autocomplete": [{ type: Input },],
"autocompleteItems": [{ type: Input },],
"autocompleteMustMatch": [{ type: Input },],
"autocompleteSelectFirstItem": [{ type: Input },],
"pasteSplitPattern": [{ type: Input },],
"placeholder": [{ type: Input },],
"addTag": [{ type: Output, args: ['addTag',] },],
"removeTag": [{ type: Output, args: ['removeTag',] },],
"tagInputElement": [{ type: ViewChild, args: ['tagInputElement',] },],
"onDocumentClick": [{ type: HostListener, args: ['document:click', ['$event', '$event.target'],] },],
};
var TagInputAutocompleteComponent = /** @class */ (function () {
function TagInputAutocompleteComponent(elementRef) {
this.elementRef = elementRef;
this.selectFirstItem = false;
this.itemSelected = new EventEmitter();
this.enterPressed = new EventEmitter();
this.selectedItemIndex = null;
}
Object.defineProperty(TagInputAutocompleteComponent.prototype, "itemsCount", {
get: function () {
return this.items ? this.items.length : 0;
},
enumerable: true,
configurable: true
});
TagInputAutocompleteComponent.prototype.ngOnInit = function () {
var _this = this;
this.keySubscription = fromEvent(window, 'keydown')
.pipe(filter(function (event) { return event.keyCode === KEYS.upArrow ||
event.keyCode === KEYS.downArrow ||
event.keyCode === KEYS.enter ||
event.keyCode === KEYS.esc; }), tap(function (event) {
switch (event.keyCode) {
case KEYS.downArrow:
_this.handleDownArrow();
break;
case KEYS.upArrow:
_this.handleUpArrow();
break;
case KEYS.enter:
_this.selectItem();
_this.enterPressed.emit();
break;
case KEYS.esc:
break;
}
event.stopPropagation();
event.preventDefault();
}))
.subscribe();
};
TagInputAutocompleteComponent.prototype.ensureHighlightVisible = function () {
var container = this.elementRef.nativeElement.querySelector('.sk-select-results__container');
if (!container) {
return;
}
var choices = container.querySelectorAll('.sk-select-results__item');
if (choices.length < 1) {
return;
}
if (this.selectedItemIndex < 0) {
return;
}
var highlighted = choices[this.selectedItemIndex];
if (!highlighted) {
return;
}
var posY = highlighted.offsetTop + highlighted.clientHeight - container.scrollTop;
var height = container.offsetHeight;
if (posY > height) {
container.scrollTop += posY - height;
}
else if (posY < highlighted.clientHeight) {
container.scrollTop -= highlighted.clientHeight - posY;
}
};
TagInputAutocompleteComponent.prototype.goToTop = function () {
this.selectedItemIndex = 0;
this.ensureHighlightVisible();
};
TagInputAutocompleteComponent.prototype.goToBottom = function (itemsCount) {
this.selectedItemIndex = itemsCount - 1;
this.ensureHighlightVisible();
};
TagInputAutocompleteComponent.prototype.goToNext = function () {
if (this.selectedItemIndex + 1 < this.itemsCount) {
this.selectedItemIndex++;
}
else {
this.goToTop();
}
this.ensureHighlightVisible();
};
TagInputAutocompleteComponent.prototype.goToPrevious = function () {
if (this.selectedItemIndex - 1 >= 0) {
this.selectedItemIndex--;
}
else {
this.goToBottom(this.itemsCount);
}
this.ensureHighlightVisible();
};
TagInputAutocompleteComponent.prototype.handleUpArrow = function () {
if (this.selectedItemIndex === null) {
this.goToBottom(this.itemsCount);
return false;
}
this.goToPrevious();
};
TagInputAutocompleteComponent.prototype.handleDownArrow = function () {
if (this.selectedItemIndex === null) {
this.goToTop();
return false;
}
this.goToNext();
};
TagInputAutocompleteComponent.prototype.selectItem = function (itemIndex) {
var itemToEmit = itemIndex ? this.items[itemIndex] : this.items[this.selectedItemIndex];
if (itemToEmit) {
this.itemSelected.emit(itemToEmit);
}
};
TagInputAutocompleteComponent.prototype.ngOnChanges = function (changes) {
if (this.selectFirstItem && this.itemsCount > 0) {
this.goToTop();
}
};
TagInputAutocompleteComponent.prototype.ngOnDestroy = function () {
this.keySubscription.unsubscribe();
};
return TagInputAutocompleteComponent;
}());
TagInputAutocompleteComponent.decorators = [
{ type: Component, args: [{
selector: 'rl-tag-input-autocomplete',
template: "\n <div\n *ngFor=\"let item of items; let itemIndex = index\"\n [ngClass]=\"{ 'is-selected': selectedItemIndex === itemIndex }\"\n (click)=\"selectItem(itemIndex)\"\n class=\"rl-autocomplete-item\">\n {{item}}\n </div>\n ",
styles: ["\n :host {\n box-shadow: 0 1.5px 4px rgba(0, 0, 0, 0.24), 0 1.5px 6px rgba(0, 0, 0, 0.12);\n display: block;\n position: absolute;\n top: 100%;\n font-family: \"Roboto\", \"Helvetica Neue\", sans-serif;\n font-size: 16px;\n color: #444444;\n background: white;\n padding: 8px 0;\n }\n\n :host .rl-autocomplete-item {\n padding: 0 16px;\n height: 48px;\n line-height: 48px;\n }\n\n :host .is-selected {\n background: #eeeeee;\n }\n "]
},] },
];
TagInputAutocompleteComponent.ctorParameters = function () { return [
{ type: ElementRef, },
]; };
TagInputAutocompleteComponent.propDecorators = {
"items": [{ type: Input },],
"selectFirstItem": [{ type: Input },],
"itemSelected": [{ type: Output },],
"enterPressed": [{ type: Output },],
};
var TagInputItemComponent = /** @class */ (function () {
function TagInputItemComponent() {
this.tagRemoved = new EventEmitter();
}
Object.defineProperty(TagInputItemComponent.prototype, "isSelected", {
get: function () { return !!this.selected; },
enumerable: true,
configurable: true
});
TagInputItemComponent.prototype.removeTag = function () {
this.tagRemoved.emit(this.index);
};
return TagInputItemComponent;
}());
TagInputItemComponent.decorators = [
{ type: Component, args: [{
selector: 'rl-tag-input-item',
template: "\n {{text}}\n <span\n class=\"ng2-tag-input-remove\"\n (click)=\"removeTag()\">×</span>\n ",
styles: ["\n :host {\n font-family: \"Roboto\", \"Helvetica Neue\", sans-serif;\n font-size: 16px;\n height: 32px;\n line-height: 32px;\n display: inline-block;\n background: #e0e0e0;\n padding: 0 12px;\n border-radius: 90px;\n margin-right: 10px;\n transition: all 0.12s ease-out;\n }\n\n :host .ng2-tag-input-remove {\n background: #a6a6a6;\n border-radius: 50%;\n color: #e0e0e0;\n cursor: pointer;\n display: inline-block;\n font-size: 17px;\n height: 24px;\n line-height: 24px;\n margin-left: 6px;\n margin-right: -6px;\n text-align: center;\n width: 24px;\n }\n\n :host.ng2-tag-input-item-selected {\n color: white;\n background: #0d8bff;\n }\n\n :host.ng2-tag-input-item-selected .ng2-tag-input-remove {\n background: white;\n color: #0d8bff;\n }\n "]
},] },
];
TagInputItemComponent.ctorParameters = function () { return []; };
TagInputItemComponent.propDecorators = {
"selected": [{ type: Input },],
"text": [{ type: Input },],
"index": [{ type: Input },],
"tagRemoved": [{ type: Output },],
"isSelected": [{ type: HostBinding, args: ['class.ng2-tag-input-item-selected',] },],
};
var RlTagInputModule = /** @class */ (function () {
function RlTagInputModule() {
}
return RlTagInputModule;
}());
RlTagInputModule.decorators = [
{ type: NgModule, args: [{
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
TagInputAutocompleteComponent,
TagInputComponent,
TagInputItemComponent
],
exports: [
TagInputComponent
]
},] },
];
export { TagInputComponent, TagInputAutocompleteComponent, TagInputItemComponent, RlTagInputModule };
//# sourceMappingURL=rl-ngx-tag-input.js.map