ngx-editor
Version:
The Rich Text Editor for Angular, Built on ProseMirror
946 lines (922 loc) • 153 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, ViewChild, Output, Input, Component, ApplicationRef, createComponent, forwardRef, ViewEncapsulation, Pipe, Injectable, Optional, HostListener, HostBinding, NgModule, InjectionToken } from '@angular/core';
import * as i4 from '@angular/forms';
import { NG_VALUE_ACCESSOR, FormGroup, FormControl, Validators as Validators$1, ReactiveFormsModule } from '@angular/forms';
import { Subject, of, isObservable, fromEvent, asyncScheduler } from 'rxjs';
import { takeUntil, throttleTime } from 'rxjs/operators';
import { NgxEditorError, clamp, uniq, isNil } from 'ngx-editor/utils';
import { DOMSerializer, DOMParser, Fragment, Slice } from 'prosemirror-model';
import { schema } from 'ngx-editor/schema';
export { marks, nodes, schema } from 'ngx-editor/schema';
import { Plugin, PluginKey, NodeSelection, Selection, EditorState } from 'prosemirror-state';
import { DecorationSet, Decoration, EditorView } from 'prosemirror-view';
import * as i3 from '@angular/common';
import { CommonModule, AsyncPipe } from '@angular/common';
import * as i1 from '@angular/platform-browser';
import { toggleMark, lift, wrapIn, setBlockType, newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock, chainCommands, exitCode, baseKeymap } from 'prosemirror-commands';
import { applyMark, removeLink, removeMark } from 'ngx-editor/commands';
import { isMarkActive, isNodeActive, canInsert, getSelectionNodes, getSelectionMarks, markInputRule } from 'ngx-editor/helpers';
import { liftListItem, wrapInList, sinkListItem, splitListItem } from 'prosemirror-schema-list';
import { undo as undo$1, redo as redo$1, history } from 'prosemirror-history';
import { computePosition, offset, autoPlacement, detectOverflow } from '@floating-ui/dom';
import { keymap } from 'prosemirror-keymap';
import { wrappingInputRule, textblockTypeInputRule, smartQuotes, ellipsis, emDash, inputRules } from 'prosemirror-inputrules';
const isString = (value) => {
return typeof value === 'string';
};
const getTrustedTypes = () => {
if (typeof window === 'undefined') {
return undefined;
}
return window.trustedTypes;
};
const isTrustedHtml = (value) => {
return getTrustedTypes()?.isHTML(value) ?? false;
};
const isHtml = (value) => {
return isString(value) || isTrustedHtml(value);
};
const emptyDoc = {
type: 'doc',
content: [
{
type: 'paragraph',
},
],
};
// https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
const toHTML = (json, inputSchema) => {
const schema$1 = inputSchema ?? schema;
const contentNode = schema$1.nodeFromJSON(json);
const html = DOMSerializer.fromSchema(schema$1).serializeFragment(contentNode.content);
const div = document.createElement('div');
div.appendChild(html);
return div.innerHTML;
};
const toDoc = (html, inputSchema, options) => {
const schema$1 = inputSchema ?? schema;
const el = document.createElement('div');
el.innerHTML = html;
return DOMParser.fromSchema(schema$1).parse(el, options).toJSON();
};
const parseContent = (value, schema, options) => {
if (!value) {
return schema.nodeFromJSON(emptyDoc);
}
if (!isHtml(value)) {
return schema.nodeFromJSON(value);
}
const docJson = toDoc(value, schema, options);
return schema.nodeFromJSON(docJson);
};
const editablePlugin = (editable = true) => {
return new Plugin({
key: new PluginKey('editable'),
state: {
init() {
return editable;
},
apply(tr, previousVal) {
return tr.getMeta('UPDATE_EDITABLE') ?? previousVal;
},
},
props: {
editable(state) {
return this.getState(state);
},
attributes(state) {
const isEnabled = this.getState(state);
if (isEnabled) {
return null;
}
return {
class: 'NgxEditor__Content--Disabled',
};
},
},
});
};
const PLACEHOLDER_CLASSNAME = 'NgxEditor__Placeholder';
const placeholderPlugin = (text) => {
return new Plugin({
key: new PluginKey('placeholder'),
state: {
init() {
return text ?? '';
},
apply(tr, previousVal) {
const placeholder = tr.getMeta('UPDATE_PLACEHOLDER') ?? previousVal;
return placeholder;
},
},
props: {
decorations(state) {
const { doc } = state;
const { textContent, childCount } = doc;
const placeholder = this.getState(state);
if (!placeholder || childCount > 1) {
return DecorationSet.empty;
}
const decorations = [];
const decorate = (node, pos) => {
if (node.type.isBlock && node.childCount === 0 && textContent.length === 0) {
const from = pos;
const to = pos + node.nodeSize;
const placeholderNode = Decoration.node(from, to, {
'class': PLACEHOLDER_CLASSNAME,
'data-placeholder': placeholder,
'data-align': node.attrs['align'] ?? null,
});
decorations.push(placeholderNode);
}
return false;
};
doc.descendants(decorate);
return DecorationSet.create(doc, decorations);
},
},
});
};
const attributesPlugin = (attributes = {}) => {
return new Plugin({
key: new PluginKey('attributes'),
props: {
attributes,
},
});
};
const focusPlugin = (cb) => {
return new Plugin({
key: new PluginKey('focus'),
props: {
handleDOMEvents: {
focus: () => {
cb();
return false;
},
},
},
});
};
const blurPlugin = (cb) => {
return new Plugin({
key: new PluginKey('blur'),
props: {
handleDOMEvents: {
blur: () => {
cb();
return false;
},
},
},
});
};
class ImageViewComponent {
src;
alt = '';
title = '';
outerWidth = '';
selected = false;
view;
imageResize = new EventEmitter();
imgEl;
startResizing(e, direction) {
e.preventDefault();
this.resizeImage(e, direction);
}
resizeImage(evt, direction) {
const startX = evt.pageX;
const startWidth = this.imgEl.nativeElement.clientWidth;
const isLeftResize = direction === 'left';
const { width } = window.getComputedStyle(this.view.dom);
const editorWidth = parseInt(width, 10);
const onMouseMove = (e) => {
const currentX = e.pageX;
const diffInPx = currentX - startX;
const computedWidth = isLeftResize ? startWidth - diffInPx : startWidth + diffInPx;
// prevent image overflow the editor
// prevent resizng below 20px
if (computedWidth > editorWidth || computedWidth < 20) {
return;
}
this.outerWidth = `${computedWidth}px`;
};
const onMouseUp = (e) => {
e.preventDefault();
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this.imageResize.emit();
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: ImageViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.8", type: ImageViewComponent, isStandalone: true, selector: "ngx-image-view", inputs: { src: "src", alt: "alt", title: "title", outerWidth: "outerWidth", selected: "selected", view: "view" }, outputs: { imageResize: "imageResize" }, viewQueries: [{ propertyName: "imgEl", first: true, predicate: ["imgEl"], descendants: true, static: true }], ngImport: i0, template: "<span class=\"NgxEditor__ImageWrapper\" [ngClass]=\"{ 'NgxEditor__Resizer--Active': selected }\" [style.width]=\"outerWidth\">\n <span class=\"NgxEditor__ResizeHandle\" *ngIf=\"selected\">\n <span class=\"NgxEditor__ResizeHandle--TL\" (mousedown)=\"startResizing($event, 'left')\"></span>\n <span class=\"NgxEditor__ResizeHandle--TR\" (mousedown)=\"startResizing($event, 'right')\"></span>\n <span class=\"NgxEditor__ResizeHandle--BL\" (mousedown)=\"startResizing($event, 'left')\"></span>\n <span class=\"NgxEditor__ResizeHandle--BR\" (mousedown)=\"startResizing($event, 'right')\"></span>\n </span>\n <img [src]=\"src\" [alt]=\"alt\" [title]=\"title\" #imgEl />\n</span>\n", styles: ["*,*:before,*:after{box-sizing:border-box}img{width:100%;height:100%}.NgxEditor__ImageWrapper{position:relative;display:inline-block;line-height:0;padding:2px}.NgxEditor__ImageWrapper.NgxEditor__Resizer--Active{padding:1px;border:1px solid #1a73e8}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle{position:absolute;height:100%;width:100%}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TL,.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BL,.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TR,.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BR{position:absolute;width:7px;height:7px;background-color:#1a73e8;border:1px solid white}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BR{bottom:-5px;right:-5px;cursor:se-resize}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TR{top:-5px;right:-5px;cursor:ne-resize}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TL{top:-5px;left:-5px;cursor:nw-resize}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BL{bottom:-5px;left:-5px;cursor:sw-resize}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: ImageViewComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-image-view', imports: [CommonModule], template: "<span class=\"NgxEditor__ImageWrapper\" [ngClass]=\"{ 'NgxEditor__Resizer--Active': selected }\" [style.width]=\"outerWidth\">\n <span class=\"NgxEditor__ResizeHandle\" *ngIf=\"selected\">\n <span class=\"NgxEditor__ResizeHandle--TL\" (mousedown)=\"startResizing($event, 'left')\"></span>\n <span class=\"NgxEditor__ResizeHandle--TR\" (mousedown)=\"startResizing($event, 'right')\"></span>\n <span class=\"NgxEditor__ResizeHandle--BL\" (mousedown)=\"startResizing($event, 'left')\"></span>\n <span class=\"NgxEditor__ResizeHandle--BR\" (mousedown)=\"startResizing($event, 'right')\"></span>\n </span>\n <img [src]=\"src\" [alt]=\"alt\" [title]=\"title\" #imgEl />\n</span>\n", styles: ["*,*:before,*:after{box-sizing:border-box}img{width:100%;height:100%}.NgxEditor__ImageWrapper{position:relative;display:inline-block;line-height:0;padding:2px}.NgxEditor__ImageWrapper.NgxEditor__Resizer--Active{padding:1px;border:1px solid #1a73e8}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle{position:absolute;height:100%;width:100%}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TL,.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BL,.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TR,.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BR{position:absolute;width:7px;height:7px;background-color:#1a73e8;border:1px solid white}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BR{bottom:-5px;right:-5px;cursor:se-resize}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TR{top:-5px;right:-5px;cursor:ne-resize}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--TL{top:-5px;left:-5px;cursor:nw-resize}.NgxEditor__ImageWrapper .NgxEditor__ResizeHandle .NgxEditor__ResizeHandle--BL{bottom:-5px;left:-5px;cursor:sw-resize}\n"] }]
}], propDecorators: { src: [{
type: Input
}], alt: [{
type: Input
}], title: [{
type: Input
}], outerWidth: [{
type: Input
}], selected: [{
type: Input
}], view: [{
type: Input
}], imageResize: [{
type: Output
}], imgEl: [{
type: ViewChild,
args: ['imgEl', { static: true }]
}] } });
class ImageRezieView {
dom;
view;
getPos;
applicationRef;
imageComponentRef;
resizeSubscription;
node;
updating = false;
constructor(node, view, getPos, injector) {
this.applicationRef = injector.get(ApplicationRef);
// create component ref
this.imageComponentRef = createComponent(ImageViewComponent, {
environmentInjector: this.applicationRef.injector,
});
// Attach to the view so that the change detector knows to run
this.applicationRef.attachView(this.imageComponentRef.hostView);
this.setNodeAttributes(node.attrs);
this.imageComponentRef.instance.view = view;
this.dom = this.imageComponentRef.location.nativeElement;
this.view = view;
this.node = node;
this.getPos = getPos;
this.resizeSubscription = this.imageComponentRef.instance.imageResize.subscribe(() => {
this.handleResize();
});
}
computeChanges(prevAttrs, newAttrs) {
return JSON.stringify(prevAttrs) === JSON.stringify(newAttrs);
}
setNodeAttributes(attrs) {
this.imageComponentRef.instance.src = attrs['src'];
this.imageComponentRef.instance.alt = attrs['alt'];
this.imageComponentRef.instance.title = attrs['title'];
this.imageComponentRef.instance.outerWidth = attrs['width'];
}
handleResize = () => {
if (this.updating) {
return;
}
const { state, dispatch } = this.view;
const { tr } = state;
const transaction = tr.setNodeMarkup(this.getPos(), undefined, {
...this.node.attrs,
width: this.imageComponentRef.instance.outerWidth,
});
const resolvedPos = transaction.doc.resolve(this.getPos());
const newSelection = new NodeSelection(resolvedPos);
transaction.setSelection(newSelection);
dispatch(transaction);
};
update(node) {
if (node.type !== this.node.type) {
return false;
}
this.node = node;
const changed = this.computeChanges(this.node.attrs, node.attrs);
if (changed) {
this.updating = true;
this.setNodeAttributes(node.attrs);
this.updating = false;
}
return true;
}
ignoreMutation() {
return true;
}
selectNode() {
this.imageComponentRef.instance.selected = true;
}
deselectNode() {
this.imageComponentRef.instance.selected = false;
}
destroy() {
this.resizeSubscription.unsubscribe();
this.applicationRef.detachView(this.imageComponentRef.hostView);
}
}
const imageResizePlugin = (injector) => {
return new Plugin({
key: new PluginKey('image-resize'),
props: {
nodeViews: {
image: (node, view, getPos) => {
return new ImageRezieView(node, view, getPos, injector);
},
},
},
});
};
const HTTP_LINK_REGEX = /(?:https?:\/\/)?[\w-]+(?:\.[\w-]+)+\.?(?:\d+)?(?:\/\S*)?$/;
const linkify = (fragment) => {
const linkified = [];
fragment.forEach((child) => {
if (child.isText) {
const text = child.text;
let pos = 0;
const match = HTTP_LINK_REGEX.exec(text);
if (match) {
const start = match.index;
const end = start + match[0].length;
const { link } = child.type.schema.marks;
if (start > 0) {
linkified.push(child.cut(pos, start));
}
const urlText = text.slice(start, end);
linkified.push(child.cut(start, end).mark(link.create({ href: urlText }).addToSet(child.marks)));
pos = end;
}
if (pos < text.length) {
linkified.push(child.cut(pos));
}
}
else {
linkified.push(child.copy(linkify(child.content)));
}
});
return Fragment.fromArray(linkified);
};
const linkifyPlugin = () => {
return new Plugin({
key: new PluginKey('linkify'),
props: {
transformPasted: (slice) => {
return new Slice(linkify(slice.content), slice.openStart, slice.openEnd);
},
},
});
};
class NgxEditorComponent {
renderer;
injector;
elementRef;
constructor(renderer, injector, elementRef) {
this.renderer = renderer;
this.injector = injector;
this.elementRef = elementRef;
}
ngxEditor;
editor;
outputFormat;
placeholder = 'Type Here...';
focusOut = new EventEmitter();
focusIn = new EventEmitter();
unsubscribe = new Subject();
onChange = () => { };
onTouched = () => { };
writeValue(value) {
if (!this.outputFormat && isHtml(value)) {
this.outputFormat = 'html';
}
this.editor.setContent(value ?? emptyDoc);
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.setMeta('UPDATE_EDITABLE', !isDisabled);
this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled);
}
handleChange(jsonDoc) {
if (this.outputFormat === 'html') {
const html = toHTML(jsonDoc, this.editor.schema);
this.onChange(html);
return;
}
this.onChange(jsonDoc);
}
setMeta(key, value) {
const { dispatch, state: { tr } } = this.editor.view;
dispatch(tr.setMeta(key, value));
}
setPlaceholder(placeholder) {
this.setMeta('UPDATE_PLACEHOLDER', placeholder);
}
registerPlugins() {
this.editor.registerPlugin(editablePlugin());
this.editor.registerPlugin(placeholderPlugin(this.placeholder));
this.editor.registerPlugin(attributesPlugin({ class: 'NgxEditor__Content' }));
this.editor.registerPlugin(focusPlugin(() => {
this.focusIn.emit();
}));
this.editor.registerPlugin(blurPlugin(() => {
this.focusOut.emit();
this.onTouched();
}));
if (this.editor.features.resizeImage) {
this.editor.registerPlugin(imageResizePlugin(this.injector));
}
if (this.editor.features.linkOnPaste) {
this.editor.registerPlugin(linkifyPlugin());
}
}
ngOnInit() {
if (!this.editor) {
throw new NgxEditorError('Required editor instance for initializing editor component');
}
this.registerPlugins();
this.renderer.appendChild(this.ngxEditor.nativeElement, this.editor.view.dom);
this.editor.valueChanges.pipe(takeUntil(this.unsubscribe)).subscribe((jsonDoc) => {
this.handleChange(jsonDoc);
});
}
ngOnChanges(changes) {
if (changes['placeholder'] && !changes['placeholder'].isFirstChange()) {
this.setPlaceholder(changes['placeholder'].currentValue);
}
}
ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: NgxEditorComponent, deps: [{ token: i0.Renderer2 }, { token: i0.Injector }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.8", type: NgxEditorComponent, isStandalone: true, selector: "ngx-editor", inputs: { editor: "editor", outputFormat: "outputFormat", placeholder: "placeholder" }, outputs: { focusOut: "focusOut", focusIn: "focusIn" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxEditorComponent),
multi: true,
},
], viewQueries: [{ propertyName: "ngxEditor", first: true, predicate: ["ngxEditor"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"NgxEditor\" #ngxEditor>\n <ng-content></ng-content>\n</div>\n", styles: [":root{--ngx-editor-border-radius: 4px;--ngx-editor-background-color: #fff;--ngx-editor-text-color: #000;--ngx-editor-placeholder-color: #6c757d;--ngx-editor-border-color: rgba(0, 0, 0, .2);--ngx-editor-wrapper-border-color: rgba(0, 0, 0, .2);--ngx-editor-menubar-bg-color: #fff;--ngx-editor-menubar-padding: 3px;--ngx-editor-menubar-height: 30px;--ngx-editor-blockquote-color: #ddd;--ngx-editor-blockquote-border-width: 3px;--ngx-editor-icon-size: 30px;--ngx-editor-popup-bg-color: #fff;--ngx-editor-popup-border-radius: 4px;--ngx-editor-popup-shadow: rgba(60, 64, 67, .15) 0px 2px 6px 2px;--ngx-editor-menu-item-border-radius: 2px;--ngx-editor-menu-item-active-color: #1a73e8;--ngx-editor-menu-item-hover-bg-color: #f1f1f1;--ngx-editor-menu-item-active-bg-color: #e8f0fe;--ngx-editor-seperator-color: #ccc;--ngx-editor-bubble-bg-color: #000;--ngx-editor-bubble-text-color: #fff;--ngx-editor-bubble-item-hover-color: #636262;--ngx-editor-bubble-seperator-color: #fff;--ngx-editor-focus-ring-color: #5e9ed6;--ngx-editor-error-color: red;--ngx-editor-click-pointer: default}.NgxEditor{background:var(--ngx-editor-background-color);color:var(--ngx-editor-text-color);background-clip:padding-box;border-radius:var(--ngx-editor-border-radius);border:1px solid var(--ngx-editor-border-color);position:relative}.NgxEditor--Disabled{opacity:.5!important;pointer-events:none!important}.NgxEditor__Placeholder:before{color:var(--ngx-editor-placeholder-color);opacity:1;-webkit-user-select:none;user-select:none;position:absolute;cursor:text;content:attr(data-placeholder)}.NgxEditor__Placeholder[data-align=right]:before{position:relative}.NgxEditor__Content{padding:8px;white-space:pre-wrap;outline:none;font-variant-ligatures:none;font-feature-settings:\"liga\" 0}.NgxEditor__Content p{margin:0 0 10px}.NgxEditor__Content blockquote{padding-left:16px;border-left:var(--ngx-editor-blockquote-border-width) solid var(--ngx-editor-blockquote-color);margin-left:0;margin-right:0}.NgxEditor__Content--Disabled{-webkit-user-select:none;user-select:none;pointer-events:none}.NgxEditor__Wrapper{border:1px solid var(--ngx-editor-wrapper-border-color);border-radius:var(--ngx-editor-border-radius)}.NgxEditor__Wrapper .NgxEditor__MenuBar{border-top-left-radius:var(--ngx-editor-border-radius);border-top-right-radius:var(--ngx-editor-border-radius);border-bottom:1px solid var(--ngx-editor-border-color)}.NgxEditor__Wrapper .NgxEditor{border-top-left-radius:0;border-top-right-radius:0;border:none}.NgxEditor__MenuBar{display:flex;flex-wrap:wrap;padding:var(--ngx-editor-menubar-padding);background-color:var(--ngx-editor-menubar-bg-color);gap:.25rem .1rem}.NgxEditor__MenuBar button:not(:disabled),.NgxEditor__MenuBar [role=button]:not(:disabled){cursor:var(--ngx-editor-click-pointer, default)}.NgxEditor__MenuItem{display:flex;align-items:center;justify-content:center;position:relative;flex-shrink:0}.NgxEditor__MenuItem.NgxEditor__MenuItem--IconContainer{display:flex;align-items:center;justify-content:center}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon{all:unset;appearance:none;height:var(--ngx-editor-icon-size);width:var(--ngx-editor-icon-size);transition:.2s ease-in-out;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--ngx-editor-menu-item-border-radius)}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon+.NgxEditor__MenuItem--Icon{margin-left:2px}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon:focus-visible{outline:1px solid var(--ngx-editor-focus-ring-color)}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__MenuItem.NgxEditor__MenuItem--Text{padding:0 5px}.NgxEditor__MenuItem.NgxEditor__MenuItem--Active,.NgxEditor__MenuItem .NgxEditor__MenuItem--Active{background-color:var(--ngx-editor-menu-item-active-bg-color);color:var(--ngx-editor-menu-item-active-color)}.NgxEditor__Dropdown{min-width:64px;position:relative;display:flex;align-items:center;flex-shrink:0}.NgxEditor__Dropdown:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Text{all:unset;appearance:none;display:flex;align-items:center;justify-content:center;padding:0 5px;height:100%;width:100%}.NgxEditor__Dropdown .NgxEditor__Dropdown--Text:focus-visible{outline:1px solid var(--ngx-editor-focus-ring-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Text:after{display:inline-block;content:\"\";margin-left:24px;vertical-align:4px;border-top:4px solid;border-right:4px solid transparent;border-bottom:0;border-left:4px solid transparent}.NgxEditor__Dropdown .NgxEditor__Dropdown--DropdownMenu{position:absolute;left:0;box-shadow:var(--ngx-editor-popup-shadow);border-radius:var(--ngx-editor-popup-border-radius);background-color:var(--ngx-editor-popup-bg-color);z-index:10;width:100%;top:calc(var(--ngx-editor-menubar-height) + 2px);display:flex;flex-direction:column}.NgxEditor__Dropdown .NgxEditor__Dropdown--Item{all:unset;appearance:none;padding:8px;white-space:nowrap;color:inherit}.NgxEditor__Dropdown .NgxEditor__Dropdown--Item:focus-visible{outline:1px solid var(--ngx-editor-focus-ring-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Item:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Selected,.NgxEditor__Dropdown .NgxEditor__Dropdown--Open{color:var(--ngx-editor-menu-item-active-color);background-color:var(--ngx-editor-menu-item-active-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Active{background-color:var(--ngx-editor-menu-item-active-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Active:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__MenuBar--Reverse .NgxEditor__Dropdown--DropdownMenu{top:unset;bottom:calc(var(--ngx-editor-menubar-height) + 2px)}.NgxEditor__MenuBar--Reverse .NgxEditor__Dropdown--Text:after{transform:rotate(180deg)}.NgxEditor__MenuBar--Reverse .NgxEditor__Popup{top:unset;bottom:calc(var(--ngx-editor-menubar-height) + 2px)}.NgxEditor__Popup{position:absolute;top:calc(var(--ngx-editor-menubar-height) + 2px);box-shadow:var(--ngx-editor-popup-shadow);border-radius:var(--ngx-editor-popup-border-radius);background-color:var(--ngx-editor-popup-bg-color);z-index:10;min-width:192px;padding:8px}.NgxEditor__Popup .NgxEditor__Popup--FormGroup{margin-bottom:8px}.NgxEditor__Popup .NgxEditor__Popup--FormGroup label{margin-bottom:3px}.NgxEditor__Popup .NgxEditor__Popup--FormGroup input[type=text],.NgxEditor__Popup .NgxEditor__Popup--FormGroup input[type=url]{padding:2px 4px}.NgxEditor__Popup .NgxEditor__Popup--Col{display:flex;flex-direction:column;position:relative}.NgxEditor__Popup .NgxEditor__Popup--Label{font-size:85%}.NgxEditor__Seperator{border-left:1px solid var(--ngx-editor-seperator-color);margin:0 5px}.NgxEditor__HelpText{font-size:80%}.NgxEditor__HelpText.NgxEditor__HelpText--Error{color:var(--ngx-editor-error-color)}\n"], encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: NgxEditorComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-editor', providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxEditorComponent),
multi: true,
},
], encapsulation: ViewEncapsulation.None, template: "<div class=\"NgxEditor\" #ngxEditor>\n <ng-content></ng-content>\n</div>\n", styles: [":root{--ngx-editor-border-radius: 4px;--ngx-editor-background-color: #fff;--ngx-editor-text-color: #000;--ngx-editor-placeholder-color: #6c757d;--ngx-editor-border-color: rgba(0, 0, 0, .2);--ngx-editor-wrapper-border-color: rgba(0, 0, 0, .2);--ngx-editor-menubar-bg-color: #fff;--ngx-editor-menubar-padding: 3px;--ngx-editor-menubar-height: 30px;--ngx-editor-blockquote-color: #ddd;--ngx-editor-blockquote-border-width: 3px;--ngx-editor-icon-size: 30px;--ngx-editor-popup-bg-color: #fff;--ngx-editor-popup-border-radius: 4px;--ngx-editor-popup-shadow: rgba(60, 64, 67, .15) 0px 2px 6px 2px;--ngx-editor-menu-item-border-radius: 2px;--ngx-editor-menu-item-active-color: #1a73e8;--ngx-editor-menu-item-hover-bg-color: #f1f1f1;--ngx-editor-menu-item-active-bg-color: #e8f0fe;--ngx-editor-seperator-color: #ccc;--ngx-editor-bubble-bg-color: #000;--ngx-editor-bubble-text-color: #fff;--ngx-editor-bubble-item-hover-color: #636262;--ngx-editor-bubble-seperator-color: #fff;--ngx-editor-focus-ring-color: #5e9ed6;--ngx-editor-error-color: red;--ngx-editor-click-pointer: default}.NgxEditor{background:var(--ngx-editor-background-color);color:var(--ngx-editor-text-color);background-clip:padding-box;border-radius:var(--ngx-editor-border-radius);border:1px solid var(--ngx-editor-border-color);position:relative}.NgxEditor--Disabled{opacity:.5!important;pointer-events:none!important}.NgxEditor__Placeholder:before{color:var(--ngx-editor-placeholder-color);opacity:1;-webkit-user-select:none;user-select:none;position:absolute;cursor:text;content:attr(data-placeholder)}.NgxEditor__Placeholder[data-align=right]:before{position:relative}.NgxEditor__Content{padding:8px;white-space:pre-wrap;outline:none;font-variant-ligatures:none;font-feature-settings:\"liga\" 0}.NgxEditor__Content p{margin:0 0 10px}.NgxEditor__Content blockquote{padding-left:16px;border-left:var(--ngx-editor-blockquote-border-width) solid var(--ngx-editor-blockquote-color);margin-left:0;margin-right:0}.NgxEditor__Content--Disabled{-webkit-user-select:none;user-select:none;pointer-events:none}.NgxEditor__Wrapper{border:1px solid var(--ngx-editor-wrapper-border-color);border-radius:var(--ngx-editor-border-radius)}.NgxEditor__Wrapper .NgxEditor__MenuBar{border-top-left-radius:var(--ngx-editor-border-radius);border-top-right-radius:var(--ngx-editor-border-radius);border-bottom:1px solid var(--ngx-editor-border-color)}.NgxEditor__Wrapper .NgxEditor{border-top-left-radius:0;border-top-right-radius:0;border:none}.NgxEditor__MenuBar{display:flex;flex-wrap:wrap;padding:var(--ngx-editor-menubar-padding);background-color:var(--ngx-editor-menubar-bg-color);gap:.25rem .1rem}.NgxEditor__MenuBar button:not(:disabled),.NgxEditor__MenuBar [role=button]:not(:disabled){cursor:var(--ngx-editor-click-pointer, default)}.NgxEditor__MenuItem{display:flex;align-items:center;justify-content:center;position:relative;flex-shrink:0}.NgxEditor__MenuItem.NgxEditor__MenuItem--IconContainer{display:flex;align-items:center;justify-content:center}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon{all:unset;appearance:none;height:var(--ngx-editor-icon-size);width:var(--ngx-editor-icon-size);transition:.2s ease-in-out;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--ngx-editor-menu-item-border-radius)}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon+.NgxEditor__MenuItem--Icon{margin-left:2px}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon:focus-visible{outline:1px solid var(--ngx-editor-focus-ring-color)}.NgxEditor__MenuItem .NgxEditor__MenuItem--Icon:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__MenuItem.NgxEditor__MenuItem--Text{padding:0 5px}.NgxEditor__MenuItem.NgxEditor__MenuItem--Active,.NgxEditor__MenuItem .NgxEditor__MenuItem--Active{background-color:var(--ngx-editor-menu-item-active-bg-color);color:var(--ngx-editor-menu-item-active-color)}.NgxEditor__Dropdown{min-width:64px;position:relative;display:flex;align-items:center;flex-shrink:0}.NgxEditor__Dropdown:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Text{all:unset;appearance:none;display:flex;align-items:center;justify-content:center;padding:0 5px;height:100%;width:100%}.NgxEditor__Dropdown .NgxEditor__Dropdown--Text:focus-visible{outline:1px solid var(--ngx-editor-focus-ring-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Text:after{display:inline-block;content:\"\";margin-left:24px;vertical-align:4px;border-top:4px solid;border-right:4px solid transparent;border-bottom:0;border-left:4px solid transparent}.NgxEditor__Dropdown .NgxEditor__Dropdown--DropdownMenu{position:absolute;left:0;box-shadow:var(--ngx-editor-popup-shadow);border-radius:var(--ngx-editor-popup-border-radius);background-color:var(--ngx-editor-popup-bg-color);z-index:10;width:100%;top:calc(var(--ngx-editor-menubar-height) + 2px);display:flex;flex-direction:column}.NgxEditor__Dropdown .NgxEditor__Dropdown--Item{all:unset;appearance:none;padding:8px;white-space:nowrap;color:inherit}.NgxEditor__Dropdown .NgxEditor__Dropdown--Item:focus-visible{outline:1px solid var(--ngx-editor-focus-ring-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Item:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Selected,.NgxEditor__Dropdown .NgxEditor__Dropdown--Open{color:var(--ngx-editor-menu-item-active-color);background-color:var(--ngx-editor-menu-item-active-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Active{background-color:var(--ngx-editor-menu-item-active-bg-color)}.NgxEditor__Dropdown .NgxEditor__Dropdown--Active:hover{background-color:var(--ngx-editor-menu-item-hover-bg-color)}.NgxEditor__MenuBar--Reverse .NgxEditor__Dropdown--DropdownMenu{top:unset;bottom:calc(var(--ngx-editor-menubar-height) + 2px)}.NgxEditor__MenuBar--Reverse .NgxEditor__Dropdown--Text:after{transform:rotate(180deg)}.NgxEditor__MenuBar--Reverse .NgxEditor__Popup{top:unset;bottom:calc(var(--ngx-editor-menubar-height) + 2px)}.NgxEditor__Popup{position:absolute;top:calc(var(--ngx-editor-menubar-height) + 2px);box-shadow:var(--ngx-editor-popup-shadow);border-radius:var(--ngx-editor-popup-border-radius);background-color:var(--ngx-editor-popup-bg-color);z-index:10;min-width:192px;padding:8px}.NgxEditor__Popup .NgxEditor__Popup--FormGroup{margin-bottom:8px}.NgxEditor__Popup .NgxEditor__Popup--FormGroup label{margin-bottom:3px}.NgxEditor__Popup .NgxEditor__Popup--FormGroup input[type=text],.NgxEditor__Popup .NgxEditor__Popup--FormGroup input[type=url]{padding:2px 4px}.NgxEditor__Popup .NgxEditor__Popup--Col{display:flex;flex-direction:column;position:relative}.NgxEditor__Popup .NgxEditor__Popup--Label{font-size:85%}.NgxEditor__Seperator{border-left:1px solid var(--ngx-editor-seperator-color);margin:0 5px}.NgxEditor__HelpText{font-size:80%}.NgxEditor__HelpText.NgxEditor__HelpText--Error{color:var(--ngx-editor-error-color)}\n"] }]
}], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i0.Injector }, { type: i0.ElementRef }], propDecorators: { ngxEditor: [{
type: ViewChild,
args: ['ngxEditor', { static: true }]
}], editor: [{
type: Input
}], outputFormat: [{
type: Input
}], placeholder: [{
type: Input
}], focusOut: [{
type: Output
}], focusIn: [{
type: Output
}] } });
class SanitizeHtmlPipe {
sanitizer;
constructor(sanitizer) {
this.sanitizer = sanitizer;
}
transform(value) {
if (isTrustedHtml(value)) {
return value;
}
return this.sanitizer.bypassSecurityTrustHtml(value);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SanitizeHtmlPipe, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.8", ngImport: i0, type: SanitizeHtmlPipe, isStandalone: true, name: "sanitizeHtml" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SanitizeHtmlPipe, decorators: [{
type: Pipe,
args: [{
name: 'sanitizeHtml',
}]
}], ctorParameters: () => [{ type: i1.DomSanitizer }] });
class Mark {
name;
constructor(name) {
this.name = name;
}
apply() {
return (state, dispatch) => {
const { schema } = state;
const type = schema.marks[this.name];
if (!type) {
return false;
}
return applyMark(type)(state, dispatch);
};
}
toggle() {
return (state, dispatch) => {
const { schema } = state;
const type = schema.marks[this.name];
if (!type) {
return false;
}
return toggleMark(type)(state, dispatch);
};
}
isActive(state) {
const { schema } = state;
const type = schema.marks[this.name];
if (!type) {
return false;
}
return isMarkActive(state, type);
}
canExecute(state) {
return this.toggle()(state);
}
}
class Blockqote {
toggle() {
return (state, dispatch) => {
const { schema } = state;
const type = schema.nodes['blockquote'];
if (!type) {
return false;
}
if (this.isActive(state)) {
return lift(state, dispatch);
}
return wrapIn(type)(state, dispatch);
};
}
isActive(state) {
const { schema } = state;
const type = schema.nodes['blockquote'];
if (!type) {
return false;
}
return isNodeActive(state, type);
}
canExecute(state) {
return this.toggle()(state);
}
}
class HorizontalRule {
insert() {
return (state, dispatch) => {
const { schema, tr } = state;
const type = schema.nodes['horizontal_rule'];
if (!type) {
return false;
}
dispatch(tr.replaceSelectionWith(type.create()).scrollIntoView());
return true;
};
}
canExecute(state) {
return canInsert(state, state.schema.nodes['horizontal_rule']);
}
}
class ListItem {
isBulletList = false;
constructor(isBulletList = false) {
this.isBulletList = isBulletList;
}
getType(schema) {
return this.isBulletList ? schema.nodes['bullet_list'] : schema.nodes['ordered_list'];
}
toggle() {
return (state, dispatch) => {
const { schema } = state;
const type = this.getType(schema);
if (!type) {
return false;
}
if (this.isActive(state)) {
return liftListItem(schema.nodes['list_item'])(state, dispatch);
}
return wrapInList(type)(state, dispatch);
};
}
isActive(state) {
const { schema } = state;
const type = this.getType(schema);
if (!type) {
return false;
}
return isNodeActive(state, type);
}
canExecute(state) {
return this.toggle()(state);
}
}
class Heading {
level;
constructor(level) {
this.level = level;
}
apply() {
return (state, dispatch) => {
const { schema } = state;
const type = schema.nodes['heading'];
if (!type) {
return false;
}
return setBlockType(type)(state, dispatch);
};
}
toggle() {
return (state, dispatch) => {
const { schema, selection, doc } = state;
const type = schema.nodes['heading'];
if (!type) {
return false;
}
const nodePos = selection.$from.before(1);
const node = doc.nodeAt(nodePos);
const attrs = node?.attrs ?? {};
if (this.isActive(state)) {
return setBlockType(schema.nodes['paragraph'], attrs)(state, dispatch);
}
return setBlockType(type, { ...attrs, level: this.level })(state, dispatch);
};
}
isActive(state) {
const { schema } = state;
const nodesInSelection = getSelectionNodes(state);
const type = schema.nodes['heading'];
if (!type) {
return false;
}
const supportedNodes = [
type,
schema.nodes['text'],
schema.nodes['blockquote'],
];
// heading is a text node
// don't mark as active when it has more nodes
const nodes = nodesInSelection.filter((node) => {
return supportedNodes.includes(node.type);
});
const acitveNode = nodes.find((node) => {
return node.attrs['level'] === this.level;
});
return Boolean(acitveNode);
}
canExecute(state) {
return this.toggle()(state);
}
}
class TextAlign {
align;
constructor(align) {
this.align = align;
}
toggle() {
return (state, dispatch) => {
const { doc, selection, tr, schema } = state;
const { from, to } = selection;
let applicable = false;
doc.nodesBetween(from, to, (node, pos) => {
const nodeType = node.type;
if ([schema.nodes['paragraph'], schema.nodes['heading']].includes(nodeType)) {
applicable = true;
const align = node.attrs['align'] === this.align ? null : this.align;
tr.setNodeMarkup(pos, nodeType, { ...node.attrs, align });
}
return true;
});
if (!applicable) {
return false;
}
if (tr.docChanged) {
dispatch?.(tr);
}
return true;
};
}
isActive(state) {
const nodes = getSelectionNodes(state);
const active = nodes.find((node) => {
return node.attrs['align'] === this.align;
});
return Boolean(active);
}
canExecute(state) {
return this.toggle()(state);
}
}
const defaultOptions = {
strict: true,
};
let Link$1 = class Link {
update(attrs) {
return (state, dispatch) => {
const { schema, selection } = state;
const type = schema.marks['link'];
if (!type) {
return false;
}
if (selection.empty) {
return false;
}
return toggleMark(type, attrs)(state, dispatch);
};
}
insert(text, attrs) {
return (state, dispatch) => {
const { schema, tr } = state;
const type = schema.marks['link'];
if (!type) {
return false;
}
const linkAttrs = {
href: attrs.href,
title: attrs.title ?? text,
target: attrs.target ?? '_blank',
};
const node = schema.text(text, [schema.marks['link'].create(linkAttrs)]);
tr.replaceSelectionWith(node, false)
.scrollIntoView();
if (tr.docChanged) {
dispatch?.(tr);
return true;
}
return false;
};
}
isActive(state, options = defaultOptions) {
if (options.strict) {
return true;
}
const { schema } = state;
const type = schema.marks['link'];
if (!type) {
return false;
}
return isMarkActive(state, type);
}
remove(state, dispatch) {
return removeLink()(state, dispatch);
}
canExecute(state) {
const testAttrs = {
href: '',
};
return this.insert('Exec', testAttrs)(state) || this.update(testAttrs)(state);
}
};
let Image$1 = class Image {
insert(src, attrs) {
return (state, dispatch) => {
const { schema, tr, selection } = state;
const type = schema.nodes['image'];
if (!type) {
return false;
}
const imageAttrs = {
width: null,
src,
...attrs,
};
if (!imageAttrs.width && selection instanceof NodeSelection && selection.node.type === type) {
imageAttrs.width = selection.node.attrs['width'];
}
tr.replaceSelectionWith(type.createAndFill(imageAttrs));
const resolvedPos = tr.doc.resolve(tr.selection.anchor - tr.selection.$anchor.nodeBefore.nodeSize);
tr
.setSelection(new NodeSelection(resolvedPos))
.scrollIntoView();
if (tr.docChanged) {
dispatch?.(tr);
return true;
}
return false;
};
}
isActive(state) {
const { selection } = state;
if (selection instanceof NodeSelection) {
return selection.node.type.name === 'image';
}
return false;
}
};
let TextColor$1 = class TextColor {
name;
attrName;
constructor(name, attrName = 'color') {
this.name = name;
this.attrName = attrName;
}
apply(attrs) {
return (state, dispatch) => {
const { schema, selection, doc } = state;
const type = schema.marks[this.name];
if (!type) {
return false;
}
const { from, to, empty } = selection;
if (!empty && (from + 1 === to)) {
const node = doc.nodeAt(from);
if (node?.isAtom && !node.isText && node.isLeaf) {
// An atomic node (e.g. Image) is selected.
return false;
}
}
return applyMark(type, attrs)(state, dispatch);
};
}
isActive(state) {
const { schema } = state;
const type = schema.marks[this.name];
if (!type) {
return false;
}
return isMarkActive(state, type);
}
getActiveColors(state) {
if (!this.isActive(state)) {
return [];
}
const { schema } = state;
const marks = getSelectionMarks(state);
const colors = marks
.filter((mark) => mark.type === schema.marks[this.name])
.map((mark) => {
return mark.attrs[this.attrName];
})
.filter(Boolean);
return colors;
}
remove() {
return (state, dispatch) => {
const { schema } = state;
const type = schema.marks[this.name];
if (!type) {
return false;
}
return removeMark(type)(state, dispatch);
};
}
canExecute(state) {
const attrs = this.name === 'text_color' ? { color: '' } : { backgroundColor: '' };
return this.apply(attrs)(state);
}
};
const SAFE_MARKS = ['link'];
class FormatClear {
insert() {
return (state, dispatch) => {
const { tr } = state;
const { ranges, empty } = tr.selection;
if (empty) {
return true;
}
Object.entries(state.schema.marks).forEach(([markType, mark]) => {
if (SAFE_MARKS.includes(markType)) {
return;
}
ranges.forEach((range) => {
tr.removeMark(range.$from.pos, range.$to.pos, mark);
});
});
dispatch(tr);
return true;
};
}
canExecute() {
return true;
}
}
const indentNodeTypes = ['paragraph', 'heading', 'blockquote'];
const minIndent = 0;
const maxIndent = 10;
const udpateIndentLevel = (tr, pos, method) => {
const node = tr.doc.nodeAt(pos);
if (!node) {
return false;
}
const nodeIndent = node.attrs['indent'] ?? 0;
const newIndent = clamp(nodeIndent + (method === 'increase' ? 1 : -1), minIndent, maxIndent);
if (newIndent === nodeIndent || newIndent < minIndent || newIndent > maxIndent) {
return false;
}
const attrs = {
...node.attrs,
indent: newIndent,
};
tr.setNodeMarkup(pos, node.type, attrs);
return true;
};
class Indent {
method = 'increase';
constructor(method) {
this.method = method;
}
insert() {
return (state, dispatch) => {
const { tr, doc