UNPKG

ngx-quill

Version:

Angular components for the easy use of the QuillJS richt text editor.

932 lines (915 loc) 49.7 kB
import { QUILL_CONFIG_TOKEN, defaultModules } from 'ngx-quill/config'; export * from 'ngx-quill/config'; import * as i0 from '@angular/core'; import { inject, Injectable, input, EventEmitter, signal, ElementRef, ChangeDetectorRef, PLATFORM_ID, Renderer2, NgZone, DestroyRef, SecurityContext, Directive, Output, forwardRef, Component, ViewEncapsulation, Inject, NgModule } from '@angular/core'; import { DOCUMENT, isPlatformServer, NgClass } from '@angular/common'; import * as i1 from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Observable, defer, firstValueFrom, from, forkJoin, of, isObservable, fromEvent, Subscription } from 'rxjs'; import { shareReplay, map, tap, mergeMap, debounceTime } from 'rxjs/operators'; import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'; const getFormat = (format, configFormat) => { const passedFormat = format || configFormat; return passedFormat || 'html'; }; const raf$ = () => { return new Observable(subscriber => { const rafId = requestAnimationFrame(() => { subscriber.next(); subscriber.complete(); }); return () => cancelAnimationFrame(rafId); }); }; class QuillService { constructor() { this.config = inject(QUILL_CONFIG_TOKEN) || { modules: defaultModules }; this.document = inject(DOCUMENT); this.quill$ = defer(async () => { if (!this.Quill) { // Quill adds events listeners on import https://github.com/quilljs/quill/blob/develop/core/emitter.js#L8 // We'd want to use the unpatched `addEventListener` method to have all event callbacks to be run outside of zone. // We don't know yet if the `zone.js` is used or not, just save the value to restore it back further. const maybePatchedAddEventListener = this.document.addEventListener; // There're 2 types of Angular applications: // 1) zone-full (by default) // 2) zone-less // The developer can avoid importing the `zone.js` package and tells Angular that he/she is responsible for running // the change detection by himself. This is done by "nooping" the zone through `CompilerOptions` when bootstrapping // the root module. We fallback to `document.addEventListener` if `__zone_symbol__addEventListener` is not defined, // this means the `zone.js` is not imported. // The `__zone_symbol__addEventListener` is basically a native DOM API, which is not patched by zone.js, thus not even going // through the `zone.js` task lifecycle. You can also access the native DOM API as follows `target[Zone.__symbol__('methodName')]`. this.document.addEventListener = this.document['__zone_symbol__addEventListener'] || this.document.addEventListener; const quillImport = await import('quill'); this.document.addEventListener = maybePatchedAddEventListener; this.Quill = ( // seems like esmodules have nested "default" quillImport.default?.default ?? quillImport.default ?? quillImport); } // Only register custom options and modules once this.config.customOptions?.forEach((customOption) => { const newCustomOption = this.Quill.import(customOption.import); newCustomOption.whitelist = customOption.whitelist; this.Quill.register(newCustomOption, true, this.config.suppressGlobalRegisterWarning); }); return firstValueFrom(this.registerCustomModules(this.Quill, this.config.customModules, this.config.suppressGlobalRegisterWarning)); }).pipe(shareReplay({ bufferSize: 1, refCount: false })); // A list of custom modules that have already been registered, // so we don’t need to await their implementation. this.registeredModules = new Set(); } getQuill() { return this.quill$; } /** @internal */ beforeRender(Quill, customModules, beforeRender = this.config.beforeRender) { // This function is called each time the editor needs to be rendered, // so it operates individually per component. If no custom module needs to be // registered and no `beforeRender` function is provided, it will emit // immediately and proceed with the rendering. const sources = [this.registerCustomModules(Quill, customModules)]; if (beforeRender) { sources.push(from(beforeRender())); } return forkJoin(sources).pipe(map(() => Quill)); } /** @internal */ registerCustomModules(Quill, customModules, suppressGlobalRegisterWarning) { if (!Array.isArray(customModules)) { return of(Quill); } const sources = []; for (const customModule of customModules) { const { path, implementation: maybeImplementation } = customModule; // If the module is already registered, proceed to the next module... if (this.registeredModules.has(path)) { continue; } this.registeredModules.add(path); if (isObservable(maybeImplementation)) { // If the implementation is an observable, we will wait for it to load and // then register it with Quill. The caller will wait until the module is registered. sources.push(maybeImplementation.pipe(tap((implementation) => { Quill.register(path, implementation, suppressGlobalRegisterWarning); }))); } else { Quill.register(path, maybeImplementation, suppressGlobalRegisterWarning); } } return sources.length > 0 ? forkJoin(sources).pipe(map(() => Quill)) : of(Quill); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class QuillEditorBase { constructor() { this.format = input(undefined); this.theme = input(undefined); this.modules = input(undefined); this.debug = input(false); this.readOnly = input(false); this.placeholder = input(undefined); this.maxLength = input(undefined); this.minLength = input(undefined); this.required = input(false); this.formats = input(undefined); this.customToolbarPosition = input('top'); this.sanitize = input(undefined); this.beforeRender = input(undefined); this.styles = input(null); this.registry = input(undefined); this.bounds = input(undefined); this.customOptions = input([]); this.customModules = input([]); this.trackChanges = input(undefined); this.classes = input(undefined); this.trimOnValidation = input(false); this.linkPlaceholder = input(undefined); this.compareValues = input(false); this.filterNull = input(false); this.debounceTime = input(undefined); /* https://github.com/KillerCodeMonkey/ngx-quill/issues/1257 - fix null value set provide default empty value by default null e.g. defaultEmptyValue="" - empty string <quill-editor defaultEmptyValue="" formControlName="message" ></quill-editor> */ this.defaultEmptyValue = input(null); this.onEditorCreated = new EventEmitter(); this.onEditorChanged = new EventEmitter(); this.onContentChanged = new EventEmitter(); this.onSelectionChanged = new EventEmitter(); this.onFocus = new EventEmitter(); this.onBlur = new EventEmitter(); this.onNativeFocus = new EventEmitter(); this.onNativeBlur = new EventEmitter(); this.disabled = false; // used to store initial value before ViewInit this.toolbarPosition = signal('top'); this.subscription = null; this.quillSubscription = null; this.elementRef = inject(ElementRef); this.document = inject(DOCUMENT); this.cd = inject(ChangeDetectorRef); this.domSanitizer = inject(DomSanitizer); this.platformId = inject(PLATFORM_ID); this.renderer = inject(Renderer2); this.zone = inject(NgZone); this.service = inject(QuillService); this.destroyRef = inject(DestroyRef); this.valueGetter = input((quillEditor) => { let html = quillEditor.getSemanticHTML(); if (this.isEmptyValue(html)) { html = this.defaultEmptyValue(); } let modelValue = html; const format = getFormat(this.format(), this.service.config.format); if (format === 'text') { modelValue = quillEditor.getText(); } else if (format === 'object') { modelValue = quillEditor.getContents(); } else if (format === 'json') { try { modelValue = JSON.stringify(quillEditor.getContents()); } catch { modelValue = quillEditor.getText(); } } return modelValue; }); this.valueSetter = input((quillEditor, value) => { const format = getFormat(this.format(), this.service.config.format); if (format === 'html') { const sanitize = [true, false].includes(this.sanitize()) ? this.sanitize() : (this.service.config.sanitize || false); if (sanitize) { value = this.domSanitizer.sanitize(SecurityContext.HTML, value); } return quillEditor.clipboard.convert({ html: value }); } else if (format === 'json') { try { return JSON.parse(value); } catch { return [{ insert: value }]; } } return value; }); this.selectionChangeHandler = (range, oldRange, source) => { const trackChanges = this.trackChanges() || this.service.config.trackChanges; const shouldTriggerOnModelTouched = !range && !!this.onModelTouched && (source === 'user' || trackChanges && trackChanges === 'all'); // only emit changes when there's any listener if (!this.onBlur.observed && !this.onFocus.observed && !this.onSelectionChanged.observed && !shouldTriggerOnModelTouched) { return; } this.zone.run(() => { if (range === null) { this.onBlur.emit({ editor: this.quillEditor, source }); } else if (oldRange === null) { this.onFocus.emit({ editor: this.quillEditor, source }); } this.onSelectionChanged.emit({ editor: this.quillEditor, oldRange, range, source }); if (shouldTriggerOnModelTouched) { this.onModelTouched(); } this.cd.markForCheck(); }); }; this.textChangeHandler = (delta, oldDelta, source) => { // only emit changes emitted by user interactions const text = this.quillEditor.getText(); const content = this.quillEditor.getContents(); let html = this.quillEditor.getSemanticHTML(); if (this.isEmptyValue(html)) { html = this.defaultEmptyValue(); } const trackChanges = this.trackChanges() || this.service.config.trackChanges; const shouldTriggerOnModelChange = (source === 'user' || trackChanges && trackChanges === 'all') && !!this.onModelChange; // only emit changes when there's any listener if (!this.onContentChanged.observed && !shouldTriggerOnModelChange) { return; } this.zone.run(() => { if (shouldTriggerOnModelChange) { const valueGetter = this.valueGetter(); this.onModelChange(valueGetter(this.quillEditor)); } this.onContentChanged.emit({ content, delta, editor: this.quillEditor, html, oldDelta, source, text }); this.cd.markForCheck(); }); }; this.editorChangeHandler = (event, current, old, source) => { // only emit changes when there's any listener if (!this.onEditorChanged.observed) { return; } // only emit changes emitted by user interactions if (event === 'text-change') { const text = this.quillEditor.getText(); const content = this.quillEditor.getContents(); let html = this.quillEditor.getSemanticHTML(); if (this.isEmptyValue(html)) { html = this.defaultEmptyValue(); } this.zone.run(() => { this.onEditorChanged.emit({ content, delta: current, editor: this.quillEditor, event, html, oldDelta: old, source, text }); this.cd.markForCheck(); }); } else { this.zone.run(() => { this.onEditorChanged.emit({ editor: this.quillEditor, event, oldRange: old, range: current, source }); this.cd.markForCheck(); }); } }; } static normalizeClassNames(classes) { const classList = classes.trim().split(' '); return classList.reduce((prev, cur) => { const trimmed = cur.trim(); if (trimmed) { prev.push(trimmed); } return prev; }, []); } ngOnInit() { this.toolbarPosition.set(this.customToolbarPosition()); } ngAfterViewInit() { if (isPlatformServer(this.platformId)) { return; } // The `quill-editor` component might be destroyed before the `quill` chunk is loaded and its code is executed // this will lead to runtime exceptions, since the code will be executed on DOM nodes that don't exist within the tree. this.quillSubscription = this.service.getQuill().pipe(mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))).subscribe(Quill => { this.editorElem = this.elementRef.nativeElement.querySelector('[quill-editor-element]'); const toolbarElem = this.elementRef.nativeElement.querySelector('[quill-editor-toolbar]'); const modules = Object.assign({}, this.modules() || this.service.config.modules); if (toolbarElem) { modules.toolbar = toolbarElem; } else if (modules.toolbar === undefined) { modules.toolbar = defaultModules.toolbar; } let placeholder = this.placeholder() !== undefined ? this.placeholder() : this.service.config.placeholder; if (placeholder === undefined) { placeholder = 'Insert text here ...'; } const styles = this.styles(); if (styles) { Object.keys(styles).forEach((key) => { this.renderer.setStyle(this.editorElem, key, styles[key]); }); } if (this.classes()) { this.addClasses(this.classes()); } this.customOptions().forEach((customOption) => { const newCustomOption = Quill.import(customOption.import); newCustomOption.whitelist = customOption.whitelist; Quill.register(newCustomOption, true); }); let bounds = this.bounds() && this.bounds() === 'self' ? this.editorElem : this.bounds(); if (!bounds) { bounds = this.service.config.bounds ? this.service.config.bounds : this.document.body; } let debug = this.debug(); if (!debug && debug !== false && this.service.config.debug) { debug = this.service.config.debug; } let readOnly = this.readOnly(); if (!readOnly && this.readOnly() !== false) { readOnly = this.service.config.readOnly !== undefined ? this.service.config.readOnly : false; } let formats = this.formats(); if (!formats && formats === undefined) { formats = this.service.config.formats ? [...this.service.config.formats] : (this.service.config.formats === null ? null : undefined); } this.zone.runOutsideAngular(() => { this.quillEditor = new Quill(this.editorElem, { bounds, debug, formats, modules, placeholder, readOnly, registry: this.registry(), theme: this.theme() || (this.service.config.theme ? this.service.config.theme : 'snow') }); if (this.onNativeBlur.observed) { // https://github.com/quilljs/quill/issues/2186#issuecomment-533401328 fromEvent(this.quillEditor.scroll.domNode, 'blur').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeBlur.next({ editor: this.quillEditor, source: 'dom' })); // https://github.com/quilljs/quill/issues/2186#issuecomment-803257538 const toolbar = this.quillEditor.getModule('toolbar'); if (toolbar.container) { fromEvent(toolbar.container, 'mousedown').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(e => e.preventDefault()); } } if (this.onNativeFocus.observed) { fromEvent(this.quillEditor.scroll.domNode, 'focus').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeFocus.next({ editor: this.quillEditor, source: 'dom' })); } // Set optional link placeholder, Quill has no native API for it so using workaround if (this.linkPlaceholder()) { const tooltip = this.quillEditor?.theme?.tooltip; const input = tooltip?.root?.querySelector('input[data-link]'); if (input?.dataset) { input.dataset.link = this.linkPlaceholder(); } } }); if (this.content) { const format = getFormat(this.format(), this.service.config.format); if (format === 'text') { this.quillEditor.setText(this.content, 'silent'); } else { const valueSetter = this.valueSetter(); const newValue = valueSetter(this.quillEditor, this.content); this.quillEditor.setContents(newValue, 'silent'); } const history = this.quillEditor.getModule('history'); history.clear(); } // initialize disabled status based on this.disabled as default value this.setDisabledState(); this.addQuillEventListeners(); // The `requestAnimationFrame` triggers change detection. There's no sense to invoke the `requestAnimationFrame` if anyone is // listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)="...">`. if (!this.onEditorCreated.observed && !this.onValidatorChanged) { return; } // The `requestAnimationFrame` will trigger change detection and `onEditorCreated` will also call `markDirty()` // internally, since Angular wraps template event listeners into `listener` instruction. We're using the `requestAnimationFrame` // to prevent the frame drop and avoid `ExpressionChangedAfterItHasBeenCheckedError` error. raf$().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { if (this.onValidatorChanged) { this.onValidatorChanged(); } this.onEditorCreated.emit(this.quillEditor); }); }); } ngOnDestroy() { this.dispose(); this.quillSubscription?.unsubscribe(); this.quillSubscription = null; } ngOnChanges(changes) { if (!this.quillEditor) { return; } if (changes.readOnly) { this.quillEditor.enable(!changes.readOnly.currentValue); } if (changes.placeholder) { this.quillEditor.root.dataset.placeholder = changes.placeholder.currentValue; } if (changes.styles) { const currentStyling = changes.styles.currentValue; const previousStyling = changes.styles.previousValue; if (previousStyling) { Object.keys(previousStyling).forEach((key) => { this.renderer.removeStyle(this.editorElem, key); }); } if (currentStyling) { Object.keys(currentStyling).forEach((key) => { this.renderer.setStyle(this.editorElem, key, this.styles()[key]); }); } } if (changes.classes) { const currentClasses = changes.classes.currentValue; const previousClasses = changes.classes.previousValue; if (previousClasses) { this.removeClasses(previousClasses); } if (currentClasses) { this.addClasses(currentClasses); } } // We'd want to re-apply event listeners if the `debounceTime` binding changes to apply the // `debounceTime` operator or vice-versa remove it. if (changes.debounceTime) { this.addQuillEventListeners(); } } addClasses(classList) { QuillEditorBase.normalizeClassNames(classList).forEach((c) => { this.renderer.addClass(this.editorElem, c); }); } removeClasses(classList) { QuillEditorBase.normalizeClassNames(classList).forEach((c) => { this.renderer.removeClass(this.editorElem, c); }); } writeValue(currentValue) { // optional fix for https://github.com/angular/angular/issues/14988 if (this.filterNull() && currentValue === null) { return; } this.content = currentValue; if (!this.quillEditor) { return; } const format = getFormat(this.format(), this.service.config.format); const valueSetter = this.valueSetter(); const newValue = valueSetter(this.quillEditor, currentValue); if (this.compareValues()) { const currentEditorValue = this.quillEditor.getContents(); if (JSON.stringify(currentEditorValue) === JSON.stringify(newValue)) { return; } } if (currentValue) { if (format === 'text') { this.quillEditor.setText(currentValue); } else { this.quillEditor.setContents(newValue); } return; } this.quillEditor.setText(''); } setDisabledState(isDisabled = this.disabled) { // store initial value to set appropriate disabled status after ViewInit this.disabled = isDisabled; if (this.quillEditor) { if (isDisabled) { this.quillEditor.disable(); this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'disabled'); } else { if (!this.readOnly()) { this.quillEditor.enable(); } this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled'); } } } registerOnChange(fn) { this.onModelChange = fn; } registerOnTouched(fn) { this.onModelTouched = fn; } registerOnValidatorChange(fn) { this.onValidatorChanged = fn; } validate() { if (!this.quillEditor) { return null; } const err = {}; let valid = true; const text = this.quillEditor.getText(); // trim text if wanted + handle special case that an empty editor contains a new line const textLength = this.trimOnValidation() ? text.trim().length : (text.length === 1 && text.trim().length === 0 ? 0 : text.length - 1); const deltaOperations = this.quillEditor.getContents().ops; const onlyEmptyOperation = !!deltaOperations && deltaOperations.length === 1 && ['\n', ''].includes(deltaOperations[0].insert?.toString()); if (this.minLength() && textLength && textLength < this.minLength()) { err.minLengthError = { given: textLength, minLength: this.minLength() }; valid = false; } if (this.maxLength() && textLength > this.maxLength()) { err.maxLengthError = { given: textLength, maxLength: this.maxLength() }; valid = false; } if (this.required() && !textLength && onlyEmptyOperation) { err.requiredError = { empty: true }; valid = false; } return valid ? null : err; } addQuillEventListeners() { this.dispose(); // We have to enter the `<root>` zone when adding event listeners, so `debounceTime` will spawn the // `AsyncAction` there w/o triggering change detections. We still re-enter the Angular's zone through // `zone.run` when we emit an event to the parent component. this.zone.runOutsideAngular(() => { this.subscription = new Subscription(); this.subscription.add( // mark model as touched if editor lost focus fromEvent(this.quillEditor, 'selection-change').subscribe(([range, oldRange, source]) => { this.selectionChangeHandler(range, oldRange, source); })); // The `fromEvent` supports passing JQuery-style event targets, the editor has `on` and `off` methods which // will be invoked upon subscription and teardown. let textChange$ = fromEvent(this.quillEditor, 'text-change'); let editorChange$ = fromEvent(this.quillEditor, 'editor-change'); if (typeof this.debounceTime() === 'number') { textChange$ = textChange$.pipe(debounceTime(this.debounceTime())); editorChange$ = editorChange$.pipe(debounceTime(this.debounceTime())); } this.subscription.add( // update model if text changes textChange$.subscribe(([delta, oldDelta, source]) => { this.textChangeHandler(delta, oldDelta, source); })); this.subscription.add( // triggered if selection or text changed editorChange$.subscribe(([event, current, old, source]) => { this.editorChangeHandler(event, current, old, source); })); }); } dispose() { if (this.subscription !== null) { this.subscription.unsubscribe(); this.subscription = null; } } isEmptyValue(html) { return html === '<p></p>' || html === '<div></div>' || html === '<p><br></p>' || html === '<div><br></div>'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.0", type: QuillEditorBase, isStandalone: true, inputs: { format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, modules: { classPropertyName: "modules", publicName: "modules", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, formats: { classPropertyName: "formats", publicName: "formats", isSignal: true, isRequired: false, transformFunction: null }, customToolbarPosition: { classPropertyName: "customToolbarPosition", publicName: "customToolbarPosition", isSignal: true, isRequired: false, transformFunction: null }, sanitize: { classPropertyName: "sanitize", publicName: "sanitize", isSignal: true, isRequired: false, transformFunction: null }, beforeRender: { classPropertyName: "beforeRender", publicName: "beforeRender", isSignal: true, isRequired: false, transformFunction: null }, styles: { classPropertyName: "styles", publicName: "styles", isSignal: true, isRequired: false, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: false, transformFunction: null }, bounds: { classPropertyName: "bounds", publicName: "bounds", isSignal: true, isRequired: false, transformFunction: null }, customOptions: { classPropertyName: "customOptions", publicName: "customOptions", isSignal: true, isRequired: false, transformFunction: null }, customModules: { classPropertyName: "customModules", publicName: "customModules", isSignal: true, isRequired: false, transformFunction: null }, trackChanges: { classPropertyName: "trackChanges", publicName: "trackChanges", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null }, trimOnValidation: { classPropertyName: "trimOnValidation", publicName: "trimOnValidation", isSignal: true, isRequired: false, transformFunction: null }, linkPlaceholder: { classPropertyName: "linkPlaceholder", publicName: "linkPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, compareValues: { classPropertyName: "compareValues", publicName: "compareValues", isSignal: true, isRequired: false, transformFunction: null }, filterNull: { classPropertyName: "filterNull", publicName: "filterNull", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, defaultEmptyValue: { classPropertyName: "defaultEmptyValue", publicName: "defaultEmptyValue", isSignal: true, isRequired: false, transformFunction: null }, valueGetter: { classPropertyName: "valueGetter", publicName: "valueGetter", isSignal: true, isRequired: false, transformFunction: null }, valueSetter: { classPropertyName: "valueSetter", publicName: "valueSetter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onEditorCreated: "onEditorCreated", onEditorChanged: "onEditorChanged", onContentChanged: "onContentChanged", onSelectionChanged: "onSelectionChanged", onFocus: "onFocus", onBlur: "onBlur", onNativeFocus: "onNativeFocus", onNativeBlur: "onNativeBlur" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillEditorBase, decorators: [{ type: Directive }], propDecorators: { onEditorCreated: [{ type: Output }], onEditorChanged: [{ type: Output }], onContentChanged: [{ type: Output }], onSelectionChanged: [{ type: Output }], onFocus: [{ type: Output }], onBlur: [{ type: Output }], onNativeFocus: [{ type: Output }], onNativeBlur: [{ type: Output }] } }); class QuillEditorComponent extends QuillEditorBase { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.0", type: QuillEditorComponent, isStandalone: true, selector: "quill-editor", providers: [ { multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => QuillEditorComponent) }, { multi: true, provide: NG_VALIDATORS, useExisting: forwardRef(() => QuillEditorComponent) } ], usesInheritance: true, ngImport: i0, template: ` @if (toolbarPosition() !== 'top') { <div quill-editor-element></div> } <ng-content select="[above-quill-editor-toolbar]"></ng-content> <ng-content select="[quill-editor-toolbar]"></ng-content> <ng-content select="[below-quill-editor-toolbar]"></ng-content> @if (toolbarPosition() === 'top') { <div quill-editor-element></div> } `, isInline: true, styles: [":host{display:inline-block}\n"] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillEditorComponent, decorators: [{ type: Component, args: [{ encapsulation: ViewEncapsulation.Emulated, providers: [ { multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => QuillEditorComponent) }, { multi: true, provide: NG_VALIDATORS, useExisting: forwardRef(() => QuillEditorComponent) } ], selector: 'quill-editor', template: ` @if (toolbarPosition() !== 'top') { <div quill-editor-element></div> } <ng-content select="[above-quill-editor-toolbar]"></ng-content> <ng-content select="[quill-editor-toolbar]"></ng-content> <ng-content select="[below-quill-editor-toolbar]"></ng-content> @if (toolbarPosition() === 'top') { <div quill-editor-element></div> } `, styles: [":host{display:inline-block}\n"] }] }] }); class QuillViewHTMLComponent { constructor(sanitizer, service) { this.sanitizer = sanitizer; this.service = service; this.content = input(''); this.theme = input(undefined); this.sanitize = input(undefined); this.innerHTML = signal(''); this.themeClass = signal('ql-snow'); } ngOnChanges(changes) { if (changes.theme) { const theme = changes.theme.currentValue || (this.service.config.theme ? this.service.config.theme : 'snow'); this.themeClass.set(`ql-${theme} ngx-quill-view-html`); } else if (!this.theme()) { const theme = this.service.config.theme ? this.service.config.theme : 'snow'; this.themeClass.set(`ql-${theme} ngx-quill-view-html`); } if (changes.content) { const content = changes.content.currentValue; const sanitize = [true, false].includes(this.sanitize()) ? this.sanitize() : (this.service.config.sanitize || false); const innerHTML = sanitize ? content : this.sanitizer.bypassSecurityTrustHtml(content); this.innerHTML.set(innerHTML); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillViewHTMLComponent, deps: [{ token: i1.DomSanitizer }, { token: QuillService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.1.0", type: QuillViewHTMLComponent, isStandalone: true, selector: "quill-view-html", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, sanitize: { classPropertyName: "sanitize", publicName: "sanitize", isSignal: true, isRequired: false, transformFunction: null } }, usesOnChanges: true, ngImport: i0, template: ` <div class="ql-container" [ngClass]="themeClass()"> <div class="ql-editor" [innerHTML]="innerHTML()"> </div> </div> `, isInline: true, styles: [".ql-container.ngx-quill-view-html{border:0}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillViewHTMLComponent, decorators: [{ type: Component, args: [{ imports: [NgClass], encapsulation: ViewEncapsulation.None, selector: 'quill-view-html', template: ` <div class="ql-container" [ngClass]="themeClass()"> <div class="ql-editor" [innerHTML]="innerHTML()"> </div> </div> `, styles: [".ql-container.ngx-quill-view-html{border:0}\n"] }] }], ctorParameters: () => [{ type: i1.DomSanitizer }, { type: QuillService }] }); class QuillViewComponent { constructor(elementRef, renderer, zone, service, domSanitizer, platformId) { this.elementRef = elementRef; this.renderer = renderer; this.zone = zone; this.service = service; this.domSanitizer = domSanitizer; this.platformId = platformId; this.format = input(undefined); this.theme = input(undefined); this.modules = input(undefined); this.debug = input(false); this.formats = input(undefined); this.sanitize = input(undefined); this.beforeRender = input(); this.strict = input(true); this.content = input(); this.customModules = input([]); this.customOptions = input([]); this.onEditorCreated = new EventEmitter(); this.quillSubscription = null; this.destroyRef = inject(DestroyRef); this.valueSetter = (quillEditor, value) => { const format = getFormat(this.format(), this.service.config.format); let content = value; if (format === 'text') { quillEditor.setText(content); } else { if (format === 'html') { const sanitize = [true, false].includes(this.sanitize()) ? this.sanitize() : (this.service.config.sanitize || false); if (sanitize) { value = this.domSanitizer.sanitize(SecurityContext.HTML, value); } content = quillEditor.clipboard.convert({ html: value }); } else if (format === 'json') { try { content = JSON.parse(value); } catch { content = [{ insert: value }]; } } quillEditor.setContents(content); } }; } ngOnChanges(changes) { if (!this.quillEditor) { return; } if (changes.content) { this.valueSetter(this.quillEditor, changes.content.currentValue); } } ngAfterViewInit() { if (isPlatformServer(this.platformId)) { return; } this.quillSubscription = this.service.getQuill().pipe(mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))).subscribe(Quill => { const modules = Object.assign({}, this.modules() || this.service.config.modules); modules.toolbar = false; this.customOptions().forEach((customOption) => { const newCustomOption = Quill.import(customOption.import); newCustomOption.whitelist = customOption.whitelist; Quill.register(newCustomOption, true); }); let debug = this.debug(); if (!debug && debug !== false && this.service.config.debug) { debug = this.service.config.debug; } let formats = this.formats(); if (!formats && formats === undefined) { formats = this.service.config.formats ? [...this.service.config.formats] : (this.service.config.formats === null ? null : undefined); } const theme = this.theme() || (this.service.config.theme ? this.service.config.theme : 'snow'); this.editorElem = this.elementRef.nativeElement.querySelector('[quill-view-element]'); this.zone.runOutsideAngular(() => { this.quillEditor = new Quill(this.editorElem, { debug, formats, modules, readOnly: true, strict: this.strict(), theme }); }); this.renderer.addClass(this.editorElem, 'ngx-quill-view'); if (this.content()) { this.valueSetter(this.quillEditor, this.content()); } // The `requestAnimationFrame` triggers change detection. There's no sense to invoke the `requestAnimationFrame` if anyone is // listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)="...">`. if (!this.onEditorCreated.observed) { return; } // The `requestAnimationFrame` will trigger change detection and `onEditorCreated` will also call `markDirty()` // internally, since Angular wraps template event listeners into `listener` instruction. We're using the `requestAnimationFrame` // to prevent the frame drop and avoid `ExpressionChangedAfterItHasBeenCheckedError` error. raf$().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this.onEditorCreated.emit(this.quillEditor); }); }); } ngOnDestroy() { this.quillSubscription?.unsubscribe(); this.quillSubscription = null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillViewComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: QuillService }, { token: i1.DomSanitizer }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.1.0", type: QuillViewComponent, isStandalone: true, selector: "quill-view", inputs: { format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, modules: { classPropertyName: "modules", publicName: "modules", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, formats: { classPropertyName: "formats", publicName: "formats", isSignal: true, isRequired: false, transformFunction: null }, sanitize: { classPropertyName: "sanitize", publicName: "sanitize", isSignal: true, isRequired: false, transformFunction: null }, beforeRender: { classPropertyName: "beforeRender", publicName: "beforeRender", isSignal: true, isRequired: false, transformFunction: null }, strict: { classPropertyName: "strict", publicName: "strict", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, customModules: { classPropertyName: "customModules", publicName: "customModules", isSignal: true, isRequired: false, transformFunction: null }, customOptions: { classPropertyName: "customOptions", publicName: "customOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onEditorCreated: "onEditorCreated" }, usesOnChanges: true, ngImport: i0, template: ` <div quill-view-element></div> `, isInline: true, styles: [".ql-container.ngx-quill-view{border:0}\n"], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillViewComponent, decorators: [{ type: Component, args: [{ encapsulation: ViewEncapsulation.None, selector: 'quill-view', template: ` <div quill-view-element></div> `, styles: [".ql-container.ngx-quill-view{border:0}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: QuillService }, { type: i1.DomSanitizer }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }], propDecorators: { onEditorCreated: [{ type: Output }] } }); class QuillModule { static forRoot(config) { return { ngModule: QuillModule, providers: [ { provide: QUILL_CONFIG_TOKEN, useValue: config } ] }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.1.0", ngImport: i0, type: QuillModule, imports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent], exports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.0", ngImport: i0, type: QuillModule, decorators: [{ type: NgModule, args: [{ imports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent], exports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent], }] }] }); /* * Public API Surface of ngx-quill */ // Re-export everything from the secondary entry-point so we can be backwards-compatible // and don't introduce breaking changes for consumers. /** * Generated bundle index. Do not edit. */ export { QuillEditorBase, QuillEditorComponent, QuillModule, QuillService, QuillViewComponent, QuillViewHTMLComponent }; //# sourceMappingURL=ngx-quill.mjs.map