UNPKG

ngx-editor

Version:

The Rich Text Editor for Angular, Built on ProseMirror

946 lines (922 loc) 153 kB
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