Handling focus in plugins

Hi all,

I have a couple of controls (such as drag handles for resizing images and tables) that should only be there if the editor view currently has focus or not. Another example is a “fake selection” decoration that should make the current text selection visible if focus is somewhere outside the editor, but let the browser handle the selection highlighting if the editor does have focus.

I am not sure how to handle these kinds of things with the ProseMirror architecture. Focus is not part of the editor state, it seems like it is only manifested in the CSS class on the editor and the hasFocus() API. There does not seem to be a good way for plugins to actually respond to focus changes. I don’t really want to do this with CSS, because I am afraid that e.g. the mere presence of the fake selection decoration (even if made invisible through CSS) could mess up or at least complicate editing operations. In other cases, the controls are actually outside of the editor view DOM element.

Any suggestions?

Best regards, Chris

1 Like

If the controls are going to mess up editing, that’ll probably occur when the editor has focus, and they need to be visible, no?

Anyway, you could write a plugin that tracks focus state if you want it in your editor state (use handleDOMEvents to listen for focus/blur events and dispatch transactions), or if the controls are overlaid and don’t use decorations, you could use a plugin view that imperatively hides/shows them based on DOM events.

Thanks for the suggestions. I am trying to follow the second option

Basically what I am doing is very close to the official tooltip example. Notice how when you click outside the editor in that example to remove focus, the tooltip sticks around? That’s what I want to avoid.

So here’s a remix where I started the implementation, but I must be missing something really stupid, as I can’t find a way to access the plugins own plugin view to be able to actually hide it:

I guess I can still put the focus state into the plugins state and work with that from the plugin view, but it sounded like there’s a simpler way?

1 Like

For the record, I did get this working by storing the focus state in the plugin state. The code now looks like this:

const key = new PluginKey('selectionSize');

let selectionSizePlugin = new Plugin({
  key,
  state: {
    init() {
      return false; // assume the view starts out without focus
    },
    apply(transaction, prevFocused) {
      // update plugin state when transaction contains the meta property
      // set by the focus/blur DOM event handlers
      let focused = transaction.getMeta(key);
      return typeof focused === 'boolean' ? focused : prevFocused;
    }
  },
  view(editorView) { return new SelectionSizeTooltip(editorView) },
  props: {
    handleDOMEvents: {
      blur(view) { view.dispatch(view.state.tr.setMeta(key, false)) },
      focus(view) { view.dispatch(view.state.tr.setMeta(key, true)) }
    }
  }
})

class SelectionSizeTooltip {

  // constructor see https://prosemirror.net/examples/tooltip/

  update(view, lastState) {
    let state = view.state
    let focused = key.getState(state);

    // Don't do anything if the document, selection, and focus didn't change
    if (lastState && lastState.doc.eq(state.doc) &&
        lastState.selection.eq(state.selection) &&
        focused === key.getState(lastState)) return

    // Hide the tooltip if the selection is empty
    if (state.selection.empty || !focused) {
      this.tooltip.style.display = "none"
      return
    }

   // rest of the code see https://prosemirror.net/examples/tooltip/
  }
}

Now I am moving the focus state plugin into its own plugin so that it can easily be reused by other similar plugins. But the general principle works great as far as I can tell. Thanks for the help.

3 Likes

Hi cmlenz, I’m facing the same issue. Did you ever break out that plugin just for focus?

PM’s surprising behavior here is that plugins don’t get an update call when the selection changes, if that change is due to an editor blur.

Also, I think you might need to return true from the handleDOMEvents methods.

Hi @reverie,

here is the code for the very simple plugin we are using. Basically this just triggers an update on focus/blur via dispatchTransaction.

import {Plugin, PluginKey} from 'prosemirror-state';

const key = new PluginKey('focus');

export const focusPlugin = new Plugin({
  key,
  state: {
    init() {
      return false;
    },
    apply(transaction, prevFocused) {
      let focused = transaction.getMeta(key);
      if (typeof focused === 'boolean') {
        return focused;
      }
      return prevFocused;
    }
  },
  props: {
    handleDOMEvents: {
      blur: view => {
        view.dispatch(view.state.tr.setMeta(key, false));
        return false;
      },
      focus: view => {
        view.dispatch(view.state.tr.setMeta(key, true));
        return false;
      }
    }
  }
});

Retrieving the focus state in other code can be done using EditorView.hasFocus, so you don’t really need to get the focus state out of the plugin state.

Why?

1 Like

Thank you so much for sharing that. I was wrong about the event handlers.