ngx-quill
Version:
Angular components for the easy use of the QuillJS richt text editor.
1,009 lines (996 loc) • 48.2 kB
JavaScript
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