@tinkoff/angular-contenteditable-accessor
Version:
This is a ControlValueAccessor for using Angular forms with contenteditable elements
175 lines (169 loc) • 7.59 kB
JavaScript
import * as i0 from '@angular/core';
import { SecurityContext, ElementRef, Renderer2, forwardRef, Directive, Inject, HostListener, NgModule } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import * as i1 from '@angular/platform-browser';
import { DomSanitizer } from '@angular/platform-browser';
/*
* This is a barebones contenteditable {@link ControlValueAccessor} allowing you to use
* Angular forms with native contenteditable HTML. For security reasons you might want
* to consider sanitizing pasted/dropped content before using it. Also make sure that
* you do not set any dangerous content as control value yourself, because directive
* just outputs control value as-is.
*/
class ContenteditableValueAccessor {
constructor(elementRef, renderer, sanitizer) {
this.elementRef = elementRef;
this.renderer = renderer;
this.sanitizer = sanitizer;
/*
* MutationObserver IE11 fallback (as opposed to input event for modern browsers).
* When mutation removes a tag, i.e. delete is pressed on the last remaining character
* inside a tag — callback is triggered before the DOM is actually changed, therefore
* setTimeout is used
*/
this.observer = new MutationObserver(() => {
setTimeout(() => {
this.onChange(this.processValue(this.elementRef.nativeElement.innerHTML));
});
});
/*
* onTouch callback that marks control as touched and allows FormHooks use
*/
this.onTouched = () => { };
/*
* onChange callback that writes value to control and allows FormHooks use
*/
this.onChange = () => { };
}
/*
* To support IE11 MutationObserver is used to monitor changes to the content
*/
ngAfterViewInit() {
this.observer.observe(this.elementRef.nativeElement, {
characterData: true,
childList: true,
subtree: true,
});
}
/*
* Disconnect MutationObserver IE11 fallback on destroy
*/
ngOnDestroy() {
this.observer.disconnect();
}
/*
* Listen to input events to write innerHTML value into control,
* also disconnect MutationObserver as it is not needed if this
* event works in current browser
*/
onInput() {
this.observer.disconnect();
this.onChange(this.processValue(this.elementRef.nativeElement.innerHTML));
}
/*
* Listen to blur event to mark control as touched
*/
onBlur() {
this.onTouched();
}
/*
* Reacts to external change
*
* @see {@link ControlValueAccessor#writeValue}
*/
writeValue(value) {
this.renderer.setProperty(this.elementRef.nativeElement, 'innerHTML', this.processValue(value));
}
/*
* Registers onChange callback
*
* @see {@link ControlValueAccessor#registerOnChange}
*/
registerOnChange(onChange) {
this.onChange = onChange;
}
/*
* Registers onTouch callback
*
* @see {@link ControlValueAccessor#registerOnTouched}
*/
registerOnTouched(onTouched) {
this.onTouched = onTouched;
}
/*
* Sets disabled state by setting contenteditable attribute to true/false
*
* @see {@link ControlValueAccessor#setDisabledState}
*/
setDisabledState(disabled) {
this.renderer.setAttribute(this.elementRef.nativeElement, 'contenteditable', String(!disabled));
}
/*
* null is treated as empty string to prevent IE11 outputting 'null',
* also single <br> is replaced with empty string when passed to the control
*/
processValue(value) {
var _a;
const processed = String(value === null || value === undefined ? '' : value);
return ((_a = this.sanitizer.sanitize(SecurityContext.HTML, processed.trim() === '<br>' ? '' : processed)) !== null && _a !== void 0 ? _a : '');
}
}
/** @nocollapse */ ContenteditableValueAccessor.ɵfac = function ContenteditableValueAccessor_Factory(t) { return new (t || ContenteditableValueAccessor)(i0.ɵɵdirectiveInject(ElementRef), i0.ɵɵdirectiveInject(Renderer2), i0.ɵɵdirectiveInject(DomSanitizer)); };
/** @nocollapse */ ContenteditableValueAccessor.ɵdir = /** @pureOrBreakMyCode */ i0.ɵɵdefineDirective({ type: ContenteditableValueAccessor, selectors: [["", "contenteditable", "", "formControlName", ""], ["", "contenteditable", "", "formControl", ""], ["", "contenteditable", "", "ngModel", ""]], hostBindings: function ContenteditableValueAccessor_HostBindings(rf, ctx) { if (rf & 1) {
i0.ɵɵlistener("input", function ContenteditableValueAccessor_input_HostBindingHandler() { return ctx.onInput(); })("blur", function ContenteditableValueAccessor_blur_HostBindingHandler() { return ctx.onBlur(); });
} }, features: [i0.ɵɵProvidersFeature([
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef((() => ContenteditableValueAccessor)),
multi: true,
},
])] });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ContenteditableValueAccessor, [{
type: Directive,
args: [{
selector: '[contenteditable][formControlName], [contenteditable][formControl], [contenteditable][ngModel]',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef((() => ContenteditableValueAccessor)),
multi: true,
},
],
}]
}], function () { return [{ type: i0.ElementRef, decorators: [{
type: Inject,
args: [ElementRef]
}] }, { type: i0.Renderer2, decorators: [{
type: Inject,
args: [Renderer2]
}] }, { type: i1.DomSanitizer, decorators: [{
type: Inject,
args: [DomSanitizer]
}] }]; }, { onInput: [{
type: HostListener,
args: ['input']
}], onBlur: [{
type: HostListener,
args: ['blur']
}] }); })();
class ContenteditableValueAccessorModule {
}
/** @nocollapse */ ContenteditableValueAccessorModule.ɵfac = function ContenteditableValueAccessorModule_Factory(t) { return new (t || ContenteditableValueAccessorModule)(); };
/** @nocollapse */ ContenteditableValueAccessorModule.ɵmod = /** @pureOrBreakMyCode */ i0.ɵɵdefineNgModule({ type: ContenteditableValueAccessorModule });
/** @nocollapse */ ContenteditableValueAccessorModule.ɵinj = /** @pureOrBreakMyCode */ i0.ɵɵdefineInjector({});
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ContenteditableValueAccessorModule, [{
type: NgModule,
args: [{
declarations: [ContenteditableValueAccessor],
exports: [ContenteditableValueAccessor],
}]
}], null, null); })();
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(ContenteditableValueAccessorModule, { declarations: [ContenteditableValueAccessor], exports: [ContenteditableValueAccessor] }); })();
/*
* Public API Surface of @tinkoff/angular-contenteditable-accessor
*/
/**
* Generated bundle index. Do not edit.
*/
export { ContenteditableValueAccessor, ContenteditableValueAccessorModule };
//# sourceMappingURL=tinkoff-angular-contenteditable-accessor.js.map