ngx-quill
Version:
Angular components for the easy use of the QuillJS richt text editor.
1 lines • 124 kB
Source Map (JSON)
{"version":3,"file":"ngx-quill.mjs","sources":["../../../projects/ngx-quill/src/lib/quill.service.ts","../../../projects/ngx-quill/src/lib/quill-editor-field.component.ts","../../../projects/ngx-quill/src/lib/helpers.ts","../../../projects/ngx-quill/src/lib/quill-editor.component.ts","../../../projects/ngx-quill/src/lib/quill-view-html.component.ts","../../../projects/ngx-quill/src/lib/quill-view.component.ts","../../../projects/ngx-quill/src/lib/quill.module.ts","../../../projects/ngx-quill/src/public-api.ts","../../../projects/ngx-quill/src/ngx-quill.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core'\nimport { defer, forkJoin, isObservable, Observable, of } from 'rxjs'\nimport { map, shareReplay, tap } from 'rxjs/operators'\n\nimport {\n CustomModule,\n defaultModules,\n QUILL_CONFIG_TOKEN,\n QuillConfig\n} from 'ngx-quill/config'\n\n@Injectable({\n providedIn: 'root',\n})\nexport class QuillService {\n readonly config = inject(QUILL_CONFIG_TOKEN) || { modules:defaultModules } as QuillConfig\n\n private Quill!: any\n\n private quill$: Observable<any> = defer(async () => {\n if (!this.Quill) {\n const { Quill } = await import('./quill')\n this.Quill = Quill\n }\n\n // Only register custom options and modules once\n this.config.customOptions?.forEach((customOption) => {\n const newCustomOption = this.Quill.import(customOption.import)\n newCustomOption.whitelist = customOption.whitelist\n this.Quill.register(\n newCustomOption,\n true,\n this.config.suppressGlobalRegisterWarning\n )\n })\n\n // Use `Promise` directly to avoid bundling `firstValueFrom`.\n return new Promise(resolve => {\n this.registerCustomModules(\n this.Quill,\n this.config.customModules,\n this.config.suppressGlobalRegisterWarning\n ).subscribe(resolve)\n })\n }).pipe(\n shareReplay({\n bufferSize: 1,\n refCount: false\n })\n )\n\n // A list of custom modules that have already been registered,\n // so we don’t need to await their implementation.\n private registeredModules = new Set<string>()\n\n getQuill() {\n return this.quill$\n }\n\n /** @internal */\n beforeRender(Quill: any, customModules: CustomModule[] | undefined, beforeRender = this.config.beforeRender) {\n // This function is called each time the editor needs to be rendered,\n // so it operates individually per component. If no custom module needs to be\n // registered and no `beforeRender` function is provided, it will emit\n // immediately and proceed with the rendering.\n const sources: (Observable<any> | Promise<any>)[] = [this.registerCustomModules(Quill, customModules)]\n if (beforeRender) {\n sources.push(beforeRender())\n }\n return forkJoin(sources).pipe(map(() => Quill))\n }\n\n /** @internal */\n private registerCustomModules(\n Quill: any,\n customModules: CustomModule[] | undefined,\n suppressGlobalRegisterWarning?: boolean\n ) {\n if (!Array.isArray(customModules)) {\n return of(Quill)\n }\n\n const sources: Observable<unknown>[] = []\n\n for (const customModule of customModules) {\n const { path, implementation: maybeImplementation } = customModule\n\n // If the module is already registered, proceed to the next module...\n if (this.registeredModules.has(path)) {\n continue\n }\n\n this.registeredModules.add(path)\n\n if (isObservable(maybeImplementation)) {\n // If the implementation is an observable, we will wait for it to load and\n // then register it with Quill. The caller will wait until the module is registered.\n sources.push(maybeImplementation.pipe(\n tap((implementation) => {\n Quill.register(path, implementation, suppressGlobalRegisterWarning)\n })\n ))\n } else {\n Quill.register(path, maybeImplementation, suppressGlobalRegisterWarning)\n }\n }\n\n return sources.length > 0 ? forkJoin(sources).pipe(map(() => Quill)) : of(Quill)\n }\n}\n","import { isPlatformServer, } from '@angular/common'\nimport {\n afterNextRender,\n booleanAttribute,\n Component,\n DestroyRef,\n Directive,\n effect,\n ElementRef,\n EventEmitter,\n inject,\n input,\n model,\n Output,\n PLATFORM_ID,\n Renderer2,\n SecurityContext,\n signal,\n ViewEncapsulation\n} from '@angular/core'\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop'\nimport { FormValueControl, ValidationResult } from '@angular/forms/signals'\nimport { DomSanitizer } from '@angular/platform-browser'\nimport { CustomModule, CustomOption, defaultModules, QuillBeforeRender, QuillFormat, QuillModules } from 'ngx-quill/config'\nimport type QuillType from 'quill'\nimport type { QuillOptions } from 'quill'\nimport type DeltaType from 'quill-delta'\nimport type History from 'quill/modules/history'\nimport type Toolbar from 'quill/modules/toolbar'\nimport { debounceTime, fromEvent, mergeMap, Subscription } from 'rxjs'\n\nimport { type Blur, type ContentChange, type EditorChangeContent, type EditorChangeSelection, type Focus, type Range, type SelectionChange } from './quill-editor.component'\nimport { QuillService } from './quill.service'\n\nexport enum ValidationKind {\n quillMinLength = 'quillMinLength',\n quillMaxLength = 'quillMaxLength',\n quillRequired = 'quillRequired'\n}\nexport const getFormat = (format?: QuillFormat, configFormat?: QuillFormat): QuillFormat => {\n const passedFormat = format || configFormat\n return passedFormat || 'html'\n}\n\n@Directive()\nexport abstract class QuillEditorFieldBase implements FormValueControl<string | DeltaType | null> {\n\n // Required\n readonly value = model<string | DeltaType | null>(null)\n\n // Writable interaction state - control updates these\n readonly touched = model<boolean>(false)\n\n // Read-only state - form system manages these\n readonly disabled = input(false, { transform: booleanAttribute })\n readonly readonly = input(false, { transform: booleanAttribute })\n\n // Custom component inputs\n readonly format = input<QuillFormat>()\n readonly theme = input<string>()\n readonly modules = input<QuillModules>()\n readonly debug = input<'warn' | 'log' | 'error' | false>(false)\n readonly placeholder = input<string>()\n readonly formats = input<string[] | null>()\n readonly customToolbarPosition = input<'top' | 'bottom'>('top')\n readonly sanitize = input<boolean>()\n readonly beforeRender = input<QuillBeforeRender>()\n readonly styles = input<any>(null)\n readonly registry = input<QuillOptions['registry']>()\n readonly bounds = input<HTMLElement | string>()\n readonly customOptions = input<CustomOption[]>([])\n readonly customModules = input<CustomModule[]>([])\n readonly trackChanges = input<'user' | 'all'>()\n readonly classes = input<string>()\n readonly trimOnValidation = input(false, { transform: booleanAttribute })\n readonly linkPlaceholder = input<string>()\n readonly compareValues = input(false, { transform: booleanAttribute })\n readonly filterNull = input(false, { transform: booleanAttribute })\n readonly debounceTime = input<number>()\n readonly onlyFormatEventData = input<boolean | 'none'>(false)\n /*\n https://github.com/KillerCodeMonkey/ngx-quill/issues/1257 - fix null value set\n\n provide default empty value\n by default null\n\n e.g. defaultEmptyValue=\"\" - empty string\n\n <quill-editor\n defaultEmptyValue=\"\"\n formControlName=\"message\"\n ></quill-editor>\n */\n readonly defaultEmptyValue = input<any>(null)\n\n /* ---------------- DEPENDENCIES ---------------- */\n\n protected quillService = inject(QuillService)\n private elementRef = inject(ElementRef)\n private domSanitizer = inject(DomSanitizer)\n private platformId = inject<string>(PLATFORM_ID)\n private renderer = inject(Renderer2)\n private service = inject(QuillService)\n private destroyRef = inject(DestroyRef)\n\n protected quill!: QuillType\n private previousStyles: any\n private previousClasses: any\n private internalChange = false\n\n private eventsSubscription: Subscription | null = null\n private quillSubscription: Subscription | null = null\n\n readonly quillEditor = signal<QuillType | null>(null)\n editorElem!: HTMLElement\n\n @Output() onEditorCreated = new EventEmitter<QuillType>()\n @Output() onEditorChanged = new EventEmitter<EditorChangeContent | EditorChangeSelection>()\n @Output() onContentChanged = new EventEmitter<ContentChange>()\n @Output() onSelectionChanged = new EventEmitter<SelectionChange>()\n @Output() onFocus = new EventEmitter<Focus>()\n @Output() onBlur = new EventEmitter<Blur>()\n @Output() onNativeFocus = new EventEmitter<Focus>()\n @Output() onNativeBlur = new EventEmitter<Blur>()\n\n readonly toolbarPosition = signal('top')\n\n constructor() {\n afterNextRender(() => {\n if (isPlatformServer(this.platformId)) {\n return\n }\n\n // The `quill-editor` component might be destroyed before the `quill` chunk is loaded and its code is executed\n // this will lead to runtime exceptions, since the code will be executed on DOM nodes that don't exist within the tree.\n\n this.quillSubscription = this.service.getQuill().pipe(\n mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))\n ).subscribe(Quill => {\n this.editorElem = this.elementRef.nativeElement.querySelector(\n '[quill-editor-element]'\n )\n\n const toolbarElem = this.elementRef.nativeElement.querySelector(\n '[quill-editor-toolbar]'\n )\n const modules = Object.assign({}, this.modules() || this.service.config.modules)\n\n if (toolbarElem) {\n modules.toolbar = toolbarElem\n } else if (modules.toolbar === undefined) {\n modules.toolbar = defaultModules.toolbar\n }\n\n let placeholder = this.placeholder() !== undefined ? this.placeholder() : this.service.config.placeholder\n if (placeholder === undefined) {\n placeholder = 'Insert text here ...'\n }\n\n const styles = this.styles()\n if (styles) {\n this.previousStyles = styles\n Object.keys(styles).forEach((key: string) => {\n this.renderer.setStyle(this.editorElem, key, styles[key])\n })\n }\n\n const previousClasses = this.classes()\n if (previousClasses) {\n this.previousClasses =previousClasses\n this.addClasses(previousClasses)\n }\n\n this.customOptions().forEach((customOption) => {\n const newCustomOption = Quill.import(customOption.import)\n newCustomOption.whitelist = customOption.whitelist\n Quill.register(newCustomOption, true)\n })\n\n let bounds = this.bounds() && this.bounds() === 'self' ? this.editorElem : this.bounds()\n if (!bounds) {\n // Can use global `document` because we execute this only in the browser.\n bounds = this.service.config.bounds ? this.service.config.bounds : document.body\n }\n\n let debug = this.debug()\n if (!debug && debug !== false && this.service.config.debug) {\n debug = this.service.config.debug\n }\n\n let readOnly = this.readonly()\n if (!readOnly && readOnly !== false) {\n readOnly = this.service.config.readOnly !== undefined ? this.service.config.readOnly : false\n }\n\n let formats = this.formats()\n if (!formats && formats === undefined) {\n formats = this.service.config.formats ? [...this.service.config.formats] : (this.service.config.formats === null ? null : undefined)\n }\n\n const editor = new Quill(this.editorElem, {\n bounds,\n debug,\n formats,\n modules,\n placeholder,\n readOnly,\n registry: this.registry(),\n theme: this.theme() || (this.service.config.theme ? this.service.config.theme : 'snow')\n })\n\n if (this.onNativeBlur.observed) {\n // https://github.com/quilljs/quill/issues/2186#issuecomment-533401328\n fromEvent(editor.scroll.domNode, 'blur').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeBlur.next({\n editor,\n source: 'dom'\n }))\n // https://github.com/quilljs/quill/issues/2186#issuecomment-803257538\n const toolbar = editor.getModule('toolbar') as Toolbar\n if (toolbar.container) {\n fromEvent(toolbar.container, 'mousedown').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(e => e.preventDefault())\n }\n }\n\n if (this.onNativeFocus.observed) {\n fromEvent(editor.scroll.domNode, 'focus').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeFocus.next({\n editor,\n source: 'dom'\n }))\n }\n\n // Set optional link placeholder, Quill has no native API for it so using workaround\n if (this.linkPlaceholder()) {\n const tooltip = (editor as any)?.theme?.tooltip\n const input = tooltip?.root?.querySelector('input[data-link]')\n if (input?.dataset) {\n input.dataset.link = this.linkPlaceholder()\n }\n }\n const value = this.value()\n\n if (value) {\n this.internalChange = true\n const format = getFormat(this.format(), this.service.config.format)\n\n if (format === 'text') {\n editor.setText(value as string, 'silent')\n } else {\n const valueSetter = this.valueSetter()\n const newValue = valueSetter(editor, value)\n editor.setContents(newValue, 'silent')\n }\n\n const history = editor.getModule('history') as History\n history.clear()\n\n // trigger initial form validation\n this.value.set(value)\n }\n\n // should trigger effects when editor set, setting init disabled, readonly + adds event listeners\n this.quillEditor.set(editor)\n\n // listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)=\"...\">`.\n if (this.onEditorCreated.observed) {\n this.onEditorCreated.emit(editor)\n }\n })\n })\n\n effect(() => {\n const customToolbarPosition = this.customToolbarPosition()\n if (this.toolbarPosition() !== customToolbarPosition) {\n this.toolbarPosition.set(customToolbarPosition)\n }\n })\n\n effect(() => {\n const readonly = this.readonly()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n if (readonly) {\n editor.disable()\n } else {\n editor.enable(true)\n }\n })\n\n effect(() => {\n let placeholder = this.placeholder()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n placeholder = placeholder !== undefined ? placeholder : this.service.config.placeholder\n if (placeholder === undefined) {\n placeholder = 'Insert text here ...'\n }\n editor.root.dataset['placeholder'] = placeholder\n })\n\n effect(() => {\n const styles = this.styles()\n const editor = this.quillEditor()\n if (!this.editorElem || !editor) {\n return\n }\n const currentStyling = styles\n const previousStyling = this.previousStyles\n\n if (previousStyling) {\n Object.keys(previousStyling).forEach((key: string) => {\n this.renderer.removeStyle(this.editorElem, key)\n })\n }\n if (currentStyling) {\n Object.keys(currentStyling).forEach((key: string) => {\n this.renderer.setStyle(this.editorElem, key, currentStyling[key])\n })\n }\n })\n\n effect(() => {\n const classes = this.classes()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n const currentClasses = classes\n const previousClasses = this.previousClasses\n\n if (previousClasses) {\n this.removeClasses(previousClasses)\n }\n\n if (currentClasses) {\n this.addClasses(currentClasses)\n }\n })\n\n effect(() => {\n const debounceTime = this.debounceTime()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n this.addQuillEventListeners(debounceTime)\n })\n\n effect(() => {\n const disabled = this.disabled()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n this.setDisabledState(disabled)\n })\n\n effect(() => {\n const value = this.value()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n if (this.internalChange) {\n this.internalChange = false\n return\n }\n this.writeValue(value)\n })\n\n this.destroyRef.onDestroy(() => {\n this.dispose()\n\n this.quillSubscription?.unsubscribe()\n this.quillSubscription = null\n })\n }\n\n static normalizeClassNames(classes: string): string[] {\n const classList = classes.trim().split(' ')\n return classList.reduce((prev: string[], cur: string) => {\n const trimmed = cur.trim()\n if (trimmed) {\n prev.push(trimmed)\n }\n\n return prev\n }, [])\n }\n\n valueGetter = input(this.getter.bind(this))\n\n valueSetter = input((quillEditor: QuillType, value: string | DeltaType | null): any => {\n const format = getFormat(this.format(), this.service.config.format)\n if (format === 'html') {\n const sanitize = (typeof this.sanitize() === 'boolean') ? this.sanitize() : (this.service.config.sanitize || false)\n if (sanitize) {\n value = this.domSanitizer.sanitize(SecurityContext.HTML, value)\n }\n return quillEditor.clipboard.convert({ html: value as string })\n }\n\n if (format === 'json') {\n try {\n return JSON.parse(value as string) as DeltaType\n } catch {\n return [{ insert: value } as unknown as DeltaType]\n }\n }\n\n return value as DeltaType\n })\n\n selectionChangeHandler = (range: Range | null, oldRange: Range | null, source: string) => {\n const trackChanges = this.trackChanges() || this.service.config.trackChanges\n const shouldTriggerOnModelTouched = !range && (source === 'user' || trackChanges && trackChanges === 'all')\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n\n // only emit changes when there's any listener\n if (!this.onBlur.observed &&\n !this.onFocus.observed &&\n !this.onSelectionChanged.observed &&\n !shouldTriggerOnModelTouched) {\n return\n }\n\n if (range === null) {\n this.onBlur.emit({\n editor,\n source\n })\n } else if (oldRange === null) {\n this.onFocus.emit({\n editor,\n source\n })\n }\n\n this.onSelectionChanged.emit({\n editor,\n oldRange,\n range,\n source\n })\n\n if (shouldTriggerOnModelTouched) {\n this.touched.set(true)\n }\n }\n\n textChangeHandler = (delta: DeltaType, oldDelta: DeltaType, source: string): void => {\n const trackChanges = this.trackChanges() || this.service.config.trackChanges\n const shouldTriggerOnModelChange = (source === 'user' || trackChanges && trackChanges === 'all')\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n\n // only emit changes when there's any listener\n if (!this.onContentChanged.observed && !shouldTriggerOnModelChange) {\n return\n }\n\n const data = this.eventCallbackFormats()\n\n if (shouldTriggerOnModelChange) {\n // do not trigger value set again\n this.internalChange = true\n this.value.set(\n // only call value getter again if not already done in eventCallbackFormats\n data.noFormat ? this.valueGetter()(editor) : data[data.format]\n )\n }\n\n if (this.onContentChanged.observed) {\n this.onContentChanged.emit({\n content: data.object,\n delta,\n editor,\n html: data.html,\n oldDelta,\n source,\n text: data.text\n })\n }\n }\n\n editorChangeHandler = (\n event: 'text-change' | 'selection-change',\n current: any | Range | null, old: any | Range | null, source: string\n ): void => {\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n // only emit changes when there's any listener\n if (!this.onEditorChanged.observed) {\n return\n }\n\n // only emit changes emitted by user interactions\n if (event === 'text-change') {\n const data = this.eventCallbackFormats()\n\n this.onEditorChanged.emit({\n content: data.object,\n delta: current,\n editor,\n event,\n html: data.html,\n oldDelta: old,\n source,\n text: data.text\n })\n } else {\n this.onEditorChanged.emit({\n editor,\n event,\n oldRange: old,\n range: current,\n source\n })\n }\n }\n\n addClasses(classList: string): void {\n QuillEditorFieldBase.normalizeClassNames(classList).forEach((c: string) => {\n this.renderer.addClass(this.editorElem, c)\n })\n }\n\n removeClasses(classList: string): void {\n QuillEditorFieldBase.normalizeClassNames(classList).forEach((c: string) => {\n this.renderer.removeClass(this.editorElem, c)\n })\n }\n\n writeValue(currentValue: string | DeltaType | null) {\n const editor = this.quillEditor()\n // optional fix for https://github.com/angular/angular/issues/14988\n if (this.filterNull() && currentValue === null) {\n return\n }\n if (!editor) {\n return\n }\n\n const format = getFormat(this.format(), this.service.config.format)\n const valueSetter = this.valueSetter()\n const newValue = valueSetter(editor, currentValue)\n\n if (this.compareValues()) {\n const currentEditorValue = editor.getContents()\n\n if (!currentEditorValue.diff(newValue).changeLength()) {\n return\n }\n }\n\n if (currentValue) {\n if (format === 'text') {\n editor.setText(currentValue as string)\n } else {\n editor.setContents(newValue)\n }\n return\n }\n\n editor.setText('')\n }\n\n setDisabledState(isDisabled: boolean): void {\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n\n if (isDisabled) {\n editor.disable()\n this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'disabled')\n } else {\n if (!this.readonly()) {\n editor.enable()\n }\n this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled')\n }\n }\n\n validate({\n required = false,\n minLength,\n maxLength\n }: {\n required?: boolean\n minLength?: number\n maxLength?: number\n }): ValidationResult<{\n kind: ValidationKind\n message?: string\n }> | null {\n const editor = this.quillEditor()\n if (!editor || (!required && !minLength && !maxLength)) {\n return null\n }\n\n const text = editor.getText()\n // trim text if wanted + handle special case that an empty editor contains a new line\n const textLength = this.trimOnValidation() ? text.trim().length : (text.length === 1 && text.trim().length === 0 ? 0 : text.length - 1)\n const deltaOperations = editor.getContents().ops\n const onlyEmptyOperation = !!deltaOperations && deltaOperations.length === 1 && ['\\n', ''].includes(deltaOperations[0].insert?.toString() || '')\n const errors:{\n kind: ValidationKind\n message?: string\n }[] = []\n\n if (minLength && textLength && textLength < minLength) {\n errors.push({\n kind: ValidationKind.quillMinLength,\n message: `text length: ${textLength}, min length: ${minLength}`\n })\n }\n\n if (maxLength && textLength > maxLength) {\n errors.push({\n kind: ValidationKind.quillMaxLength,\n message: `text length: ${textLength}, min length: ${maxLength}`\n })\n }\n\n if (required && !textLength && onlyEmptyOperation) {\n errors.push({\n kind: ValidationKind.quillRequired,\n message: `text length: ${textLength}`,\n })\n }\n\n return errors.length ? errors : null\n }\n\n focus() {\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n\n editor.focus()\n }\n\n private addQuillEventListeners(dbt?: number | null): void {\n this.dispose()\n const editor = this.quillEditor()\n if (!editor) {\n return\n }\n\n this.eventsSubscription = new Subscription()\n\n this.eventsSubscription.add(\n // mark model as touched if editor lost focus\n fromEvent(editor, 'selection-change').subscribe(\n ([range, oldRange, source]) => {\n this.selectionChangeHandler(range as any, oldRange as any, source)\n }\n )\n )\n\n // The `fromEvent` supports passing JQuery-style event targets, the editor has `on` and `off` methods which\n // will be invoked upon subscription and teardown.\n let textChange$ = fromEvent(editor, 'text-change')\n let editorChange$ = fromEvent(editor, 'editor-change')\n\n if (typeof dbt === 'number') {\n textChange$ = textChange$.pipe(debounceTime(dbt))\n editorChange$ = editorChange$.pipe(debounceTime(dbt))\n }\n\n this.eventsSubscription.add(\n // update model if text changes\n textChange$.subscribe(([delta, oldDelta, source]) => {\n this.textChangeHandler(delta as any, oldDelta as any, source)\n })\n )\n\n this.eventsSubscription.add(\n // triggered if selection or text changed\n editorChange$.subscribe(([event, current, old, source]) => {\n this.editorChangeHandler(event as 'text-change' | 'selection-change', current, old, source)\n })\n )\n }\n\n private dispose(): void {\n this.eventsSubscription?.unsubscribe()\n this.eventsSubscription = null\n }\n\n private isEmptyValue(html: string | null) {\n return html === '<p></p>' || html === '<div></div>' || html === '<p><br></p>' || html === '<div><br></div>'\n }\n\n private getter(quillEditor: QuillType, forceFormat?: QuillFormat): string | any {\n let modelValue: string | DeltaType | null = null\n const format = forceFormat ?? getFormat(this.format(), this.service.config.format)\n\n if (format === 'html') {\n let html: string | null = quillEditor.getSemanticHTML()\n if (this.isEmptyValue(html)) {\n html = this.defaultEmptyValue()\n }\n modelValue = html\n } else if (format === 'text') {\n modelValue = quillEditor.getText()\n } else if (format === 'object') {\n modelValue = quillEditor.getContents()\n } else if (format === 'json') {\n try {\n modelValue = JSON.stringify(quillEditor.getContents())\n } catch {\n modelValue = quillEditor.getText()\n }\n }\n\n return modelValue\n }\n\n private eventCallbackFormats() {\n const format = getFormat(this.format(), this.service.config.format)\n const onlyFormat = this.onlyFormatEventData() === true\n const noFormat = this.onlyFormatEventData() === 'none'\n let text: string | null = null\n let html: string | null = null\n let object: DeltaType | null = null\n let json: string | null = null\n const editor = this.quillEditor()\n\n // do nothing if no formatted value needed\n if (noFormat || !editor) {\n return {\n format,\n onlyFormat,\n noFormat,\n text,\n object,\n json,\n html\n }\n }\n\n // use getter input to grab value\n const value = this.valueGetter()(editor)\n\n if (format === 'text') {\n text = value\n } else if (format === 'html') {\n html = value\n } else if (format === 'object') {\n object = value\n json = JSON.stringify(value)\n } else if (format === 'json') {\n json = value\n object = JSON.parse(value)\n }\n\n // return current values, if only the editor format is needed\n if (onlyFormat) {\n return {\n format,\n onlyFormat,\n noFormat,\n text,\n json,\n html,\n object\n }\n }\n\n // return all format values\n return {\n format,\n onlyFormat,\n noFormat,\n // use internal getter to retrieve correct other values - this.valueGetter can be overwritten\n text: format === 'text' ? text : this.getter(editor, 'text'),\n json: format === 'json' ? json : this.getter(editor, 'json'),\n html: format === 'html' ? html : this.getter(editor, 'html'),\n object: format === 'object' ? object : this.getter(editor, 'object')\n }\n }\n}\n\n@Component({\n encapsulation: ViewEncapsulation.Emulated,\n selector: 'quill-editor-field',\n template: `\n @if (toolbarPosition() !== 'top') {\n <div quill-editor-element></div>\n }\n\n <ng-content select=\"[above-quill-editor-toolbar]\"></ng-content>\n <ng-content select=\"[quill-editor-toolbar]\"></ng-content>\n <ng-content select=\"[below-quill-editor-toolbar]\"></ng-content>\n\n @if (toolbarPosition() === 'top') {\n <div quill-editor-element></div>\n }\n `,\n styles: [\n `\n :host {\n display: inline-block;\n }\n `\n ]\n})\nexport class QuillEditorFieldComponent extends QuillEditorFieldBase { }\n\n","import { QuillFormat } from 'ngx-quill/config'\n\nexport const getFormat = (format?: QuillFormat, configFormat?: QuillFormat): QuillFormat => {\n const passedFormat = format || configFormat\n return passedFormat || 'html'\n}\n","import { isPlatformServer } from '@angular/common'\nimport { DomSanitizer } from '@angular/platform-browser'\n\nimport type QuillType from 'quill'\nimport type { QuillOptions } from 'quill'\nimport type DeltaType from 'quill-delta'\n\nimport {\n afterNextRender,\n booleanAttribute,\n Component,\n DestroyRef,\n Directive,\n effect,\n ElementRef,\n EventEmitter,\n forwardRef,\n inject,\n input,\n Output,\n PLATFORM_ID,\n Renderer2,\n SecurityContext,\n signal,\n ViewEncapsulation\n} from '@angular/core'\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop'\nimport { debounceTime, fromEvent, Subscription } from 'rxjs'\nimport { mergeMap } from 'rxjs/operators'\n\nimport { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'\n\nimport { CustomModule, CustomOption, defaultModules, QuillBeforeRender, QuillFormat, QuillModules } from 'ngx-quill/config'\n\nimport type History from 'quill/modules/history'\nimport type Toolbar from 'quill/modules/toolbar'\nimport { getFormat } from './helpers'\nimport { QuillService } from './quill.service'\n\nexport interface Range {\n index: number\n length: number\n}\n\nexport interface ContentChange {\n content: DeltaType\n delta: DeltaType\n editor: QuillType\n html: string | null\n oldDelta: DeltaType\n source: string\n text: string\n}\n\nexport interface SelectionChange {\n editor: QuillType\n oldRange: Range | null\n range: Range | null\n source: string\n}\n\nexport interface Blur {\n editor: QuillType\n source: string\n}\n\nexport interface Focus {\n editor: QuillType\n source: string\n}\n\nexport type EditorChangeContent = ContentChange & { event: 'text-change' }\nexport type EditorChangeSelection = SelectionChange & { event: 'selection-change' }\n\n@Directive()\nexport abstract class QuillEditorBase implements ControlValueAccessor, Validator {\n readonly format = input<QuillFormat>()\n readonly theme = input<string>()\n readonly modules = input<QuillModules>()\n readonly debug = input<'warn' | 'log' | 'error' | false>(false)\n readonly readOnly = input(false, { transform: booleanAttribute })\n readonly placeholder = input<string>()\n readonly maxLength = input<number>()\n readonly minLength = input<number>()\n readonly required = input(false, { transform: booleanAttribute })\n readonly formats = input<string[] | null>()\n readonly customToolbarPosition = input<'top' | 'bottom'>('top')\n readonly sanitize = input<boolean>()\n readonly beforeRender = input<QuillBeforeRender>()\n readonly styles = input<any>(null)\n readonly registry = input<QuillOptions['registry']>()\n readonly bounds = input<HTMLElement | string>()\n readonly customOptions = input<CustomOption[]>([])\n readonly customModules = input<CustomModule[]>([])\n readonly trackChanges = input<'user' | 'all'>()\n readonly classes = input<string>()\n readonly trimOnValidation = input(false, { transform: booleanAttribute })\n readonly linkPlaceholder = input<string>()\n readonly compareValues = input(false, { transform: booleanAttribute })\n readonly filterNull = input(false, { transform: booleanAttribute })\n readonly debounceTime = input<number>()\n readonly onlyFormatEventData = input<boolean | 'none'>(false)\n /*\n https://github.com/KillerCodeMonkey/ngx-quill/issues/1257 - fix null value set\n\n provide default empty value\n by default null\n\n e.g. defaultEmptyValue=\"\" - empty string\n\n <quill-editor\n defaultEmptyValue=\"\"\n formControlName=\"message\"\n ></quill-editor>\n */\n readonly defaultEmptyValue = input<any>(null)\n\n @Output() onEditorCreated = new EventEmitter<QuillType>()\n @Output() onEditorChanged = new EventEmitter<EditorChangeContent | EditorChangeSelection>()\n @Output() onContentChanged = new EventEmitter<ContentChange>()\n @Output() onSelectionChanged = new EventEmitter<SelectionChange>()\n @Output() onFocus = new EventEmitter<Focus>()\n @Output() onBlur = new EventEmitter<Blur>()\n @Output() onNativeFocus = new EventEmitter<Focus>()\n @Output() onNativeBlur = new EventEmitter<Blur>()\n\n quillEditor!: QuillType\n editorElem!: HTMLElement\n content: any\n disabled = false // used to store initial value before ViewInit\n\n readonly toolbarPosition = signal('top')\n\n onModelChange: ((modelValue?: any) => void) | undefined\n onModelTouched: (() => void) | undefined\n onValidatorChanged: (() => void) | undefined\n\n private eventsSubscription: Subscription | null = null\n private quillSubscription: Subscription | null = null\n\n private elementRef = inject(ElementRef)\n\n private domSanitizer = inject(DomSanitizer)\n private platformId = inject<string>(PLATFORM_ID)\n private renderer = inject(Renderer2)\n private service = inject(QuillService)\n private destroyRef = inject(DestroyRef)\n\n private previousStyles: any\n private previousClasses: any\n\n init = false\n\n constructor() {\n afterNextRender(() => {\n if (isPlatformServer(this.platformId)) {\n return\n }\n\n // The `quill-editor` component might be destroyed before the `quill` chunk is loaded and its code is executed\n // this will lead to runtime exceptions, since the code will be executed on DOM nodes that don't exist within the tree.\n\n this.quillSubscription = this.service.getQuill().pipe(\n mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))\n ).subscribe(Quill => {\n this.editorElem = this.elementRef.nativeElement.querySelector(\n '[quill-editor-element]'\n )\n\n const toolbarElem = this.elementRef.nativeElement.querySelector(\n '[quill-editor-toolbar]'\n )\n const modules = Object.assign({}, this.modules() || this.service.config.modules)\n\n if (toolbarElem) {\n modules.toolbar = toolbarElem\n } else if (modules.toolbar === undefined) {\n modules.toolbar = defaultModules.toolbar\n }\n\n let placeholder = this.placeholder() !== undefined ? this.placeholder() : this.service.config.placeholder\n if (placeholder === undefined) {\n placeholder = 'Insert text here ...'\n }\n\n const styles = this.styles()\n if (styles) {\n this.previousStyles = styles\n Object.keys(styles).forEach((key: string) => {\n this.renderer.setStyle(this.editorElem, key, styles[key])\n })\n }\n\n const previousClasses = this.classes()\n if (previousClasses) {\n this.previousClasses =previousClasses\n this.addClasses(previousClasses)\n }\n\n this.customOptions().forEach((customOption) => {\n const newCustomOption = Quill.import(customOption.import)\n newCustomOption.whitelist = customOption.whitelist\n Quill.register(newCustomOption, true)\n })\n\n let bounds = this.bounds() && this.bounds() === 'self' ? this.editorElem : this.bounds()\n if (!bounds) {\n // Can use global `document` because we execute this only in the browser.\n bounds = this.service.config.bounds ? this.service.config.bounds : document.body\n }\n\n let debug = this.debug()\n if (!debug && debug !== false && this.service.config.debug) {\n debug = this.service.config.debug\n }\n\n let readOnly = this.readOnly()\n if (!readOnly && this.readOnly() !== false) {\n readOnly = this.service.config.readOnly !== undefined ? this.service.config.readOnly : false\n }\n\n let formats = this.formats()\n if (!formats && formats === undefined) {\n formats = this.service.config.formats ? [...this.service.config.formats] : (this.service.config.formats === null ? null : undefined)\n }\n\n this.quillEditor = new Quill(this.editorElem, {\n bounds,\n debug,\n formats,\n modules,\n placeholder,\n readOnly,\n registry: this.registry(),\n theme: this.theme() || (this.service.config.theme ? this.service.config.theme : 'snow')\n })\n\n if (this.onNativeBlur.observed) {\n // https://github.com/quilljs/quill/issues/2186#issuecomment-533401328\n fromEvent(this.quillEditor.scroll.domNode, 'blur').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeBlur.next({\n editor: this.quillEditor,\n source: 'dom'\n }))\n // https://github.com/quilljs/quill/issues/2186#issuecomment-803257538\n const toolbar = this.quillEditor.getModule('toolbar') as Toolbar\n if (toolbar.container) {\n fromEvent(toolbar.container, 'mousedown').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(e => e.preventDefault())\n }\n }\n\n if (this.onNativeFocus.observed) {\n fromEvent(this.quillEditor.scroll.domNode, 'focus').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeFocus.next({\n editor: this.quillEditor,\n source: 'dom'\n }))\n }\n\n // Set optional link placeholder, Quill has no native API for it so using workaround\n if (this.linkPlaceholder()) {\n const tooltip = (this.quillEditor as any)?.theme?.tooltip\n const input = tooltip?.root?.querySelector('input[data-link]')\n if (input?.dataset) {\n input.dataset.link = this.linkPlaceholder()\n }\n }\n\n if (this.content) {\n const format = getFormat(this.format(), this.service.config.format)\n\n if (format === 'text') {\n this.quillEditor.setText(this.content, 'silent')\n } else {\n const valueSetter = this.valueSetter()\n const newValue = valueSetter(this.quillEditor, this.content)\n this.quillEditor.setContents(newValue, 'silent')\n }\n\n const history = this.quillEditor.getModule('history') as History\n history.clear()\n }\n\n // initialize disabled status based on this.disabled as default value\n this.setDisabledState()\n\n this.addQuillEventListeners()\n\n // listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)=\"...\">`.\n if (!this.onEditorCreated.observed && !this.onValidatorChanged) {\n this.init = true\n return\n }\n\n if (this.onValidatorChanged) {\n this.onValidatorChanged()\n }\n this.onEditorCreated.emit(this.quillEditor)\n this.init = true\n })\n })\n\n effect(() => {\n const customToolbarPosition = this.customToolbarPosition()\n if (this.toolbarPosition() !== customToolbarPosition) {\n this.toolbarPosition.set(customToolbarPosition)\n }\n })\n\n effect(() => {\n const readOnly = this.readOnly()\n if (this.init) {\n if (readOnly) {\n this.quillEditor?.disable()\n } else {\n this.quillEditor?.enable(true)\n }\n }\n })\n\n effect(() => {\n const placeholder = this.placeholder()\n if (this.init && this.quillEditor) {\n this.quillEditor.root.dataset.placeholder = placeholder\n }\n })\n\n effect(() => {\n const styles = this.styles()\n if (!this.init || !this.editorElem) {\n return\n }\n const currentStyling = styles\n const previousStyling = this.previousStyles\n\n if (previousStyling) {\n Object.keys(previousStyling).forEach((key: string) => {\n this.renderer.removeStyle(this.editorElem, key)\n })\n }\n if (currentStyling) {\n Object.keys(currentStyling).forEach((key: string) => {\n this.renderer.setStyle(this.editorElem, key, currentStyling[key])\n })\n }\n })\n\n effect(() => {\n const classes = this.classes()\n if (!this.init || !this.quillEditor) {\n return\n }\n const currentClasses = classes\n const previousClasses = this.previousClasses\n\n if (previousClasses) {\n this.removeClasses(previousClasses)\n }\n\n if (currentClasses) {\n this.addClasses(currentClasses)\n }\n })\n\n effect(() => {\n const debounceTime = this.debounceTime()\n if (!this.init || !this.quillEditor) {\n return\n }\n if (debounceTime) {\n this.addQuillEventListeners()\n }\n })\n\n this.destroyRef.onDestroy(() => {\n this.dispose()\n\n this.quillSubscription?.unsubscribe()\n this.quillSubscription = null\n })\n }\n\n static normalizeClassNames(classes: string): string[] {\n const classList = classes.trim().split(' ')\n return classList.reduce((prev: string[], cur: string) => {\n const trimmed = cur.trim()\n if (trimmed) {\n prev.push(trimmed)\n }\n\n return prev\n }, [])\n }\n\n valueGetter = input(this.getter.bind(this))\n\n valueSetter = input((quillEditor: QuillType, value: any): any => {\n const format = getFormat(this.format(), this.service.config.format)\n if (format === 'html') {\n const sanitize = (typeof this.sanitize() === 'boolean') ? this.sanitize() : (this.service.config.sanitize || false)\n if (sanitize) {\n value = this.domSanitizer.sanitize(SecurityContext.HTML, value)\n }\n return quillEditor.clipboard.convert({ html: value })\n } else if (format === 'json') {\n try {\n return JSON.parse(value)\n } catch {\n return [{ insert: value }]\n }\n }\n\n return value\n })\n\n selectionChangeHandler = (range: Range | null, oldRange: Range | null, source: string) => {\n const trackChanges = this.trackChanges() || this.service.config.trackChanges\n const shouldTriggerOnModelTouched = !range && !!this.onModelTouched && (source === 'user' || trackChanges && trackChanges === 'all')\n\n // only emit changes when there's any listener\n if (!this.onBlur.observed &&\n !this.onFocus.observed &&\n !this.onSelectionChanged.observed &&\n !shouldTriggerOnModelTouched) {\n return\n }\n\n if (range === null) {\n this.onBlur.emit({\n editor: this.quillEditor,\n source\n })\n } else if (oldRange === null) {\n this.onFocus.emit({\n editor: this.quillEditor,\n source\n })\n }\n\n this.onSelectionChanged.emit({\n editor: this.quillEditor,\n oldRange,\n range,\n source\n })\n\n if (shouldTriggerOnModelTouched) {\n this.onModelTouched!()\n }\n }\n\n textChangeHandler = (delta: DeltaType, oldDelta: DeltaType, source: string): void => {\n const trackChanges = this.trackChanges() || this.service.config.trackChanges\n const shouldTriggerOnModelChange = (source === 'user' || trackChanges && trackChanges === 'all') && !!this.onModelChange\n\n // only emit changes when there's any listener\n if (!this.onContentChanged.observed && !shouldTriggerOnModelChange) {\n return\n }\n\n const data = this.eventCallbackFormats()\n\n if (shouldTriggerOnModelChange) {\n this.onModelChange!(\n // only call value getter again if not already done in eventCallbackFormats\n data.noFormat ? this.valueGetter()(this.quillEditor) : data[data.format]\n )\n }\n\n this.onContentChanged.emit({\n content: data.object,\n delta,\n editor: this.quillEditor,\n html: data.html,\n oldDelta,\n source,\n text: data.text\n })\n }\n\n editorChangeHandler = (\n event: 'text-change' | 'selection-change',\n current: any | Range | null, old: any | Range | null, source: string\n ): void => {\n // only emit changes when there's any listener\n if (!this.onEditorChanged.observed) {\n return\n }\n\n // only emit changes emitted by user interactions\n if (event === 'text-change') {\n const data = this.eventCallbackFormats()\n\n this.onEditorChanged.emit({\n content: data.object,\n delta: current,\n editor: this.quillEditor,\n event,\n html: data.html,\n oldDelta: old,\n source,\n text: data.text\n })\n } else {\n this.onEditorChanged.emit({\n editor: this.quillEditor,\n event,\n oldRange: old,\n range: current,\n source\n })\n }\n }\n\n addClasses(classList: string): void {\n QuillEditorBase.normalizeClassNames(classList).forEach((c: string) => {\n this.renderer.addClass(this.editorElem, c)\n })\n }\n\n removeClasses(classList: string): void {\n QuillEditorBase.normalizeClassNames(classList).forEach((c: string) => {\n this.renderer.removeClass(this.editorElem, c)\n })\n }\n\n writeValue(currentValue: any) {\n // optional fix for https://github.com/angular/angular/issues/14988\n if (this.filterNull() && currentValue === null) {\n return\n }\n\n this.content = currentValue\n\n if (!this.quillEditor) {\n return\n }\n\n const format = getFormat(this.format(), this.service.config.format)\n const valueSetter = this.valueSetter()\n const newValue = valueSetter(this.quillEditor, currentValue)\n\n if (this.compareValues()) {\n const currentEditorValue = this.quillEditor.getContents()\n if (JSON.stringify(currentEditorValue) === JSON.stringify(newValue)) {\n return\n }\n }\n\n if (currentValue) {\n if (format === 'text') {\n this.quillEditor.setText(currentValue)\n } else {\n this.quillEditor.setContents(newValue)\n }\n return\n }\n this.quillEditor.setText('')\n\n }\n\n setDisabledState(isDisabled: boolean = this.disabled): void {\n // store initial value to set appropriate disabled status after ViewInit\n this.disabled = isDisabled\n if (this.quillEditor) {\n if (isDisabled) {\n this.quillEditor.disable()\n this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'disabled')\n } else {\n if (!this.readOnly()) {\n this.quillEditor.enable()\n }\n this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled')\n }\n }\n }\n\n registerOnChange(fn: (modelValue: any) => void): void {\n this.onModelChange = fn\n }\n\n registerOnTouched(fn: () => void): void {\n this.onModelTouched = fn\n }\n\n registerOnValidatorChange(fn: () => void) {\n this.onValidatorChanged = fn\n }\n\n validate() {\n if (!this.quillEditor) {\n return null\n }\n\n const err: {\n minLengthError?: {\n given: number\n minLength: number\n }\n maxLengthError?: {\n given: number\n maxLength: number\n }\n requiredError?: { empty: boolean }\n } = {}\n let valid = true\n\n const text = this.quillEditor.getText()\n // trim text if wanted + handle special case that an empty editor contains a new line\n const textLength = this.trimOnValidation() ? text.trim().length : (text.length === 1 && text.trim().length === 0 ? 0 : text.length - 1)\n const deltaOperations = this.quillEditor.getContents().ops\n const onlyEmptyOperation = !!deltaOperations && deltaOperations.length === 1 && ['\\n', ''].includes(deltaOperations[0].insert?.toString() || '')\n\n const minLength = this.minLength()\n if (minLength && textLength && textLength < minLength) {\n err.minLengthError = {\n given: textLength,\n minLength\n }\n\n valid = false\n }\n\n const maxLength = this.maxLength()\n if (maxLength && textLength > maxLength) {\n err.maxLengthError = {\n given: textLength,\n maxLength\n }\n\n valid = false\n }\n\n if (this.required() &&