ngx-tagify
Version:
Angular library that wraps @yaireo/tagify
270 lines (263 loc) • 13 kB
JavaScript
import Tagify from '@yaireo/tagify';
export { default as Tagify } from '@yaireo/tagify';
import * as i0 from '@angular/core';
import { Injectable, inject, ElementRef, viewChild, input, output, effect, forwardRef, Component, NgModule } from '@angular/core';
import { NgClass } from '@angular/common';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject, BehaviorSubject, pairwise, fromEvent, asyncScheduler } from 'rxjs';
import { takeUntil, throttleTime } from 'rxjs/operators';
import { toObservable } from '@angular/core/rxjs-interop';
class TagifyService {
constructor() {
this.tagifyMap = new Map();
}
/**
* Adds a tagify instance, so it is available via service. Used internally.
*/
add(name, tagify) {
if (this.tagifyMap.get(name)) {
console.warn(`There already exists a tagify instance with name ${name}!`);
return;
}
this.tagifyMap.set(name, tagify);
}
/**
* Get tagify instance for full access to tagify API.
*/
get(name) {
return this.tagifyMap.get(name);
}
/**
* Removes a tagify instance from service. Used internally.
*/
remove(name) {
this.tagifyMap.delete(name);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class TagifyComponent {
get value() {
return this.valueData;
}
set value(v) {
if (v !== this.valueData) {
this.valueData = v;
this.onChange(v);
}
}
constructor() {
this.valueType = 'undefined';
this.onChange = Function.prototype;
this.onTouched = Function.prototype;
this.unsubscribe$ = new Subject();
this.value$ = new BehaviorSubject(null);
this.skip = false;
this.tagifyService = inject(TagifyService);
this.element = inject((ElementRef));
this.inputRef = viewChild('inputRef', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
this.settings = input({}, ...(ngDevMode ? [{ debugName: "settings" }] : []));
this.name = input('', ...(ngDevMode ? [{ debugName: "name" }] : []));
this.whitelist = input(...(ngDevMode ? [undefined, { debugName: "whitelist" }] : []));
this.inputClass = input('', ...(ngDevMode ? [{ debugName: "inputClass" }] : []));
this.readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
this.add = output();
this.remove = output();
this.tInput = output();
effect(() => {
const readonly = this.readonly();
if (this.tagify) {
this.tagify.setReadonly(readonly);
}
});
effect(() => {
const disabled = this.disabled();
if (this.tagify) {
this.tagify.setDisabled(disabled);
}
});
toObservable(this.inputClass)
.pipe(pairwise())
.subscribe(([oldValue, newValue]) => {
this.setTagsClass(oldValue, newValue);
});
}
ngAfterViewInit() {
const settings = this.settings();
settings.callbacks = settings.callbacks || {};
if (!Object.prototype.hasOwnProperty.call(settings.callbacks, 'add')) {
settings.callbacks.add = () => this.add.emit({
tags: this.tagify.value,
added: this.tagify.value[this.tagify.value.length - 1],
});
}
if (!Object.prototype.hasOwnProperty.call(settings.callbacks, 'remove')) {
settings.callbacks.remove = () => this.remove.emit(this.tagify.value);
}
const innerText = this.element.nativeElement.textContent;
this.tagify = new Tagify(this.inputRef().nativeElement, settings);
// add to service if name is provided
if (this.name().length) {
this.tagifyService.add(this.name(), this.tagify);
}
this.tagify.setReadonly(this.readonly());
this.tagify.setDisabled(this.disabled());
// if there is some text inside component, load this value and skip first change check
if (innerText.length) {
this.tagify.loadOriginalValues(innerText);
this.skip = true;
setTimeout(() => {
this.setValue();
});
}
// listen to value changes from outside
this.value$.pipe(takeUntil(this.unsubscribe$)).subscribe((tags) => {
if (tags === null)
return;
if (this.skip) {
this.skip = false;
return;
}
if (this.valueType === 'undefined') {
this.valueType = typeof tags;
}
// if string is passed, e.g. via reactive forms
if (typeof tags === 'string') {
this.tagify.loadOriginalValues(tags);
setTimeout(() => {
this.setValue();
});
return;
}
// add all tags (already existing tags will be skipped
this.tagify.addTags(tags, false, true);
// remove all tags that are not part of value anymore
this.tagify.value.forEach((v) => {
if (!tags.find((t) => t.value === v.value)) {
// somehow removeTags() with string parameter doesn't always find the tag element
// this is a workaround for finding the right tag element
const tagElm = this.tagify
.getTagElms()
.find((el) => el.attributes.getNamedItem('value').textContent === v.value);
this.tagify.removeTags(tagElm);
}
});
});
// listen to tagify events
this.tagify.on('input', (e) => {
const value = 'value' in e.detail ? e.detail.value : e.detail.textContent;
this.tInput.emit(value);
if (this.valueType === 'string' && this.tagify.settings.mode === 'mix') {
this.value = this.tagify.getMixedTagsAsString();
}
});
fromEvent(this.tagify, 'change')
.pipe(
// throttle used to reduce number of value changes when adding/removing a bunch of tags
throttleTime(0, asyncScheduler, { leading: false, trailing: true }), takeUntil(this.unsubscribe$))
.subscribe(() => {
this.setValue();
});
// listen to suggestions updates
this.whitelist()
?.pipe(takeUntil(this.unsubscribe$))
.subscribe((list) => {
this.tagify.settings.whitelist = list;
});
}
writeValue(tags) {
this.value$.next(tags);
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setValue() {
if (this.valueType === 'string') {
if (this.tagify.settings.mode === 'mix') {
this.value = this.tagify.getMixedTagsAsString();
}
else {
this.value = this.tagify.DOM.originalInput.value;
}
}
else {
this.value = this.tagify.value.slice();
}
}
/**
* Tagify creates a `tags` element to which the classes of the `input` element are applied.
* Changes of `inputClass` are applied automatically to the `input` element, but have to be
* manually applied to the `tags` element.
*/
setTagsClass(oldClass, newClass) {
const tagsElement = this.element.nativeElement.querySelector('tags');
if (tagsElement) {
tagsElement.classList.remove(...oldClass.split(/\s+/));
tagsElement.classList.add(...newClass.split(/\s+/));
}
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
this.tagify.destroy();
const instanceName = this.name();
if (instanceName.length) {
this.tagifyService.remove(instanceName);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.15", type: TagifyComponent, isStandalone: true, selector: "tagify", inputs: { settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, whitelist: { classPropertyName: "whitelist", publicName: "whitelist", isSignal: true, isRequired: false, transformFunction: null }, inputClass: { classPropertyName: "inputClass", publicName: "inputClass", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { add: "add", remove: "remove", tInput: "tInput" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TagifyComponent),
multi: true,
},
], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `<input [ngClass]="inputClass()" #inputRef />
<span style="display: none"><ng-content></ng-content></span>`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyComponent, decorators: [{
type: Component,
args: [{
selector: 'tagify',
imports: [NgClass],
template: `<input [ngClass]="inputClass()" #inputRef />
<span style="display: none"><ng-content></ng-content></span>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TagifyComponent),
multi: true,
},
],
}]
}], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['inputRef', { isSignal: true }] }], settings: [{ type: i0.Input, args: [{ isSignal: true, alias: "settings", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], whitelist: [{ type: i0.Input, args: [{ isSignal: true, alias: "whitelist", required: false }] }], inputClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputClass", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], add: [{ type: i0.Output, args: ["add"] }], remove: [{ type: i0.Output, args: ["remove"] }], tInput: [{ type: i0.Output, args: ["tInput"] }] } });
class TagifyModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: TagifyModule, imports: [TagifyComponent], exports: [TagifyComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TagifyModule, decorators: [{
type: NgModule,
args: [{
imports: [TagifyComponent],
exports: [TagifyComponent],
}]
}] });
/*
* Public API Surface of ngx-tagify
*/
/**
* Generated bundle index. Do not edit.
*/
export { TagifyComponent, TagifyModule, TagifyService };
//# sourceMappingURL=ngx-tagify.mjs.map