UNPKG

ngx-quill

Version:

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

890 lines (878 loc) 54.9 kB
import { defaultModules, QUILL_CONFIG_TOKEN } from 'ngx-quill/config'; export * from 'ngx-quill/config'; import * as i0 from '@angular/core'; import { inject, Injectable, input, booleanAttribute, EventEmitter, signal, ElementRef, PLATFORM_ID, Renderer2, DestroyRef, SecurityContext, afterNextRender, effect, Output, Directive, forwardRef, ChangeDetectionStrategy, ViewEncapsulation, Component, computed, NgModule } from '@angular/core'; import { isPlatformServer } from '@angular/common'; import { DomSanitizer } from '@angular/platform-browser'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { defer, forkJoin, of, isObservable, fromEvent, Subscription, debounceTime } from 'rxjs'; import { shareReplay, map, tap, mergeMap } from 'rxjs/operators'; import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'; const getFormat = (format, configFormat) => { const passedFormat = format || configFormat; return passedFormat || 'html'; }; class QuillService { constructor() { this.config = inject(QUILL_CONFIG_TOKEN) || { modules: defaultModules }; this.quill$ = defer(async () => { if (!this.Quill) { const { Quill } = await import('./ngx-quill-quill-CUw8Q_m0.mjs'); this.Quill = Quill; } // 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); }); // Use `Promise` directly to avoid bundling `firstValueFrom`. return new Promise(resolve => { this.registerCustomModules(this.Quill, this.config.customModules, this.config.suppressGlobalRegisterWarning).subscribe(resolve); }); }).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(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: "21.0.0", ngImport: i0, type: QuillService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class QuillEditorBase { constructor() { this.format = input(...(ngDevMode ? [undefined, { debugName: "format" }] : [])); this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : [])); this.modules = input(...(ngDevMode ? [undefined, { debugName: "modules" }] : [])); this.debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : [])); this.readOnly = input(false, ...(ngDevMode ? [{ debugName: "readOnly", transform: booleanAttribute }] : [{ transform: booleanAttribute }])); this.placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : [])); this.maxLength = input(...(ngDevMode ? [undefined, { debugName: "maxLength" }] : [])); this.minLength = input(...(ngDevMode ? [undefined, { debugName: "minLength" }] : [])); this.required = input(false, ...(ngDevMode ? [{ debugName: "required", transform: booleanAttribute }] : [{ transform: booleanAttribute }])); this.formats = input(...(ngDevMode ? [undefined, { debugName: "formats" }] : [])); this.customToolbarPosition = input('top', ...(ngDevMode ? [{ debugName: "customToolbarPosition" }] : [])); this.sanitize = input(...(ngDevMode ? [undefined, { debugName: "sanitize" }] : [])); this.beforeRender = input(...(ngDevMode ? [undefined, { debugName: "beforeRender" }] : [])); this.styles = input(null, ...(ngDevMode ? [{ debugName: "styles" }] : [])); this.registry = input(...(ngDevMode ? [undefined, { debugName: "registry" }] : [])); this.bounds = input(...(ngDevMode ? [undefined, { debugName: "bounds" }] : [])); this.customOptions = input([], ...(ngDevMode ? [{ debugName: "customOptions" }] : [])); this.customModules = input([], ...(ngDevMode ? [{ debugName: "customModules" }] : [])); this.trackChanges = input(...(ngDevMode ? [undefined, { debugName: "trackChanges" }] : [])); this.classes = input(...(ngDevMode ? [undefined, { debugName: "classes" }] : [])); this.trimOnValidation = input(false, ...(ngDevMode ? [{ debugName: "trimOnValidation", transform: booleanAttribute }] : [{ transform: booleanAttribute }])); this.linkPlaceholder = input(...(ngDevMode ? [undefined, { debugName: "linkPlaceholder" }] : [])); this.compareValues = input(false, ...(ngDevMode ? [{ debugName: "compareValues", transform: booleanAttribute }] : [{ transform: booleanAttribute }])); this.filterNull = input(false, ...(ngDevMode ? [{ debugName: "filterNull", transform: booleanAttribute }] : [{ transform: booleanAttribute }])); this.debounceTime = input(...(ngDevMode ? [undefined, { debugName: "debounceTime" }] : [])); this.onlyFormatEventData = input(false, ...(ngDevMode ? [{ debugName: "onlyFormatEventData" }] : [])); /* 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, ...(ngDevMode ? [{ debugName: "defaultEmptyValue" }] : [])); 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', ...(ngDevMode ? [{ debugName: "toolbarPosition" }] : [])); this.eventsSubscription = null; this.quillSubscription = null; this.elementRef = inject(ElementRef); this.domSanitizer = inject(DomSanitizer); this.platformId = inject(PLATFORM_ID); this.renderer = inject(Renderer2); this.service = inject(QuillService); this.destroyRef = inject(DestroyRef); this.init = false; this.valueGetter = input(this.getter.bind(this), ...(ngDevMode ? [{ debugName: "valueGetter" }] : [])); this.valueSetter = input((quillEditor, value) => { const format = getFormat(this.format(), this.service.config.format); if (format === 'html') { const sanitize = (typeof this.sanitize() === 'boolean') ? 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; }, ...(ngDevMode ? [{ debugName: "valueSetter" }] : [])); 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; } 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.textChangeHandler = (delta, oldDelta, source) => { 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; } const data = this.eventCallbackFormats(); if (shouldTriggerOnModelChange) { this.onModelChange( // only call value getter again if not already done in eventCallbackFormats data.noFormat ? this.valueGetter()(this.quillEditor) : data[data.format]); } this.onContentChanged.emit({ content: data.object, delta, editor: this.quillEditor, html: data.html, oldDelta, source, text: data.text }); }; 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 data = this.eventCallbackFormats(); this.onEditorChanged.emit({ content: data.object, delta: current, editor: this.quillEditor, event, html: data.html, oldDelta: old, source, text: data.text }); } else { this.onEditorChanged.emit({ editor: this.quillEditor, event, oldRange: old, range: current, source }); } }; afterNextRender(() => { 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) { this.previousStyles = styles; Object.keys(styles).forEach((key) => { this.renderer.setStyle(this.editorElem, key, styles[key]); }); } const previousClasses = this.classes(); if (previousClasses) { this.previousClasses = previousClasses; this.addClasses(previousClasses); } 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) { // Can use global `document` because we execute this only in the browser. bounds = this.service.config.bounds ? this.service.config.bounds : 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.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(); // listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)="...">`. if (!this.onEditorCreated.observed && !this.onValidatorChanged) { this.init = true; return; } if (this.onValidatorChanged) { this.onValidatorChanged(); } this.onEditorCreated.emit(this.quillEditor); this.init = true; }); }); effect(() => { const customToolbarPosition = this.customToolbarPosition(); if (this.init && this.toolbarPosition() !== customToolbarPosition) { this.toolbarPosition.set(customToolbarPosition); } }); effect(() => { const readOnly = this.readOnly(); if (this.init) { if (readOnly) { this.quillEditor?.disable(); } else { this.quillEditor?.enable(true); } } }); effect(() => { const placeholder = this.placeholder(); if (this.init && this.quillEditor) { this.quillEditor.root.dataset.placeholder = placeholder; } }); effect(() => { const styles = this.styles(); if (!this.init || !this.editorElem) { return; } const currentStyling = styles; const previousStyling = this.previousStyles; 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, currentStyling[key]); }); } }); effect(() => { const classes = this.classes(); if (!this.init || !this.quillEditor) { return; } const currentClasses = classes; const previousClasses = this.previousClasses; if (previousClasses) { this.removeClasses(previousClasses); } if (currentClasses) { this.addClasses(currentClasses); } }); effect(() => { const debounceTime = this.debounceTime(); if (!this.init || !this.quillEditor) { return; } if (debounceTime) { this.addQuillEventListeners(); } }); this.destroyRef.onDestroy(() => { this.dispose(); this.quillSubscription?.unsubscribe(); this.quillSubscription = null; }); } static normalizeClassNames(classes) { const classList = classes.trim().split(' '); return classList.reduce((prev, cur) => { const trimmed = cur.trim(); if (trimmed) { prev.push(trimmed); } return prev; }, []); } 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() || ''); const minLength = this.minLength(); if (minLength && textLength && textLength < minLength) { err.minLengthError = { given: textLength, minLength }; valid = false; } const maxLength = this.maxLength(); if (maxLength && textLength > maxLength) { err.maxLengthError = { given: textLength, maxLength }; valid = false; } if (this.required() && !textLength && onlyEmptyOperation) { err.requiredError = { empty: true }; valid = false; } return valid ? null : err; } addQuillEventListeners() { this.dispose(); this.eventsSubscription = new Subscription(); this.eventsSubscription.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'); const _debounceTime = this.debounceTime(); if (typeof _debounceTime === 'number') { textChange$ = textChange$.pipe(debounceTime(_debounceTime)); editorChange$ = editorChange$.pipe(debounceTime(_debounceTime)); } this.eventsSubscription.add( // update model if text changes textChange$.subscribe(([delta, oldDelta, source]) => { this.textChangeHandler(delta, oldDelta, source); })); this.eventsSubscription.add( // triggered if selection or text changed editorChange$.subscribe(([event, current, old, source]) => { this.editorChangeHandler(event, current, old, source); })); } dispose() { this.eventsSubscription?.unsubscribe(); this.eventsSubscription = null; } isEmptyValue(html) { return html === '<p></p>' || html === '<div></div>' || html === '<p><br></p>' || html === '<div><br></div>'; } getter(quillEditor, forceFormat) { let modelValue = null; const format = forceFormat ?? getFormat(this.format(), this.service.config.format); if (format === 'html') { let html = quillEditor.getSemanticHTML(); if (this.isEmptyValue(html)) { html = this.defaultEmptyValue(); } modelValue = html; } else 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; } eventCallbackFormats() { const format = getFormat(this.format(), this.service.config.format); const onlyFormat = this.onlyFormatEventData() === true; const noFormat = this.onlyFormatEventData() === 'none'; let text = null; let html = null; let object = null; let json = null; // do nothing if no formatted value needed if (noFormat) { return { format, onlyFormat, noFormat, text, object, json, html }; } // use getter input to grab value const value = this.valueGetter()(this.quillEditor); if (format === 'text') { text = value; } else if (format === 'html') { html = value; } else if (format === 'object') { object = value; json = JSON.stringify(value); } else if (format === 'json') { json = value; object = JSON.parse(value); } // return current values, if only the editor format is needed if (onlyFormat) { return { format, onlyFormat, noFormat, text, json, html, object }; } // return all format values return { format, onlyFormat, noFormat, // use internal getter to retrieve correct other values - this.valueGetter can be overwritten text: format === 'text' ? text : this.getter(this.quillEditor, 'text'), json: format === 'json' ? json : this.getter(this.quillEditor, 'json'), html: format === 'html' ? html : this.getter(this.quillEditor, 'html'), object: format === 'object' ? object : this.getter(this.quillEditor, 'object') }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.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 }, onlyFormatEventData: { classPropertyName: "onlyFormatEventData", publicName: "onlyFormatEventData", 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" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorBase, decorators: [{ type: Directive }], ctorParameters: () => [], propDecorators: { format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], modules: [{ type: i0.Input, args: [{ isSignal: true, alias: "modules", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], minLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minLength", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], formats: [{ type: i0.Input, args: [{ isSignal: true, alias: "formats", required: false }] }], customToolbarPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "customToolbarPosition", required: false }] }], sanitize: [{ type: i0.Input, args: [{ isSignal: true, alias: "sanitize", required: false }] }], beforeRender: [{ type: i0.Input, args: [{ isSignal: true, alias: "beforeRender", required: false }] }], styles: [{ type: i0.Input, args: [{ isSignal: true, alias: "styles", required: false }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], bounds: [{ type: i0.Input, args: [{ isSignal: true, alias: "bounds", required: false }] }], customOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "customOptions", required: false }] }], customModules: [{ type: i0.Input, args: [{ isSignal: true, alias: "customModules", required: false }] }], trackChanges: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackChanges", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }], trimOnValidation: [{ type: i0.Input, args: [{ isSignal: true, alias: "trimOnValidation", required: false }] }], linkPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "linkPlaceholder", required: false }] }], compareValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareValues", required: false }] }], filterNull: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterNull", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], onlyFormatEventData: [{ type: i0.Input, args: [{ isSignal: true, alias: "onlyFormatEventData", required: false }] }], defaultEmptyValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultEmptyValue", required: false }] }], onEditorCreated: [{ type: Output }], onEditorChanged: [{ type: Output }], onContentChanged: [{ type: Output }], onSelectionChanged: [{ type: Output }], onFocus: [{ type: Output }], onBlur: [{ type: Output }], onNativeFocus: [{ type: Output }], onNativeBlur: [{ type: Output }], valueGetter: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueGetter", required: false }] }], valueSetter: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueSetter", required: false }] }] } }); class QuillEditorComponent extends QuillEditorBase { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.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"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.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) } ], changeDetection: ChangeDetectionStrategy.OnPush, 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() { this.content = input('', ...(ngDevMode ? [{ debugName: "content" }] : [])); this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : [])); this.sanitize = input(...(ngDevMode ? [undefined, { debugName: "sanitize" }] : [])); this.innerHTML = computed(() => { const sanitize = this.sanitize(); const content = this.content(); return ((typeof sanitize === 'boolean') ? sanitize : (this.service.config.sanitize || false)) ? content : this.sanitizer.bypassSecurityTrustHtml(content); }, ...(ngDevMode ? [{ debugName: "innerHTML" }] : [])); this.themeClass = computed(() => { const base = this.service.config.theme ? this.service.config.theme : 'snow'; return `ql-${this.theme() || base} ngx-quill-view-html`; }, ...(ngDevMode ? [{ debugName: "themeClass" }] : [])); this.sanitizer = inject(DomSanitizer); this.service = inject(QuillService); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillViewHTMLComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.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 } }, ngImport: i0, template: ` <div class="ql-container" [class]="themeClass()"> <div class="ql-editor" [innerHTML]="innerHTML()"> </div> </div> `, isInline: true, styles: [".ql-container.ngx-quill-view-html{border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillViewHTMLComponent, decorators: [{ type: Component, args: [{ changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, selector: 'quill-view-html', template: ` <div class="ql-container" [class]="themeClass()"> <div class="ql-editor" [innerHTML]="innerHTML()"> </div> </div> `, styles: [".ql-container.ngx-quill-view-html{border:0}\n"] }] }], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], sanitize: [{ type: i0.Input, args: [{ isSignal: true, alias: "sanitize", required: false }] }] } }); class QuillViewComponent { constructor() { this.format = input(...(ngDevMode ? [undefined, { debugName: "format" }] : [])); this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : [])); this.modules = input(...(ngDevMode ? [undefined, { debugName: "modules" }] : [])); this.debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : [])); this.formats = input(...(ngDevMode ? [undefined, { debugName: "formats" }] : [])); this.sanitize = input(...(ngDevMode ? [undefined, { debugName: "sanitize" }] : [])); this.beforeRender = input(...(ngDevMode ? [undefined, { debugName: "beforeRender" }] : [])); this.strict = input(true, ...(ngDevMode ? [{ debugName: "strict" }] : [])); this.content = input(...(ngDevMode ? [undefined, { debugName: "content" }] : [])); this.customModules = input([], ...(ngDevMode ? [{ debugName: "customModules" }] : [])); this.customOptions = input([], ...(ngDevMode ? [{ debugName: "customOptions" }] : [])); this.onEditorCreated = new EventEmitter(); this.init = false; this.elementRef = inject(ElementRef); this.renderer = inject(Renderer2); this.service = inject(QuillService); this.sanitizer = inject(DomSanitizer); this.platformId = inject(PLATFORM_ID); 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 = (typeof this.sanitize() === 'boolean') ? this.sanitize() : (this.service.config.sanitize || false); if (sanitize) { value = this.sanitizer.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); } }; afterNextRender(() => { if (isPlatformServer(this.platformId)) { return; } const 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 === 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.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()); } // listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)="...">`. if (!this.onEditorCreated.observed) { this.init = true; return; } this.onEditorCreated.emit(this.quillEditor); this.init = true; }); this.destroyRef.onDestroy(() => quillSubscription.unsubscribe()); }); effect(() => { const content = this.content(); if (!this.quillEditor || !this.init) { return; } if (content) { this.valueSetter(this.quillEditor, content); } }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.0", type: QuillViewComponent, isStandalone: true, selector: "quill-view", inputs: { format: { classPropertyName: "format", publicName: "format", isSignal