Skip to content

Instantly share code, notes, and snippets.

@neharkarvishal
Created September 4, 2022 15:20
Show Gist options
  • Select an option

  • Save neharkarvishal/f2310e21ebb6726b000c0d523d47a375 to your computer and use it in GitHub Desktop.

Select an option

Save neharkarvishal/f2310e21ebb6726b000c0d523d47a375 to your computer and use it in GitHub Desktop.

Revisions

  1. neharkarvishal created this gist Sep 4, 2022.
    27 changes: 27 additions & 0 deletions ContextMenu.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,27 @@
    import { Extension } from '@tiptap/core';
    import { ComponentType } from 'react';
    import { ContextMenuPlugin, ContextMenuProps } from './ContextMenuPlugin';

    interface ContextMenuOptions {
    component: ComponentType<ContextMenuProps>;
    }

    export const ContextMenu = Extension.create<ContextMenuOptions>({
    name: 'contextMenu',

    addOptions() {
    return {
    element: null,
    component: () => null,
    };
    },

    addProseMirrorPlugins() {
    return [
    ContextMenuPlugin({
    editor: this.editor,
    component: this.options.component,
    }),
    ];
    },
    });
    89 changes: 89 additions & 0 deletions ContextMenuPlugin.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,89 @@
    import Tippy from '@tippyjs/react';
    import { Editor } from '@tiptap/core';
    import { ReactRenderer } from '@tiptap/react';
    import { Plugin, PluginKey } from 'prosemirror-state';
    import { EditorView } from 'prosemirror-view';
    import React, { ComponentType } from 'react';

    export interface ContextMenuProps {
    editor: Editor;
    clientRect: () => DOMRect;
    onBlur: () => void;
    onExit: () => void;
    }

    export interface ContextMenuPluginProps {
    editor: Editor;
    component: ComponentType<ContextMenuProps>;
    }

    export type ContextMenuViewProps = ContextMenuPluginProps & {
    view: EditorView;
    };

    export class ContextMenuView {
    editor: Editor;
    view: EditorView;
    component: ComponentType<ContextMenuProps>;

    constructor({ editor, view, component }: ContextMenuViewProps) {
    this.editor = editor;
    this.view = view;
    this.component = component;

    this.onContextMenu = this.onContextMenu.bind(this);
    this.view.dom.addEventListener('contextmenu', this.onContextMenu);
    }

    onContextMenu(event: Event) {
    event.preventDefault();
    const ev = event as MouseEvent;

    const clientRect = () => ({
    bottom: ev.clientY,
    left: ev.clientX,
    right: ev.clientX,
    top: ev.clientY,
    width: 0,
    height: 0,
    x: 0,
    y: 0,
    toJSON: () => ({}),
    });

    const component = (props: ContextMenuProps) => (
    <Tippy
    appendTo={() => document.body}
    showOnCreate={true}
    interactive={true}
    trigger="manual"
    placement="bottom-start"
    getReferenceClientRect={props.clientRect}
    onClickOutside={() => props.onBlur()}
    render={() => <this.component {...props} />}
    zIndex={1500}
    />
    );

    const renderer = new ReactRenderer(component, {
    editor: this.editor,
    props: {
    editor: this.editor,
    clientRect,
    onBlur: () => renderer.destroy(),
    onExit: () => renderer.destroy(),
    },
    });
    }

    destroy() {
    this.view.dom.removeEventListener('contextmenu', this.onContextMenu);
    }
    }

    export const ContextMenuPlugin = (options: ContextMenuPluginProps) => {
    return new Plugin({
    key: new PluginKey('contextMenu'),
    view: (view) => new ContextMenuView({ view, ...options }),
    });
    };