How to send shared cursor updated information to plugin?

I wish to display the shared cursor information of other clients in the editor.

So far, I have editted: EditorConnection.js in the web example, and is able to get the information of client A’s cursor information to be sent to client B and client C…etc.

The information that client B and client C receives is here: {clientID: “1e8f4b00-0326-11eb-aa11-3d2f6cec7c6a” selection: {anchor: 226 head: 226 type: “text”}}

but I am not sure how to make this a display in the editor. I have created a CursorPlugin.js // @flow

import {EditorState, Plugin, PluginKey} from 'prosemirror-state';
import {TextSelection} from 'prosemirror-state';
import {EditorView} from 'prosemirror-view';
import {Decoration, DecorationSet} from 'prosemirror-view';

import {MARK_LINK} from './MarkNames';
import LinkTooltip from './ui/LinkTooltip';

import './ui/czi-pop-up.css';

const PLACE_HOLDER_ID = {name: 'CursorPlaceholderPlugin2'};

export function showCursor(
  json: any,
  coords: any
): boolean {
  const {runtime, state, readOnly, disabled} = view;
  const {schema, plugins} = state;
  if (readOnly || disabled || !runtime || !runtime.canUploadImage) {
    return false;
  }
}

export function showCursorPlaceholder(
  state: any,
  view: any,
  json: any
): boolean {
  // console.log('in');
  console.log(state.edit.plugins);
  console.log(state.edit.plugins[2]);
  state.edit.plugins[2].update_cursor(state.edit, json);
  // console.log(view);
  // console.log(json);


}

function defaultCursorBuilder() {
  const cursor = document.createElement('span')
  cursor.classList.add('ProseMirror-yjs-cursor')
  cursor.setAttribute('style', `border-color: #000000`)
  const userDiv = document.createElement('div')
  userDiv.setAttribute('style', `background-color: #000000`)
  userDiv.insertBefore(document.createTextNode('Ted'), null)
  cursor.insertBefore(userDiv, null)

  return cursor
}

class CursorPlaceholderPlugin2 extends Plugin {
  _object = null;
  _editor = null;

  constructor() {
    super({
      // [FS] IRAD-1005 2020-07-07
      // Upgrade outdated packages.
      key: new PluginKey('CursorPlaceholderPlugin2'),
      state: {
        init() {
          return DecorationSet.empty;
        },
        // apply(tr, prevState, oldState, newState) {
        //   console.log('# bookmark 1');
        //   console.log(tr);
        //   console.log(prevState);
        //   console.log(oldState);
        //   console.log(newState);
        //   prevState = prevState.map(tr.mapping, tr.doc);
        //   // const action = tr.getMeta(this);
        //   // if (!action) {
        //   //   return set;
        //   // }
        //   // console.log('');
        //   // const widget = document.createElement('czi-cursor-placeholder2');
        //   // widget.className = 'czi-cursor-placeholder2';
        //   // const deco = Decoration.widget(action.add.pos, widget, {
        //   //   id: PLACE_HOLDER_ID,
        //   // });
        //   // set = set.add(tr.doc, [deco]);
        //
        //   console.log(prevState);
        //   return prevState;
        // }
        apply(tr: Transform, set: DecorationSet): DecorationSet {
          console.log('123142414151');
          // Adjust decoration positions to changes made by the transaction
          set = set.map(tr.mapping, tr.doc);
          // See if the transaction adds or removes any placeholders
          const action = tr.getMeta(this);
          console.log(tr);
          if (action && action.add) {

            const cursor = defaultCursorBuilder();

            const deco = Decoration.widget(action.add.pos, cursor, {
              id: action.add.id,
            });

            set = set.add(tr.doc, [deco]);
          } else if (action && action.remove) {
            const finder = spec => spec.id == action.remove.id;
            set = set.remove(set.find(null, null, finder));
          }
          return set;
        }
      },
      view(editorView: EditorView) {
        console.log('# bookmark 2');
        console.log(editorView);
        this._object = new CursorView(editorView);

        this._editor = editorView;
        return this._object;
      },
    });
  }

