UNPKG

ngx-quill

Version:

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

1,009 lines (996 loc) 48.2 kB
import { defaultModules, QUILL_CONFIG_TOKEN } from 'ngx-quill/config'; export * from 'ngx-quill/config'; import * as i0 from '@angular/core'; import { Injectable, Optional, Inject, EventEmitter, inject, ElementRef, ChangeDetectorRef, PLATFORM_ID, Renderer2, NgZone, SecurityContext, Directive, Input, Output, forwardRef, Component, ViewEncapsulation, NgModule } from '@angular/core'; import * as i1 from '@angular/common'; import { DOCUMENT, isPlatformServer, CommonModule } from '@angular/common'; import * as i2 from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser'; import { defer, isObservable, firstValueFrom, Subscription, fromEvent } from 'rxjs'; import { shareReplay, 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'; }; class QuillService { constructor(injector, config) { this.config = config; 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 = // eslint-disable-next-line @typescript-eslint/dot-notation this.document['__zone_symbol__addEventListener'] || this.document.addEventListener; const quillImport = await import(/* webpackChunkName: 'quill' */ 'quill'); this.document.addEventListener = maybePatchedAddEventListener; this.Quill = (quillImport.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 await this.registerCustomModules(this.Quill, this.config.customModules, this.config.suppressGlobalRegisterWarning); }).pipe(shareReplay({ bufferSize: 1, refCount: true })); this.document = injector.get(DOCUMENT); if (!this.config) { this.config = { modules: defaultModules }; } } getQuill() { return this.quill$; } /** * Marked as internal so it won't be available for `ngx-quill` consumers, this is only * internal method to be used within the library. * * @internal */ async registerCustomModules(Quill, customModules, suppressGlobalRegisterWarning) { if (Array.isArray(customModules)) { // eslint-disable-next-line prefer-const for (let { implementation, path } of customModules) { // The `implementation` might be an observable that resolves the actual implementation, // e.g. if it should be lazy loaded. if (isObservable(implementation)) { implementation = await firstValueFrom(implementation); } Quill.register(path, implementation, suppressGlobalRegisterWarning); } } // Return `Quill` constructor so we'll be able to re-use its return value except of using // `map` operators, etc. return Quill; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillService, deps: [{ token: i0.Injector }, { token: QUILL_CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: i0.Injector }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [QUILL_CONFIG_TOKEN] }] }]; } }); class QuillEditorBase { constructor() { this.required = false; this.customToolbarPosition = 'top'; this.styles = null; this.strict = true; this.customOptions = []; this.customModules = []; this.preserveWhitespace = false; this.trimOnValidation = false; this.compareValues = false; this.filterNull = false; /* 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 = 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.preserve = false; this.toolbarPosition = '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.valueGetter = (quillEditor, editorElement) => { let html = editorElement.querySelector('.ql-editor').innerHTML; if (html === '<p><br></p>' || html === '<div><br></div>') { 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 (e) { modelValue = quillEditor.getText(); } } return modelValue; }; this.valueSetter = (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(value); } else if (format === 'json') { try { return JSON.parse(value); } catch (e) { return [{ insert: value }]; } } return value; }; this.selectionChangeHandler = (range, oldRange, source) => { const shouldTriggerOnModelTouched = !range && !!this.onModelTouched; // 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.editorElem.querySelector('.ql-editor').innerHTML; if (html === '<p><br></p>' || html === '<div><br></div>') { 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) { this.onModelChange(this.valueGetter(this.quillEditor, this.editorElem)); } this.onContentChanged.emit({ content, delta, editor: this.quillEditor, html, oldDelta, source, text }); this.cd.markForCheck(); }); }; // eslint-disable-next-line max-len 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.editorElem.querySelector('.ql-editor').innerHTML; if (html === '<p><br></p>' || html === '<div><br></div>') { 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.preserve = this.preserveWhitespace; this.toolbarPosition = 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) => { const promises = [this.service.registerCustomModules(Quill, this.customModules)]; const beforeRender = this.beforeRender ?? this.service.config.beforeRender; if (beforeRender) { promises.push(beforeRender()); } return Promise.all(promises).then(() => Quill); })).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 ...'; } if (this.styles) { Object.keys(this.styles).forEach((key) => { this.renderer.setStyle(this.editorElem, key, this.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 defaultEmptyValue = this.defaultEmptyValue; // eslint-disable-next-line no-prototype-builtins if (this.service.config.hasOwnProperty('defaultEmptyValue')) { defaultEmptyValue = this.service.config.defaultEmptyValue; } let scrollingContainer = this.scrollingContainer; if (!scrollingContainer && this.scrollingContainer !== null) { scrollingContainer = this.service.config.scrollingContainer === null || this.service.config.scrollingContainer ? this.service.config.scrollingContainer : null; } 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: debug, formats: formats, modules, placeholder, readOnly, defaultEmptyValue, scrollingContainer: scrollingContainer, strict: this.strict, 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 this.quillEditor.scroll.domNode.addEventListener('blur', () => this.onNativeBlur.next({ editor: this.quillEditor, source: 'dom' })); // https://github.com/quilljs/quill/issues/2186#issuecomment-803257538 this.quillEditor.getModule('toolbar').container.addEventListener('mousedown', (e) => e.preventDefault()); } if (this.onNativeFocus.observed) { this.quillEditor.scroll.domNode.addEventListener('focus', () => 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 newValue = this.valueSetter(this.quillEditor, this.content); this.quillEditor.setContents(newValue, 'silent'); } this.quillEditor.getModule('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. requestAnimationFrame(() => { if (this.onValidatorChanged) { this.onValidatorChanged(); } this.onEditorCreated.emit(this.quillEditor); this.onEditorCreated.complete(); }); }); } ngOnDestroy() { this.dispose(); this.quillSubscription?.unsubscribe(); this.quillSubscription = null; } ngOnChanges(changes) { if (!this.quillEditor) { return; } /* eslint-disable @typescript-eslint/dot-notation */ if (changes.readOnly) { this.quillEditor.enable(!changes.readOnly.currentValue); } if (changes.placeholder) { this.quillEditor.root.dataset.placeholder = changes.placeholder.currentValue; } if (changes.defaultEmptyValue) { this.quillEditor.root.dataset.defaultEmptyValue = changes.defaultEmptyValue.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(); } /* eslint-enable @typescript-eslint/dot-notation */ } 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 newValue = this.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); 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; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.5", type: QuillEditorBase, inputs: { format: "format", theme: "theme", modules: "modules", debug: "debug", readOnly: "readOnly", placeholder: "placeholder", maxLength: "maxLength", minLength: "minLength", required: "required", formats: "formats", customToolbarPosition: "customToolbarPosition", sanitize: "sanitize", beforeRender: "beforeRender", styles: "styles", strict: "strict", scrollingContainer: "scrollingContainer", bounds: "bounds", customOptions: "customOptions", customModules: "customModules", trackChanges: "trackChanges", preserveWhitespace: "preserveWhitespace", classes: "classes", trimOnValidation: "trimOnValidation", linkPlaceholder: "linkPlaceholder", compareValues: "compareValues", filterNull: "filterNull", debounceTime: "debounceTime", defaultEmptyValue: "defaultEmptyValue", valueGetter: "valueGetter", valueSetter: "valueSetter" }, 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: "16.2.5", ngImport: i0, type: QuillEditorBase, decorators: [{ type: Directive }], propDecorators: { format: [{ type: Input }], theme: [{ type: Input }], modules: [{ type: Input }], debug: [{ type: Input }], readOnly: [{ type: Input }], placeholder: [{ type: Input }], maxLength: [{ type: Input }], minLength: [{ type: Input }], required: [{ type: Input }], formats: [{ type: Input }], customToolbarPosition: [{ type: Input }], sanitize: [{ type: Input }], beforeRender: [{ type: Input }], styles: [{ type: Input }], strict: [{ type: Input }], scrollingContainer: [{ type: Input }], bounds: [{ type: Input }], customOptions: [{ type: Input }], customModules: [{ type: Input }], trackChanges: [{ type: Input }], preserveWhitespace: [{ type: Input }], classes: [{ type: Input }], trimOnValidation: [{ type: Input }], linkPlaceholder: [{ type: Input }], compareValues: [{ type: Input }], filterNull: [{ type: Input }], debounceTime: [{ type: Input }], defaultEmptyValue: [{ type: Input }], 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: Input }], valueSetter: [{ type: Input }] } }); class QuillEditorComponent extends QuillEditorBase { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.5", type: QuillEditorComponent, isStandalone: true, selector: "quill-editor", providers: [ { multi: true, provide: NG_VALUE_ACCESSOR, // eslint-disable-next-line @typescript-eslint/no-use-before-define useExisting: forwardRef(() => QuillEditorComponent) }, { multi: true, provide: NG_VALIDATORS, // eslint-disable-next-line @typescript-eslint/no-use-before-define useExisting: forwardRef(() => QuillEditorComponent) } ], usesInheritance: true, ngImport: i0, template: ` <ng-template [ngIf]="toolbarPosition !== 'top'"> <pre quill-editor-element *ngIf="preserve; else noPreserveTpl"></pre> </ng-template> <ng-content select="[quill-editor-toolbar]"></ng-content> <ng-template [ngIf]="toolbarPosition === 'top'"> <pre quill-editor-element *ngIf="preserve; else noPreserveTpl"></pre> </ng-template> <ng-template #noPreserveTpl> <div quill-editor-element></div> </ng-template> `, isInline: true, styles: [":host{display:inline-block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillEditorComponent, decorators: [{ type: Component, args: [{ encapsulation: ViewEncapsulation.Emulated, providers: [ { multi: true, provide: NG_VALUE_ACCESSOR, // eslint-disable-next-line @typescript-eslint/no-use-before-define useExisting: forwardRef(() => QuillEditorComponent) }, { multi: true, provide: NG_VALIDATORS, // eslint-disable-next-line @typescript-eslint/no-use-before-define useExisting: forwardRef(() => QuillEditorComponent) } ], selector: 'quill-editor', template: ` <ng-template [ngIf]="toolbarPosition !== 'top'"> <pre quill-editor-element *ngIf="preserve; else noPreserveTpl"></pre> </ng-template> <ng-content select="[quill-editor-toolbar]"></ng-content> <ng-template [ngIf]="toolbarPosition === 'top'"> <pre quill-editor-element *ngIf="preserve; else noPreserveTpl"></pre> </ng-template> <ng-template #noPreserveTpl> <div quill-editor-element></div> </ng-template> `, standalone: true, imports: [CommonModule], styles: [":host{display:inline-block}\n"] }] }] }); class QuillViewHTMLComponent { constructor(sanitizer, service) { this.sanitizer = sanitizer; this.service = service; this.content = ''; this.innerHTML = ''; this.themeClass = 'ql-snow'; } ngOnChanges(changes) { if (changes.theme) { const theme = changes.theme.currentValue || (this.service.config.theme ? this.service.config.theme : 'snow'); this.themeClass = `ql-${theme} ngx-quill-view-html`; } else if (!this.theme) { const theme = this.service.config.theme ? this.service.config.theme : 'snow'; this.themeClass = `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); this.innerHTML = sanitize ? content : this.sanitizer.bypassSecurityTrustHtml(content); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillViewHTMLComponent, deps: [{ token: DomSanitizer }, { token: QuillService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.5", type: QuillViewHTMLComponent, isStandalone: true, selector: "quill-view-html", inputs: { content: "content", theme: "theme", sanitize: "sanitize" }, 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: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillViewHTMLComponent, decorators: [{ type: Component, args: [{ encapsulation: ViewEncapsulation.None, selector: 'quill-view-html', template: ` <div class="ql-container" [ngClass]="themeClass"> <div class="ql-editor" [innerHTML]="innerHTML"> </div> </div> `, standalone: true, imports: [CommonModule], styles: [".ql-container.ngx-quill-view-html{border:0}\n"] }] }], ctorParameters: function () { return [{ type: i2.DomSanitizer, decorators: [{ type: Inject, args: [DomSanitizer] }] }, { type: QuillService }]; }, propDecorators: { content: [{ type: Input }], theme: [{ type: Input }], sanitize: [{ type: Input }] } }); 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.strict = true; this.customModules = []; this.customOptions = []; this.preserveWhitespace = false; this.onEditorCreated = new EventEmitter(); this.preserve = false; this.quillSubscription = null; 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(value); } else if (format === 'json') { try { content = JSON.parse(value); } catch (e) { content = [{ insert: value }]; } } quillEditor.setContents(content); } }; } ngOnInit() { this.preserve = this.preserveWhitespace; } 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) => { const promises = [this.service.registerCustomModules(Quill, this.customModules)]; const beforeRender = this.beforeRender ?? this.service.config.beforeRender; if (beforeRender) { promises.push(beforeRender()); } return Promise.all(promises).then(() => Quill); })).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 ? Object.assign({}, 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: debug, formats: 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.observers.length) { 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. requestAnimationFrame(() => { this.onEditorCreated.emit(this.quillEditor); this.onEditorCreated.complete(); }); }); } ngOnDestroy() { this.quillSubscription?.unsubscribe(); this.quillSubscription = null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillViewComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: QuillService }, { token: i2.DomSanitizer }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.5", type: QuillViewComponent, isStandalone: true, selector: "quill-view", inputs: { format: "format", theme: "theme", modules: "modules", debug: "debug", formats: "formats", sanitize: "sanitize", beforeRender: "beforeRender", strict: "strict", content: "content", customModules: "customModules", customOptions: "customOptions", preserveWhitespace: "preserveWhitespace" }, outputs: { onEditorCreated: "onEditorCreated" }, usesOnChanges: true, ngImport: i0, template: ` <div quill-view-element *ngIf="!preserve"></div> <pre quill-view-element *ngIf="preserve"></pre> `, isInline: true, styles: [".ql-container.ngx-quill-view{border:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillViewComponent, decorators: [{ type: Component, args: [{ encapsulation: ViewEncapsulation.None, selector: 'quill-view', template: ` <div quill-view-element *ngIf="!preserve"></div> <pre quill-view-element *ngIf="preserve"></pre> `, standalone: true, imports: [CommonModule], styles: [".ql-container.ngx-quill-view{border:0}\n"] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: QuillService }, { type: i2.DomSanitizer }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }]; }, propDecorators: { format: [{ type: Input }], theme: [{ type: Input }], modules: [{ type: Input }], debug: [{ type: Input }], formats: [{ type: Input }], sanitize: [{ type: Input }], beforeRender: [{ type: Input }], strict: [{ type: Input }], content: [{ type: Input }], customModules: [{ type: Input }], customOptions: [{ type: Input }], preserveWhitespace: [{ type: Input }], 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: "16.2.5", ngImport: i0, type: QuillModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.5", ngImport: i0, type: QuillModule, imports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent], exports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.5", ngImport: i0, type: QuillModule, imports: [QuillEditorComponent, QuillViewComponent, QuillViewHTMLComponent] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.5", 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