UNPKG

rl-ngx-tag-input

Version:

Tag input component for Angular

440 lines (437 loc) 20.6 kB
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()\">&times;</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