Resizable Embedded Inner Editor View

I want to create a new feature Vignette box plugin which can be resized from all corners. This box actually holds another inner editor view, so that every feature in the main editor view except this Vignette box is available inside the inner view i.e. to be specific I can use image, text etc inside this resizable rectangle area. Vignette box will hold attributes like width, height which shall be used to set the size of the inner editor view. Can be resized while editing too. Is this possible to achieve? If yes please enlighten me in the right direction. Thanks for considering!

It should be possible to have a node type for this with width/height attributes, and a node view that implements drag controls for the sides, which dispatch transactions that update those attributes. Your node view can implement an update method to resize the box without redrawing it when its attributes change.

Even before reaching the resize part, I am facing issues. Since my innerview doesn’t behave as anticipated, not able to type in, sharing here the relevant node spec, node view. Please guide me on what I am missing. Thanks!

Node View:

import {undo, redo} from 'prosemirror-history';
import {EditorView, NodeView} from 'prosemirror-view';
import {keymap} from 'prosemirror-keymap';
import {EditorState} from 'prosemirror-state';
import {Node} from 'prosemirror-model';
import React from 'react';
import {StepMap} from 'prosemirror-transform';

export class VignetteView implements NodeView {
  dom: HTMLElement;
  node: Node;
  outerView: EditorView;
  getPos: () => number;
  innerView: EditorView;
  updating: boolean;

  constructor(
    licit: typeof React.Component,
    node: Node,
    view: EditorView,
    getPos: () => number
  ) {
    // Store for later
    this.node = node;
    this.outerView = view;
    this.getPos = getPos;

    // reusing the plugins from the application main editor view, except a few
    const plugins = [...view.state.plugins.slice(1, 12)];

    plugins.forEach((plugin) => {
      plugin.spec.state = undefined;
    });

    this.innerView = new EditorView(null, {
      state: EditorState.create({
        doc: view.state.schema.nodeFromJSON({
          type: 'doc',
          content: [
            {
              type: 'paragraph',
              content: [{type: 'text', text: ' '}],
            },
          ],
        }),
        schema: view.state.schema,
        plugins: [
          ...plugins,
          keymap({
            'Mod-z': () => undo(this.outerView.state, this.outerView.dispatch),
            'Mod-y': () => redo(this.outerView.state, this.outerView.dispatch),
          }),
        ],
      }),
      // This is the magic part
      dispatchTransaction: this.dispatchInner.bind(this),
      handleDOMEvents: {
        mousedown: () => {
          // Kludge to prevent issues due to the fact that the whole
          // footnote is node-selected (and thus DOM-selected) when
          // the parent editor is focused.
          if (this.outerView.hasFocus()) this.innerView.focus();
        },
      },
    });

    // The editor's outer node is our DOM representation
    this.dom = this.innerView.dom;
    this.dom.style.borderStyle = 'dotted';
    this.dom.style.resize = 'both';
    // This flag is used to avoid an update loop between the outer and
    // inner editor
    this.updating = false;
  }

  dispatchInner(tr) {
    let {state, transactions} = this.innerView.state.applyTransaction(tr);
    this.innerView.updateState(state);

    if (!tr.getMeta('fromOutside')) {
      let outerTr = this.outerView.state.tr,
        offsetMap = StepMap.offset(this.getPos() + 1);
      for (let i = 0; i < transactions.length; i++) {
        let steps = transactions[i].steps;
        for (let j = 0; j < steps.length; j++)
          outerTr.step(steps[j].map(offsetMap));
      }
      if (outerTr.docChanged) this.outerView.dispatch(outerTr);
    }
  }

  update(node) {
    if (!node.sameMarkup(this.node)) return false;
    this.node = node;
    if (this.innerView) {
      let state = this.innerView.state;
      let start = node.content.findDiffStart(state.doc.content);
      if (start != null) {
        let {a: endA, b: endB} = node.content.findDiffEnd(state.doc.content);
        let overlap = start - Math.min(endA, endB);
        if (overlap > 0) {
          endA += overlap;
          endB += overlap;
        }
        this.innerView.dispatch(
          state.tr
            .replace(start, endB, node.slice(start, endA))
            .setMeta('fromOutside', true)
        );
      }
    }
    return true;
  }

  selectNode() {
    this.innerView.focus();
  }

  stopEvent(event: Event) {
    return this.innerView.dom.contains(event.target as HTMLElement);
  }
}

Node Spec:

import {NodeSpec} from 'prosemirror-model';

const VignetteNodeSpec: NodeSpec = {
  attrs: {
    id: {default: ''},
    class: {default: 'vignette'},
  },
  group: 'block',
  atom: true,
  selectable: true,
  draggable: true,
  content: 'block*',
  parseDOM: [
    {
      tag: 'div.vignette',
      getAttrs(dom: HTMLElement) {
        return {
          id: dom.getAttribute('id'),
          class: dom.getAttribute('class'),
        };
      },
    },
  ],
  toDOM(node) {
    return ['div', node.attrs, 0];
  },
};

export default VignetteNodeSpec;

I think the problem is that you need an additional wrapper element around that inner editor, or the outer one will, since that node is marked atom: true, set it to be uneditable.

Thanks, it worked and you saved my day. Now I have an inner editor view that is resizable using CSS resize property. When the node is selected, it activates the editor view and when node selection goes, it destroys the inner view. So far good, but the latest is that when the cursor is in the inner view and select a word inside that inner view (where have other text as well) and apply bold from the toolbar, the entire inner view gets the bold text, at the same time if Ctrl+B is pressed only the selected text inside the inner view is applied, which is what I anticipate in the toolbar action too. What am I doing wrong? I appreciate your quick help. Thanks once again!

Is it possible to place this node inline i.e. want to insert this node in between text like image, but retaining the same features available now is editable resizable inner view?

Is it possible to place Editor View in a span? I am trying to use resize functionality in czi-prosemirror/ImageNodeView.js at master · chanzuckerberg/czi-prosemirror · GitHub for resizing inner editor view and hence my question. So it’s like contenteditable false span which is the parent holding contenteditable true inner view span.

Thanks!

Yes, see the {mount} format of the second argument to new EditorView.

Thanks! But still I am not able to achieve the anticipated behaviour. This is the plugin (GitHub - MO-Movia/licit-plugin-contrib-vignette at initial), that I am working on. Earlier when I was using the div and resize css property, inplace editing of inner view was happening, not when I introduced ResizeControl component and span so that I could place the node in a paragraph between text and from this node when selecting invoke inplace inner view to make edits and the output is this then placed in the span. I have spending many weeks with not reaching anywhere. Please help me out. Thanks!