ngx-tiptap
Version:
Angular bindings for tiptap v2
497 lines (484 loc) • 21.3 kB
JavaScript
import * as i0 from '@angular/core';
import { forwardRef, Directive, Input, HostBinding, Component, NgModule, ApplicationRef, createComponent, ElementRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu';
import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
import { NodeView, getRenderedAttributes } from '@tiptap/core';
class EditorDirective {
constructor(elRef, renderer, changeDetectorRef) {
this.elRef = elRef;
this.renderer = renderer;
this.changeDetectorRef = changeDetectorRef;
this.outputFormat = 'html';
this.onChange = () => { };
this.onTouched = () => { };
this.handleChange = ({ editor, transaction }) => {
if (!transaction.docChanged) {
return;
}
// Needed for ChangeDetectionStrategy.OnPush to get notified about changes
this.changeDetectorRef.markForCheck();
if (this.outputFormat === 'html') {
this.onChange(editor.getHTML());
return;
}
this.onChange(editor.getJSON());
};
}
// Writes a new value to the element.
// This methods is called when programmatic changes from model to view are requested.
writeValue(value) {
if (!this.outputFormat && typeof value === 'string') {
this.outputFormat = 'html';
}
this.editor.chain().setContent(value, false).run();
}
// Registers a callback function that is called when the control's value changes in the UI.
registerOnChange(fn) {
this.onChange = fn;
}
// Registers a callback function that is called by the forms API on initialization to update the form model on blur.
registerOnTouched(fn) {
this.onTouched = fn;
}
// Called by the forms api to enable or disable the element
setDisabledState(isDisabled) {
this.editor.setEditable(!isDisabled);
this.renderer.setProperty(this.elRef.nativeElement, 'disabled', isDisabled);
}
ngOnInit() {
if (!this.editor) {
throw new Error('Required: Input `editor`');
}
// take the inner contents and clear the block
const { innerHTML } = this.elRef.nativeElement;
this.elRef.nativeElement.innerHTML = '';
// insert the editor in the dom
this.elRef.nativeElement.append(...Array.from(this.editor.options.element.childNodes));
// update the options for the editor
this.editor.setOptions({ element: this.elRef.nativeElement });
// update content to the editor
if (innerHTML) {
this.editor.chain().setContent(innerHTML, false).run();
}
// register blur handler to update `touched` property
this.editor.on('blur', () => {
this.onTouched();
});
// register update handler to listen to changes on update
this.editor.on('update', this.handleChange);
// Needed for ChangeDetectionStrategy.OnPush to get notified
this.editor.on('selectionUpdate', () => this.changeDetectorRef.markForCheck());
}
ngAfterViewInit() {
this.changeDetectorRef.detectChanges();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: EditorDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.4", type: EditorDirective, selector: "tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]", inputs: { editor: "editor", outputFormat: "outputFormat" }, providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EditorDirective),
multi: true,
}], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: EditorDirective, decorators: [{
type: Directive,
args: [{
selector: 'tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EditorDirective),
multi: true,
}],
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }], propDecorators: { editor: [{
type: Input
}], outputFormat: [{
type: Input
}] } });
class FloatingMenuDirective {
constructor(elRef) {
this.elRef = elRef;
this.pluginKey = 'NgxTiptapFloatingMenu';
this.tippyOptions = {};
this.shouldShow = null;
}
ngOnInit() {
if (!this.editor) {
throw new Error('Required: Input `editor`');
}
this.editor.registerPlugin(FloatingMenuPlugin({
pluginKey: this.pluginKey,
editor: this.editor,
element: this.elRef.nativeElement,
tippyOptions: this.tippyOptions,
shouldShow: this.shouldShow,
}));
}
ngOnDestroy() {
this.editor.unregisterPlugin(this.pluginKey);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: FloatingMenuDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.4", type: FloatingMenuDirective, selector: "tiptap-floating-menu[editor], [tiptapFloatingMenu][editor]", inputs: { pluginKey: "pluginKey", editor: "editor", tippyOptions: "tippyOptions", shouldShow: "shouldShow" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: FloatingMenuDirective, decorators: [{
type: Directive,
args: [{
selector: 'tiptap-floating-menu[editor], [tiptapFloatingMenu][editor]',
}]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { pluginKey: [{
type: Input
}], editor: [{
type: Input
}], tippyOptions: [{
type: Input
}], shouldShow: [{
type: Input
}] } });
class BubbleMenuDirective {
constructor(elRef) {
this.elRef = elRef;
this.pluginKey = 'NgxTiptapBubbleMenu';
this.tippyOptions = {};
this.shouldShow = null;
}
ngOnInit() {
if (!this.editor) {
throw new Error('Required: Input `editor`');
}
this.editor.registerPlugin(BubbleMenuPlugin({
pluginKey: this.pluginKey,
editor: this.editor,
element: this.elRef.nativeElement,
tippyOptions: this.tippyOptions,
shouldShow: this.shouldShow,
updateDelay: this.updateDelay,
}));
}
ngOnDestroy() {
this.editor.unregisterPlugin(this.pluginKey);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: BubbleMenuDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.4", type: BubbleMenuDirective, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: { pluginKey: "pluginKey", editor: "editor", tippyOptions: "tippyOptions", shouldShow: "shouldShow", updateDelay: "updateDelay" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: BubbleMenuDirective, decorators: [{
type: Directive,
args: [{
selector: 'tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]',
}]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { pluginKey: [{
type: Input
}], editor: [{
type: Input
}], tippyOptions: [{
type: Input
}], shouldShow: [{
type: Input
}], updateDelay: [{
type: Input
}] } });
class DraggableDirective {
constructor() {
this.draggable = true;
this.handle = '';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: DraggableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.4", type: DraggableDirective, selector: "[tiptapDraggable]", host: { properties: { "attr.draggable": "this.draggable", "attr.data-drag-handle": "this.handle" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: DraggableDirective, decorators: [{
type: Directive,
args: [{
selector: '[tiptapDraggable]',
}]
}], propDecorators: { draggable: [{
type: HostBinding,
args: ['attr.draggable']
}], handle: [{
type: HostBinding,
args: ['attr.data-drag-handle']
}] } });
class NodeViewContentDirective {
constructor() {
this.handle = '';
this.whiteSpace = 'pre-wrap';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NodeViewContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.4", type: NodeViewContentDirective, selector: "[tiptapNodeViewContent]", host: { properties: { "attr.data-node-view-content": "this.handle", "style.white-space": "this.whiteSpace" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NodeViewContentDirective, decorators: [{
type: Directive,
args: [{
selector: '[tiptapNodeViewContent]',
}]
}], propDecorators: { handle: [{
type: HostBinding,
args: ['attr.data-node-view-content']
}], whiteSpace: [{
type: HostBinding,
args: ['style.white-space']
}] } });
class AngularNodeViewComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: AngularNodeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: AngularNodeViewComponent, selector: "ng-component", inputs: { editor: "editor", node: "node", decorations: "decorations", innerDecorations: "innerDecorations", view: "view", selected: "selected", extension: "extension", HTMLAttributes: "HTMLAttributes", getPos: "getPos", updateAttributes: "updateAttributes", deleteNode: "deleteNode" }, ngImport: i0, template: '', isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: AngularNodeViewComponent, decorators: [{
type: Component,
args: [{ template: '' }]
}], propDecorators: { editor: [{
type: Input
}], node: [{
type: Input
}], decorations: [{
type: Input
}], innerDecorations: [{
type: Input
}], view: [{
type: Input
}], selected: [{
type: Input
}], extension: [{
type: Input
}], HTMLAttributes: [{
type: Input
}], getPos: [{
type: Input
}], updateAttributes: [{
type: Input
}], deleteNode: [{
type: Input
}] } });
class NgxTiptapModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NgxTiptapModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.4", ngImport: i0, type: NgxTiptapModule, declarations: [EditorDirective,
FloatingMenuDirective,
BubbleMenuDirective,
DraggableDirective,
NodeViewContentDirective], exports: [EditorDirective,
FloatingMenuDirective,
BubbleMenuDirective,
DraggableDirective,
NodeViewContentDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NgxTiptapModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NgxTiptapModule, decorators: [{
type: NgModule,
args: [{
declarations: [
EditorDirective,
FloatingMenuDirective,
BubbleMenuDirective,
DraggableDirective,
NodeViewContentDirective,
],
exports: [
EditorDirective,
FloatingMenuDirective,
BubbleMenuDirective,
DraggableDirective,
NodeViewContentDirective,
],
}]
}] });
class AngularRenderer {
constructor(ViewComponent, injector, props) {
this.applicationRef = injector.get(ApplicationRef);
this.componentRef = createComponent(ViewComponent, {
environmentInjector: this.applicationRef.injector,
elementInjector: injector,
});
// set input props to the component
this.updateProps(props);
this.applicationRef.attachView(this.componentRef.hostView);
}
get instance() {
return this.componentRef.instance;
}
get elementRef() {
return this.componentRef.injector.get(ElementRef);
}
get dom() {
return this.elementRef.nativeElement;
}
updateProps(props) {
Object.entries(props).forEach(([key, value]) => {
this.componentRef.setInput(key, value);
});
}
updateAttributes(attributes) {
Object.keys(attributes).forEach((key) => {
this.dom.setAttribute(key, attributes[key]);
});
}
detectChanges() {
this.componentRef.changeDetectorRef.detectChanges();
}
destroy() {
this.componentRef.destroy();
this.applicationRef.detachView(this.componentRef.hostView);
}
}
class AngularNodeView extends NodeView {
mount() {
const injector = this.options.injector;
const props = {
editor: this.editor,
node: this.node,
decorations: this.decorations,
innerDecorations: this.innerDecorations,
view: this.view,
selected: false,
extension: this.extension,
HTMLAttributes: this.HTMLAttributes,
getPos: () => this.getPos(),
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
deleteNode: () => this.deleteNode(),
};
this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this);
this.editor.on('selectionUpdate', this.handleSelectionUpdate);
// create renderer
this.renderer = new AngularRenderer(this.component, injector, props);
// Register drag handler
if (this.extension.config.draggable) {
this.renderer.elementRef.nativeElement.ondragstart = (e) => {
this.onDragStart(e);
};
}
this.contentDOMElement = this.node.isLeaf ? null : document.createElement(this.node.isInline ? 'span' : 'div');
if (this.contentDOMElement) {
// For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
// With this fix it seems to work fine
// See: https://github.com/ueberdosis/tiptap/issues/1197
this.contentDOMElement.style.whiteSpace = 'inherit';
// Required for editable node views
// The content won't be rendered if `editable` is set to `false`
this.renderer.detectChanges();
}
this.appendContendDom();
this.updateElementAttributes();
}
get dom() {
return this.renderer.dom;
}
get contentDOM() {
if (this.node.isLeaf) {
return null;
}
return this.contentDOMElement;
}
appendContendDom() {
const contentElement = this.dom.querySelector('[data-node-view-content]');
if (this.contentDOMElement
&& contentElement
&& !contentElement.contains(this.contentDOMElement)) {
contentElement.appendChild(this.contentDOMElement);
}
}
handleSelectionUpdate() {
const { from, to } = this.editor.state.selection;
const pos = this.getPos();
if (typeof pos !== 'number') {
return;
}
if (from <= pos && to >= pos + this.node.nodeSize) {
if (this.renderer.instance.selected) {
return;
}
this.selectNode();
}
else {
if (!this.renderer.instance.selected) {
return;
}
this.deselectNode();
}
}
update(node, decorations, innerDecorations) {
const updateProps = (props) => {
this.renderer.updateProps(props);
if (typeof this.options.attrs === 'function') {
this.updateElementAttributes();
}
};
if (this.options.update) {
const oldNode = this.node;
const oldDecorations = this.decorations;
const oldInnerDecorations = this.innerDecorations;
this.node = node;
this.decorations = decorations;
this.innerDecorations = innerDecorations;
return this.options.update({
oldNode,
oldDecorations,
oldInnerDecorations,
newNode: node,
newDecorations: decorations,
innerDecorations: this.innerDecorations,
updateProps: () => updateProps({
node,
decorations: decorations,
innerDecorations,
}),
});
}
if (node.type !== this.node.type) {
return false;
}
if (node === this.node
&& this.decorations === decorations
&& this.innerDecorations === innerDecorations) {
return true;
}
this.node = node;
this.decorations = decorations;
this.innerDecorations = innerDecorations;
updateProps({
node,
decorations: decorations,
innerDecorations,
});
return true;
}
selectNode() {
this.renderer.updateProps({ selected: true });
this.renderer.dom.classList.add('ProseMirror-selectednode');
}
deselectNode() {
this.renderer.updateProps({ selected: false });
this.renderer.dom.classList.remove('ProseMirror-selectednode');
}
destroy() {
this.renderer.destroy();
this.editor.off('selectionUpdate', this.handleSelectionUpdate);
this.contentDOMElement = null;
}
/**
* Update the attributes of the top-level element that holds the React component.
* Applying the attributes defined in the `attrs` option.
*/
updateElementAttributes() {
if (this.options.attrs) {
let attrsObj = {};
if (typeof this.options.attrs === 'function') {
const extensionAttributes = this.editor.extensionManager.attributes;
const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes);
attrsObj = this.options.attrs({ node: this.node, HTMLAttributes });
}
else {
attrsObj = this.options.attrs;
}
this.renderer.updateAttributes(attrsObj);
}
}
}
const AngularNodeViewRenderer = (ViewComponent, options) => {
return (props) => {
return new AngularNodeView(ViewComponent, props, options);
};
};
/*
* Public API Surface of ngx-tiptap
*/
/**
* Generated bundle index. Do not edit.
*/
export { AngularNodeViewComponent, AngularNodeViewRenderer, AngularRenderer, BubbleMenuDirective, DraggableDirective, EditorDirective, FloatingMenuDirective, NgxTiptapModule, NodeViewContentDirective };
//# sourceMappingURL=ngx-tiptap.mjs.map