ngx-quill
Version:
Angular components for the easy use of the QuillJS richt text editor.
890 lines (878 loc) • 54.9 kB
JavaScript
import { defaultModules, QUILL_CONFIG_TOKEN } from 'ngx-quill/config';
export * from 'ngx-quill/config';
import * as i0 from '@angular/core';
import { inject, Injectable, input, booleanAttribute, EventEmitter, signal, ElementRef, PLATFORM_ID, Renderer2, DestroyRef, SecurityContext, afterNextRender, effect, Output, Directive, forwardRef, ChangeDetectionStrategy, ViewEncapsulation, Component, computed, NgModule } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { DomSanitizer } from '@angular/platform-browser';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { defer, forkJoin, of, isObservable, fromEvent, Subscription, debounceTime } from 'rxjs';
import { shareReplay, map, tap, mergeMap } from 'rxjs/operators';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
const getFormat = (format, configFormat) => {
const passedFormat = format || configFormat;
return passedFormat || 'html';
};
class QuillService {
constructor() {
this.config = inject(QUILL_CONFIG_TOKEN) || { modules: defaultModules };
this.quill$ = defer(async () => {
if (!this.Quill) {
const { Quill } = await import('./ngx-quill-quill-CUw8Q_m0.mjs');
this.Quill = Quill;
}
// Only register custom options and modules once
this.config.customOptions?.forEach((customOption) => {
const newCustomOption = this.Quill.import(customOption.import);
newCustomOption.whitelist = customOption.whitelist;
this.Quill.register(newCustomOption, true, this.config.suppressGlobalRegisterWarning);
});
// Use `Promise` directly to avoid bundling `firstValueFrom`.
return new Promise(resolve => {
this.registerCustomModules(this.Quill, this.config.customModules, this.config.suppressGlobalRegisterWarning).subscribe(resolve);
});
}).pipe(shareReplay({
bufferSize: 1,
refCount: false
}));
// A list of custom modules that have already been registered,
// so we don’t need to await their implementation.
this.registeredModules = new Set();
}
getQuill() {
return this.quill$;
}
/** @internal */
beforeRender(Quill, customModules, beforeRender = this.config.beforeRender) {
// This function is called each time the editor needs to be rendered,
// so it operates individually per component. If no custom module needs to be
// registered and no `beforeRender` function is provided, it will emit
// immediately and proceed with the rendering.
const sources = [this.registerCustomModules(Quill, customModules)];
if (beforeRender) {
sources.push(beforeRender());
}
return forkJoin(sources).pipe(map(() => Quill));
}
/** @internal */
registerCustomModules(Quill, customModules, suppressGlobalRegisterWarning) {
if (!Array.isArray(customModules)) {
return of(Quill);
}
const sources = [];
for (const customModule of customModules) {
const { path, implementation: maybeImplementation } = customModule;
// If the module is already registered, proceed to the next module...
if (this.registeredModules.has(path)) {
continue;
}
this.registeredModules.add(path);
if (isObservable(maybeImplementation)) {
// If the implementation is an observable, we will wait for it to load and
// then register it with Quill. The caller will wait until the module is registered.
sources.push(maybeImplementation.pipe(tap((implementation) => {
Quill.register(path, implementation, suppressGlobalRegisterWarning);
})));
}
else {
Quill.register(path, maybeImplementation, suppressGlobalRegisterWarning);
}
}
return sources.length > 0 ? forkJoin(sources).pipe(map(() => Quill)) : of(Quill);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class QuillEditorBase {
constructor() {
this.format = input(...(ngDevMode ? [undefined, { debugName: "format" }] : []));
this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : []));
this.modules = input(...(ngDevMode ? [undefined, { debugName: "modules" }] : []));
this.debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : []));
this.readOnly = input(false, ...(ngDevMode ? [{ debugName: "readOnly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
this.placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : []));
this.maxLength = input(...(ngDevMode ? [undefined, { debugName: "maxLength" }] : []));
this.minLength = input(...(ngDevMode ? [undefined, { debugName: "minLength" }] : []));
this.required = input(false, ...(ngDevMode ? [{ debugName: "required", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
this.formats = input(...(ngDevMode ? [undefined, { debugName: "formats" }] : []));
this.customToolbarPosition = input('top', ...(ngDevMode ? [{ debugName: "customToolbarPosition" }] : []));
this.sanitize = input(...(ngDevMode ? [undefined, { debugName: "sanitize" }] : []));
this.beforeRender = input(...(ngDevMode ? [undefined, { debugName: "beforeRender" }] : []));
this.styles = input(null, ...(ngDevMode ? [{ debugName: "styles" }] : []));
this.registry = input(...(ngDevMode ? [undefined, { debugName: "registry" }] : []));
this.bounds = input(...(ngDevMode ? [undefined, { debugName: "bounds" }] : []));
this.customOptions = input([], ...(ngDevMode ? [{ debugName: "customOptions" }] : []));
this.customModules = input([], ...(ngDevMode ? [{ debugName: "customModules" }] : []));
this.trackChanges = input(...(ngDevMode ? [undefined, { debugName: "trackChanges" }] : []));
this.classes = input(...(ngDevMode ? [undefined, { debugName: "classes" }] : []));
this.trimOnValidation = input(false, ...(ngDevMode ? [{ debugName: "trimOnValidation", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
this.linkPlaceholder = input(...(ngDevMode ? [undefined, { debugName: "linkPlaceholder" }] : []));
this.compareValues = input(false, ...(ngDevMode ? [{ debugName: "compareValues", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
this.filterNull = input(false, ...(ngDevMode ? [{ debugName: "filterNull", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
this.debounceTime = input(...(ngDevMode ? [undefined, { debugName: "debounceTime" }] : []));
this.onlyFormatEventData = input(false, ...(ngDevMode ? [{ debugName: "onlyFormatEventData" }] : []));
/*
https://github.com/KillerCodeMonkey/ngx-quill/issues/1257 - fix null value set
provide default empty value
by default null
e.g. defaultEmptyValue="" - empty string
<quill-editor
defaultEmptyValue=""
formControlName="message"
></quill-editor>
*/
this.defaultEmptyValue = input(null, ...(ngDevMode ? [{ debugName: "defaultEmptyValue" }] : []));
this.onEditorCreated = new EventEmitter();
this.onEditorChanged = new EventEmitter();
this.onContentChanged = new EventEmitter();
this.onSelectionChanged = new EventEmitter();
this.onFocus = new EventEmitter();
this.onBlur = new EventEmitter();
this.onNativeFocus = new EventEmitter();
this.onNativeBlur = new EventEmitter();
this.disabled = false; // used to store initial value before ViewInit
this.toolbarPosition = signal('top', ...(ngDevMode ? [{ debugName: "toolbarPosition" }] : []));
this.eventsSubscription = null;
this.quillSubscription = null;
this.elementRef = inject(ElementRef);
this.domSanitizer = inject(DomSanitizer);
this.platformId = inject(PLATFORM_ID);
this.renderer = inject(Renderer2);
this.service = inject(QuillService);
this.destroyRef = inject(DestroyRef);
this.init = false;
this.valueGetter = input(this.getter.bind(this), ...(ngDevMode ? [{ debugName: "valueGetter" }] : []));
this.valueSetter = input((quillEditor, value) => {
const format = getFormat(this.format(), this.service.config.format);
if (format === 'html') {
const sanitize = (typeof this.sanitize() === 'boolean') ? this.sanitize() : (this.service.config.sanitize || false);
if (sanitize) {
value = this.domSanitizer.sanitize(SecurityContext.HTML, value);
}
return quillEditor.clipboard.convert({ html: value });
}
else if (format === 'json') {
try {
return JSON.parse(value);
}
catch {
return [{ insert: value }];
}
}
return value;
}, ...(ngDevMode ? [{ debugName: "valueSetter" }] : []));
this.selectionChangeHandler = (range, oldRange, source) => {
const trackChanges = this.trackChanges() || this.service.config.trackChanges;
const shouldTriggerOnModelTouched = !range && !!this.onModelTouched && (source === 'user' || trackChanges && trackChanges === 'all');
// only emit changes when there's any listener
if (!this.onBlur.observed &&
!this.onFocus.observed &&
!this.onSelectionChanged.observed &&
!shouldTriggerOnModelTouched) {
return;
}
if (range === null) {
this.onBlur.emit({
editor: this.quillEditor,
source
});
}
else if (oldRange === null) {
this.onFocus.emit({
editor: this.quillEditor,
source
});
}
this.onSelectionChanged.emit({
editor: this.quillEditor,
oldRange,
range,
source
});
if (shouldTriggerOnModelTouched) {
this.onModelTouched();
}
};
this.textChangeHandler = (delta, oldDelta, source) => {
const trackChanges = this.trackChanges() || this.service.config.trackChanges;
const shouldTriggerOnModelChange = (source === 'user' || trackChanges && trackChanges === 'all') && !!this.onModelChange;
// only emit changes when there's any listener
if (!this.onContentChanged.observed && !shouldTriggerOnModelChange) {
return;
}
const data = this.eventCallbackFormats();
if (shouldTriggerOnModelChange) {
this.onModelChange(
// only call value getter again if not already done in eventCallbackFormats
data.noFormat ? this.valueGetter()(this.quillEditor) : data[data.format]);
}
this.onContentChanged.emit({
content: data.object,
delta,
editor: this.quillEditor,
html: data.html,
oldDelta,
source,
text: data.text
});
};
this.editorChangeHandler = (event, current, old, source) => {
// only emit changes when there's any listener
if (!this.onEditorChanged.observed) {
return;
}
// only emit changes emitted by user interactions
if (event === 'text-change') {
const data = this.eventCallbackFormats();
this.onEditorChanged.emit({
content: data.object,
delta: current,
editor: this.quillEditor,
event,
html: data.html,
oldDelta: old,
source,
text: data.text
});
}
else {
this.onEditorChanged.emit({
editor: this.quillEditor,
event,
oldRange: old,
range: current,
source
});
}
};
afterNextRender(() => {
if (isPlatformServer(this.platformId)) {
return;
}
// The `quill-editor` component might be destroyed before the `quill` chunk is loaded and its code is executed
// this will lead to runtime exceptions, since the code will be executed on DOM nodes that don't exist within the tree.
this.quillSubscription = this.service.getQuill().pipe(mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))).subscribe(Quill => {
this.editorElem = this.elementRef.nativeElement.querySelector('[quill-editor-element]');
const toolbarElem = this.elementRef.nativeElement.querySelector('[quill-editor-toolbar]');
const modules = Object.assign({}, this.modules() || this.service.config.modules);
if (toolbarElem) {
modules.toolbar = toolbarElem;
}
else if (modules.toolbar === undefined) {
modules.toolbar = defaultModules.toolbar;
}
let placeholder = this.placeholder() !== undefined ? this.placeholder() : this.service.config.placeholder;
if (placeholder === undefined) {
placeholder = 'Insert text here ...';
}
const styles = this.styles();
if (styles) {
this.previousStyles = styles;
Object.keys(styles).forEach((key) => {
this.renderer.setStyle(this.editorElem, key, styles[key]);
});
}
const previousClasses = this.classes();
if (previousClasses) {
this.previousClasses = previousClasses;
this.addClasses(previousClasses);
}
this.customOptions().forEach((customOption) => {
const newCustomOption = Quill.import(customOption.import);
newCustomOption.whitelist = customOption.whitelist;
Quill.register(newCustomOption, true);
});
let bounds = this.bounds() && this.bounds() === 'self' ? this.editorElem : this.bounds();
if (!bounds) {
// Can use global `document` because we execute this only in the browser.
bounds = this.service.config.bounds ? this.service.config.bounds : document.body;
}
let debug = this.debug();
if (!debug && debug !== false && this.service.config.debug) {
debug = this.service.config.debug;
}
let readOnly = this.readOnly();
if (!readOnly && this.readOnly() !== false) {
readOnly = this.service.config.readOnly !== undefined ? this.service.config.readOnly : false;
}
let formats = this.formats();
if (!formats && formats === undefined) {
formats = this.service.config.formats ? [...this.service.config.formats] : (this.service.config.formats === null ? null : undefined);
}
this.quillEditor = new Quill(this.editorElem, {
bounds,
debug,
formats,
modules,
placeholder,
readOnly,
registry: this.registry(),
theme: this.theme() || (this.service.config.theme ? this.service.config.theme : 'snow')
});
if (this.onNativeBlur.observed) {
// https://github.com/quilljs/quill/issues/2186#issuecomment-533401328
fromEvent(this.quillEditor.scroll.domNode, 'blur').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeBlur.next({
editor: this.quillEditor,
source: 'dom'
}));
// https://github.com/quilljs/quill/issues/2186#issuecomment-803257538
const toolbar = this.quillEditor.getModule('toolbar');
if (toolbar.container) {
fromEvent(toolbar.container, 'mousedown').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(e => e.preventDefault());
}
}
if (this.onNativeFocus.observed) {
fromEvent(this.quillEditor.scroll.domNode, 'focus').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.onNativeFocus.next({
editor: this.quillEditor,
source: 'dom'
}));
}
// Set optional link placeholder, Quill has no native API for it so using workaround
if (this.linkPlaceholder()) {
const tooltip = this.quillEditor?.theme?.tooltip;
const input = tooltip?.root?.querySelector('input[data-link]');
if (input?.dataset) {
input.dataset.link = this.linkPlaceholder();
}
}
if (this.content) {
const format = getFormat(this.format(), this.service.config.format);
if (format === 'text') {
this.quillEditor.setText(this.content, 'silent');
}
else {
const valueSetter = this.valueSetter();
const newValue = valueSetter(this.quillEditor, this.content);
this.quillEditor.setContents(newValue, 'silent');
}
const history = this.quillEditor.getModule('history');
history.clear();
}
// initialize disabled status based on this.disabled as default value
this.setDisabledState();
this.addQuillEventListeners();
// listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)="...">`.
if (!this.onEditorCreated.observed && !this.onValidatorChanged) {
this.init = true;
return;
}
if (this.onValidatorChanged) {
this.onValidatorChanged();
}
this.onEditorCreated.emit(this.quillEditor);
this.init = true;
});
});
effect(() => {
const customToolbarPosition = this.customToolbarPosition();
if (this.init && this.toolbarPosition() !== customToolbarPosition) {
this.toolbarPosition.set(customToolbarPosition);
}
});
effect(() => {
const readOnly = this.readOnly();
if (this.init) {
if (readOnly) {
this.quillEditor?.disable();
}
else {
this.quillEditor?.enable(true);
}
}
});
effect(() => {
const placeholder = this.placeholder();
if (this.init && this.quillEditor) {
this.quillEditor.root.dataset.placeholder = placeholder;
}
});
effect(() => {
const styles = this.styles();
if (!this.init || !this.editorElem) {
return;
}
const currentStyling = styles;
const previousStyling = this.previousStyles;
if (previousStyling) {
Object.keys(previousStyling).forEach((key) => {
this.renderer.removeStyle(this.editorElem, key);
});
}
if (currentStyling) {
Object.keys(currentStyling).forEach((key) => {
this.renderer.setStyle(this.editorElem, key, currentStyling[key]);
});
}
});
effect(() => {
const classes = this.classes();
if (!this.init || !this.quillEditor) {
return;
}
const currentClasses = classes;
const previousClasses = this.previousClasses;
if (previousClasses) {
this.removeClasses(previousClasses);
}
if (currentClasses) {
this.addClasses(currentClasses);
}
});
effect(() => {
const debounceTime = this.debounceTime();
if (!this.init || !this.quillEditor) {
return;
}
if (debounceTime) {
this.addQuillEventListeners();
}
});
this.destroyRef.onDestroy(() => {
this.dispose();
this.quillSubscription?.unsubscribe();
this.quillSubscription = null;
});
}
static normalizeClassNames(classes) {
const classList = classes.trim().split(' ');
return classList.reduce((prev, cur) => {
const trimmed = cur.trim();
if (trimmed) {
prev.push(trimmed);
}
return prev;
}, []);
}
addClasses(classList) {
QuillEditorBase.normalizeClassNames(classList).forEach((c) => {
this.renderer.addClass(this.editorElem, c);
});
}
removeClasses(classList) {
QuillEditorBase.normalizeClassNames(classList).forEach((c) => {
this.renderer.removeClass(this.editorElem, c);
});
}
writeValue(currentValue) {
// optional fix for https://github.com/angular/angular/issues/14988
if (this.filterNull() && currentValue === null) {
return;
}
this.content = currentValue;
if (!this.quillEditor) {
return;
}
const format = getFormat(this.format(), this.service.config.format);
const valueSetter = this.valueSetter();
const newValue = valueSetter(this.quillEditor, currentValue);
if (this.compareValues()) {
const currentEditorValue = this.quillEditor.getContents();
if (JSON.stringify(currentEditorValue) === JSON.stringify(newValue)) {
return;
}
}
if (currentValue) {
if (format === 'text') {
this.quillEditor.setText(currentValue);
}
else {
this.quillEditor.setContents(newValue);
}
return;
}
this.quillEditor.setText('');
}
setDisabledState(isDisabled = this.disabled) {
// store initial value to set appropriate disabled status after ViewInit
this.disabled = isDisabled;
if (this.quillEditor) {
if (isDisabled) {
this.quillEditor.disable();
this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'disabled');
}
else {
if (!this.readOnly()) {
this.quillEditor.enable();
}
this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled');
}
}
}
registerOnChange(fn) {
this.onModelChange = fn;
}
registerOnTouched(fn) {
this.onModelTouched = fn;
}
registerOnValidatorChange(fn) {
this.onValidatorChanged = fn;
}
validate() {
if (!this.quillEditor) {
return null;
}
const err = {};
let valid = true;
const text = this.quillEditor.getText();
// trim text if wanted + handle special case that an empty editor contains a new line
const textLength = this.trimOnValidation() ? text.trim().length : (text.length === 1 && text.trim().length === 0 ? 0 : text.length - 1);
const deltaOperations = this.quillEditor.getContents().ops;
const onlyEmptyOperation = !!deltaOperations && deltaOperations.length === 1 && ['\n', ''].includes(deltaOperations[0].insert?.toString() || '');
const minLength = this.minLength();
if (minLength && textLength && textLength < minLength) {
err.minLengthError = {
given: textLength,
minLength
};
valid = false;
}
const maxLength = this.maxLength();
if (maxLength && textLength > maxLength) {
err.maxLengthError = {
given: textLength,
maxLength
};
valid = false;
}
if (this.required() && !textLength && onlyEmptyOperation) {
err.requiredError = {
empty: true
};
valid = false;
}
return valid ? null : err;
}
addQuillEventListeners() {
this.dispose();
this.eventsSubscription = new Subscription();
this.eventsSubscription.add(
// mark model as touched if editor lost focus
fromEvent(this.quillEditor, 'selection-change').subscribe(([range, oldRange, source]) => {
this.selectionChangeHandler(range, oldRange, source);
}));
// The `fromEvent` supports passing JQuery-style event targets, the editor has `on` and `off` methods which
// will be invoked upon subscription and teardown.
let textChange$ = fromEvent(this.quillEditor, 'text-change');
let editorChange$ = fromEvent(this.quillEditor, 'editor-change');
const _debounceTime = this.debounceTime();
if (typeof _debounceTime === 'number') {
textChange$ = textChange$.pipe(debounceTime(_debounceTime));
editorChange$ = editorChange$.pipe(debounceTime(_debounceTime));
}
this.eventsSubscription.add(
// update model if text changes
textChange$.subscribe(([delta, oldDelta, source]) => {
this.textChangeHandler(delta, oldDelta, source);
}));
this.eventsSubscription.add(
// triggered if selection or text changed
editorChange$.subscribe(([event, current, old, source]) => {
this.editorChangeHandler(event, current, old, source);
}));
}
dispose() {
this.eventsSubscription?.unsubscribe();
this.eventsSubscription = null;
}
isEmptyValue(html) {
return html === '<p></p>' || html === '<div></div>' || html === '<p><br></p>' || html === '<div><br></div>';
}
getter(quillEditor, forceFormat) {
let modelValue = null;
const format = forceFormat ?? getFormat(this.format(), this.service.config.format);
if (format === 'html') {
let html = quillEditor.getSemanticHTML();
if (this.isEmptyValue(html)) {
html = this.defaultEmptyValue();
}
modelValue = html;
}
else if (format === 'text') {
modelValue = quillEditor.getText();
}
else if (format === 'object') {
modelValue = quillEditor.getContents();
}
else if (format === 'json') {
try {
modelValue = JSON.stringify(quillEditor.getContents());
}
catch {
modelValue = quillEditor.getText();
}
}
return modelValue;
}
eventCallbackFormats() {
const format = getFormat(this.format(), this.service.config.format);
const onlyFormat = this.onlyFormatEventData() === true;
const noFormat = this.onlyFormatEventData() === 'none';
let text = null;
let html = null;
let object = null;
let json = null;
// do nothing if no formatted value needed
if (noFormat) {
return {
format,
onlyFormat,
noFormat,
text,
object,
json,
html
};
}
// use getter input to grab value
const value = this.valueGetter()(this.quillEditor);
if (format === 'text') {
text = value;
}
else if (format === 'html') {
html = value;
}
else if (format === 'object') {
object = value;
json = JSON.stringify(value);
}
else if (format === 'json') {
json = value;
object = JSON.parse(value);
}
// return current values, if only the editor format is needed
if (onlyFormat) {
return {
format,
onlyFormat,
noFormat,
text,
json,
html,
object
};
}
// return all format values
return {
format,
onlyFormat,
noFormat,
// use internal getter to retrieve correct other values - this.valueGetter can be overwritten
text: format === 'text' ? text : this.getter(this.quillEditor, 'text'),
json: format === 'json' ? json : this.getter(this.quillEditor, 'json'),
html: format === 'html' ? html : this.getter(this.quillEditor, 'html'),
object: format === 'object' ? object : this.getter(this.quillEditor, 'object')
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.0", type: QuillEditorBase, isStandalone: true, inputs: { format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, modules: { classPropertyName: "modules", publicName: "modules", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, formats: { classPropertyName: "formats", publicName: "formats", isSignal: true, isRequired: false, transformFunction: null }, customToolbarPosition: { classPropertyName: "customToolbarPosition", publicName: "customToolbarPosition", isSignal: true, isRequired: false, transformFunction: null }, sanitize: { classPropertyName: "sanitize", publicName: "sanitize", isSignal: true, isRequired: false, transformFunction: null }, beforeRender: { classPropertyName: "beforeRender", publicName: "beforeRender", isSignal: true, isRequired: false, transformFunction: null }, styles: { classPropertyName: "styles", publicName: "styles", isSignal: true, isRequired: false, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: false, transformFunction: null }, bounds: { classPropertyName: "bounds", publicName: "bounds", isSignal: true, isRequired: false, transformFunction: null }, customOptions: { classPropertyName: "customOptions", publicName: "customOptions", isSignal: true, isRequired: false, transformFunction: null }, customModules: { classPropertyName: "customModules", publicName: "customModules", isSignal: true, isRequired: false, transformFunction: null }, trackChanges: { classPropertyName: "trackChanges", publicName: "trackChanges", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null }, trimOnValidation: { classPropertyName: "trimOnValidation", publicName: "trimOnValidation", isSignal: true, isRequired: false, transformFunction: null }, linkPlaceholder: { classPropertyName: "linkPlaceholder", publicName: "linkPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, compareValues: { classPropertyName: "compareValues", publicName: "compareValues", isSignal: true, isRequired: false, transformFunction: null }, filterNull: { classPropertyName: "filterNull", publicName: "filterNull", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, onlyFormatEventData: { classPropertyName: "onlyFormatEventData", publicName: "onlyFormatEventData", isSignal: true, isRequired: false, transformFunction: null }, defaultEmptyValue: { classPropertyName: "defaultEmptyValue", publicName: "defaultEmptyValue", isSignal: true, isRequired: false, transformFunction: null }, valueGetter: { classPropertyName: "valueGetter", publicName: "valueGetter", isSignal: true, isRequired: false, transformFunction: null }, valueSetter: { classPropertyName: "valueSetter", publicName: "valueSetter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onEditorCreated: "onEditorCreated", onEditorChanged: "onEditorChanged", onContentChanged: "onContentChanged", onSelectionChanged: "onSelectionChanged", onFocus: "onFocus", onBlur: "onBlur", onNativeFocus: "onNativeFocus", onNativeBlur: "onNativeBlur" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorBase, decorators: [{
type: Directive
}], ctorParameters: () => [], propDecorators: { format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], modules: [{ type: i0.Input, args: [{ isSignal: true, alias: "modules", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], minLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minLength", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], formats: [{ type: i0.Input, args: [{ isSignal: true, alias: "formats", required: false }] }], customToolbarPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "customToolbarPosition", required: false }] }], sanitize: [{ type: i0.Input, args: [{ isSignal: true, alias: "sanitize", required: false }] }], beforeRender: [{ type: i0.Input, args: [{ isSignal: true, alias: "beforeRender", required: false }] }], styles: [{ type: i0.Input, args: [{ isSignal: true, alias: "styles", required: false }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], bounds: [{ type: i0.Input, args: [{ isSignal: true, alias: "bounds", required: false }] }], customOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "customOptions", required: false }] }], customModules: [{ type: i0.Input, args: [{ isSignal: true, alias: "customModules", required: false }] }], trackChanges: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackChanges", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }], trimOnValidation: [{ type: i0.Input, args: [{ isSignal: true, alias: "trimOnValidation", required: false }] }], linkPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "linkPlaceholder", required: false }] }], compareValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareValues", required: false }] }], filterNull: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterNull", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], onlyFormatEventData: [{ type: i0.Input, args: [{ isSignal: true, alias: "onlyFormatEventData", required: false }] }], defaultEmptyValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultEmptyValue", required: false }] }], onEditorCreated: [{
type: Output
}], onEditorChanged: [{
type: Output
}], onContentChanged: [{
type: Output
}], onSelectionChanged: [{
type: Output
}], onFocus: [{
type: Output
}], onBlur: [{
type: Output
}], onNativeFocus: [{
type: Output
}], onNativeBlur: [{
type: Output
}], valueGetter: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueGetter", required: false }] }], valueSetter: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueSetter", required: false }] }] } });
class QuillEditorComponent extends QuillEditorBase {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.0", type: QuillEditorComponent, isStandalone: true, selector: "quill-editor", providers: [
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QuillEditorComponent)
},
{
multi: true,
provide: NG_VALIDATORS,
useExisting: forwardRef(() => QuillEditorComponent)
}
], usesInheritance: true, ngImport: i0, template: `
@if (toolbarPosition() !== 'top') {
<div quill-editor-element></div>
}
<ng-content select="[above-quill-editor-toolbar]"></ng-content>
<ng-content select="[quill-editor-toolbar]"></ng-content>
<ng-content select="[below-quill-editor-toolbar]"></ng-content>
@if (toolbarPosition() === 'top') {
<div quill-editor-element></div>
}
`, isInline: true, styles: [":host{display:inline-block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillEditorComponent, decorators: [{
type: Component,
args: [{ encapsulation: ViewEncapsulation.Emulated, providers: [
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QuillEditorComponent)
},
{
multi: true,
provide: NG_VALIDATORS,
useExisting: forwardRef(() => QuillEditorComponent)
}
], changeDetection: ChangeDetectionStrategy.OnPush, selector: 'quill-editor', template: `
@if (toolbarPosition() !== 'top') {
<div quill-editor-element></div>
}
<ng-content select="[above-quill-editor-toolbar]"></ng-content>
<ng-content select="[quill-editor-toolbar]"></ng-content>
<ng-content select="[below-quill-editor-toolbar]"></ng-content>
@if (toolbarPosition() === 'top') {
<div quill-editor-element></div>
}
`, styles: [":host{display:inline-block}\n"] }]
}] });
class QuillViewHTMLComponent {
constructor() {
this.content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : []));
this.sanitize = input(...(ngDevMode ? [undefined, { debugName: "sanitize" }] : []));
this.innerHTML = computed(() => {
const sanitize = this.sanitize();
const content = this.content();
return ((typeof sanitize === 'boolean') ? sanitize : (this.service.config.sanitize || false)) ? content : this.sanitizer.bypassSecurityTrustHtml(content);
}, ...(ngDevMode ? [{ debugName: "innerHTML" }] : []));
this.themeClass = computed(() => {
const base = this.service.config.theme ? this.service.config.theme : 'snow';
return `ql-${this.theme() || base} ngx-quill-view-html`;
}, ...(ngDevMode ? [{ debugName: "themeClass" }] : []));
this.sanitizer = inject(DomSanitizer);
this.service = inject(QuillService);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillViewHTMLComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.0", type: QuillViewHTMLComponent, isStandalone: true, selector: "quill-view-html", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, sanitize: { classPropertyName: "sanitize", publicName: "sanitize", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
<div class="ql-container" [class]="themeClass()">
<div class="ql-editor" [innerHTML]="innerHTML()">
</div>
</div>
`, isInline: true, styles: [".ql-container.ngx-quill-view-html{border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillViewHTMLComponent, decorators: [{
type: Component,
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, selector: 'quill-view-html', template: `
<div class="ql-container" [class]="themeClass()">
<div class="ql-editor" [innerHTML]="innerHTML()">
</div>
</div>
`, styles: [".ql-container.ngx-quill-view-html{border:0}\n"] }]
}], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], sanitize: [{ type: i0.Input, args: [{ isSignal: true, alias: "sanitize", required: false }] }] } });
class QuillViewComponent {
constructor() {
this.format = input(...(ngDevMode ? [undefined, { debugName: "format" }] : []));
this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : []));
this.modules = input(...(ngDevMode ? [undefined, { debugName: "modules" }] : []));
this.debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : []));
this.formats = input(...(ngDevMode ? [undefined, { debugName: "formats" }] : []));
this.sanitize = input(...(ngDevMode ? [undefined, { debugName: "sanitize" }] : []));
this.beforeRender = input(...(ngDevMode ? [undefined, { debugName: "beforeRender" }] : []));
this.strict = input(true, ...(ngDevMode ? [{ debugName: "strict" }] : []));
this.content = input(...(ngDevMode ? [undefined, { debugName: "content" }] : []));
this.customModules = input([], ...(ngDevMode ? [{ debugName: "customModules" }] : []));
this.customOptions = input([], ...(ngDevMode ? [{ debugName: "customOptions" }] : []));
this.onEditorCreated = new EventEmitter();
this.init = false;
this.elementRef = inject(ElementRef);
this.renderer = inject(Renderer2);
this.service = inject(QuillService);
this.sanitizer = inject(DomSanitizer);
this.platformId = inject(PLATFORM_ID);
this.destroyRef = inject(DestroyRef);
this.valueSetter = (quillEditor, value) => {
const format = getFormat(this.format(), this.service.config.format);
let content = value;
if (format === 'text') {
quillEditor.setText(content);
}
else {
if (format === 'html') {
const sanitize = (typeof this.sanitize() === 'boolean') ? this.sanitize() : (this.service.config.sanitize || false);
if (sanitize) {
value = this.sanitizer.sanitize(SecurityContext.HTML, value);
}
content = quillEditor.clipboard.convert({ html: value });
}
else if (format === 'json') {
try {
content = JSON.parse(value);
}
catch {
content = [{ insert: value }];
}
}
quillEditor.setContents(content);
}
};
afterNextRender(() => {
if (isPlatformServer(this.platformId)) {
return;
}
const quillSubscription = this.service.getQuill().pipe(mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))).subscribe(Quill => {
const modules = Object.assign({}, this.modules() || this.service.config.modules);
modules.toolbar = false;
this.customOptions().forEach((customOption) => {
const newCustomOption = Quill.import(customOption.import);
newCustomOption.whitelist = customOption.whitelist;
Quill.register(newCustomOption, true);
});
let debug = this.debug();
if (!debug && debug !== false && this.service.config.debug) {
debug = this.service.config.debug;
}
let formats = this.formats();
if (formats === undefined) {
formats = this.service.config.formats ? [...this.service.config.formats] : (this.service.config.formats === null ? null : undefined);
}
const theme = this.theme() || (this.service.config.theme ? this.service.config.theme : 'snow');
this.editorElem = this.elementRef.nativeElement.querySelector('[quill-view-element]');
this.quillEditor = new Quill(this.editorElem, {
debug,
formats,
modules,
readOnly: true,
strict: this.strict(),
theme
});
this.renderer.addClass(this.editorElem, 'ngx-quill-view');
if (this.content()) {
this.valueSetter(this.quillEditor, this.content());
}
// listening to the `onEditorCreated` event inside the template, for instance `<quill-view (onEditorCreated)="...">`.
if (!this.onEditorCreated.observed) {
this.init = true;
return;
}
this.onEditorCreated.emit(this.quillEditor);
this.init = true;
});
this.destroyRef.onDestroy(() => quillSubscription.unsubscribe());
});
effect(() => {
const content = this.content();
if (!this.quillEditor || !this.init) {
return;
}
if (content) {
this.valueSetter(this.quillEditor, content);
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: QuillViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.0", type: QuillViewComponent, isStandalone: true, selector: "quill-view", inputs: { format: { classPropertyName: "format", publicName: "format", isSignal