  update_cursor(state, json) {
    console.log(state);
    this._object.update(this._editor, json);
  }
}

class CursorView {
  _editor = null;
  _cursor = null;
  _cursor_div = null;

  constructor(editorView: EditorView) {
    this._editor = editorView;
    this._cursor = document.createElement('span');
    this._cursor.className = 'czi-cursor-placeholder2';
    this._cursor.contenteditable = 'false';
    this._cursor_div = document.createElement('div');
    this._cursor_div.textContent = 'User'
    this._cursor.appendChild(this._cursor_div);
    editorView.dom.parentNode.appendChild(this._cursor);
    this.update(editorView, null);
  }

  update(view, editor): void {
    //state
    //view.state.doc
    //editor.doc
    // let json = editor;
    if (view != null) {
      console.log('#12314');
      console.log(view);

      this._cursor.style.display = '';
      let box = this._cursor.offsetParent.getBoundingClientRect();
      let start = view.coordsAtPos(view.state.selection.head), end = view.coordsAtPos(view.state.selection.anchor)
      let left = Math.max((start.left + end.left) / 2, start.left + 3)
      this._cursor.style.left = (left - box.left) + 'px'
      this._cursor.style.bottom = (box.bottom - start.top - 15) + 'px'

      let decorations = []
      if (view.state.selection.anchor !== null && view.state.selection.head !== null) {
        const maxsize = Math.max(view.state.doc.content.size - 1, 0)
        const anchor = Math.min(view.state.selection.anchor, maxsize)
        const head = Math.min(view.state.selection.head, maxsize)
        decorations.push(Decoration.widget(head, defaultCursorBuilder(), { key: 12345 + '', side: 10 }))
        const from = Math.min(anchor, head)
        const to = Math.max(anchor, head)
        decorations.push(Decoration.inline(from, to, { style: `background-color: #000000` }, { inclusiveEnd: true, inclusiveStart: false }))
      }

      return DecorationSet.create(view.state.doc, decorations)
    } else {
      this._cursor.style.display = 'none';

      return DecorationSet.empty;
    }
  }

  destroy() {
    this._cursor.style.display = 'none';

    return DecorationSet.empty;
  }
}

export default CursorPlaceholderPlugin2;

I tried to export showCursorPlaceholder(), and call it directly from EditorConnection.js, but the editorview is empty, I guess it is necessary to be called only through transaction and dispatch?

How can I make a transaction to change state without changing the contents in the editor?

Sorry that my understanding is not as thorough. If there is something unclear, please do let me know.

Hi @ted_chou12

if I understand the question correctly, you are asking how you can tell a plugin, that something should be displayed as a decoration (e.g. a cursor position from another user)? The method for that would be to dispatch a transaction that contains meta data.

The plugin reads that meta data and adds it to its internal state. Maybe have a look at the Track Changes example and see how setMeta and getMeta is used.

best regards

Frederik

@frederik Thank you so much! Yes, it is indeed what I wanted to do.

I see. I had a look at the current transaction:

steps is the usual doc changes.

const tr = receiveTransaction(
connection.state.edit,
json.steps.map(j => Step.fromJSON(connection.getEffectiveSchema(), j)),
json.clientIDs
);
newEditState = this.state.edit ? this.state.edit.apply(tr) : null;

But I wonder how I should send the selection information?

{clientID: “1e8f4b00-0326-11eb-aa11-3d2f6cec7c6a” selection: {anchor: 226 head: 226 type: “text”}}

Here’s an example: https://github.com/yjs/y-prosemirror/blob/master/src/plugins/cursor-plugin.js. You can either store the selection information in a prosemirror plugin state using setMeta and getMeta, or initialize the plugin with an external state store (like the awareness in the example) and simply set a boolean flag of when to update decorations from that external store.

1 Like