Custom scroll function

Is there a way to create a custom step to handle scrolling to a particular $pos.

We have a scenario where we want to scroll to a $pos in a document without updating or setting the selection. Currently, we have a added a function in our updateView method. But this is quite laggy on larger documents.

Ultimately we would like to trigger a scroll event as part of our Decorator plugins apply method.

editorState.scrollIntoView($activePos);
new Plugin({
  key: new PluginKey('sample'),
  state: {
    init(config, editorState) {
      return sampleStrategy(editorState.doc);
    },
    apply(transaction, decorationSet, editorState) {
      const { mapping, doc } = transaction;
      const $activePos = getActiveDecorationPos(editorState);

      /*
        We want to trigger a scroll to the $activePos
        when  the decoration is rendered
      */

      return decorationSet.map(mapping, doc);
    },
  },
  props: {
    decorations(state) {
      return this.getState(state);
    },
  },
});

Any suggestions would be great.

What’s the logic in your updateView method?

I have a similar challenge to you, and one idea I’ve been chewing on is to write a custom element that scrolls the page to bring itself into view. The idea is to do this in the connected callback, so that when it is attached to the DOM it can schedule scrolling via requestAnimationFrame. You would hook this into ProseMirror by just rendering it as a decoration.

There’s probably a few wrinkles I haven’t thought about, but one that does come to mind is:

  • After the scroll has completed, how do you stop it from repeating when the decoration is rendered again?

Hey @bradleyayers

We have a solution in place but we are not overly happy with it…


const VIEW_DELAY = 50;

class editor {
  updateView = () => {

    ...

    if (!this.editorState.edit) return this.setView(null);

    if (this.editorView) {
      this.editorView.updateState(this.editorState.edit);
    } else {
      this.setView(
        new EditorView(document.querySelector('#editor'), {
          nodeViews,
          state: this.editorState.edit,
          dispatchTransaction: transaction =>
            this.dispatch({ type: 'transaction', transaction }),
        }),
      );

      ...

    }

    // Delay for Editor render --
    window.clearTimeout(this.scrollerTimeout);
    this.scrollerTimeout = window.setTimeout(this.handleScrolling, VIEW_DELAY);
  };

  handleScrolling = () => {
    if (scrollTargetId === null) return;

    const scrollTarget = getElemById(scrollTargetId);

    scrollToElement(scrollTarget);
    setScrollTargetId(null);
  };
}

Currently, we are adding an arbitrary delay to wait for the view to render (This is required for larger documents).

We are using redux to set a target id. Then we scroll to that target on the next view update and set the target back to null.

I can’t see any documentation regarding a callback on the setView method. Apologies if I have missed this.

Do you have an alternate solution?

1 Like

Where does the target ID come from, and how does that end up in the editor DOM?

Could you use decoration to indicate which portion of the document (e.g. node) should be scrolled into view, and a NodeView to perform the scrolling?

We are setting a decoration and then using the rendered decoration as the target.

It would be nice to have an onUpdated callback on the EditorView.

updateState is synchronous, so as soon as it returns the DOM update is finished.

While the view might at some point get additional scroll-related methods, for now I just recommend building them yourself on top of coordsAtPos. There will probably not be further integration between the state and the scroll position—scrolling would be an imperative effect on the view object, not something you model with a step or a new transaction property. (Though of course nothing is stopping you from doing it like that in your own code—for example with a meta property on the transaction.)

1 Like