How to add decoration without focusing the editor on dispatch?

I would like to add decorations to my editor. I am hitting an issue when doing so shortly after user has switched away from the editor brings the focus back to the editor. I see that view.dispatch({effects}); triggers focus/focusIn event breaking signal cascade in my application:

Screenshot from 2023-04-15 20-48-30

where index.js in trace above is CodeMirror’s code (most recent at the top). Based on the trace, it appears related to updating selections.

It is reproducible using Chrome in playground using minimally modified Decoration example from documentation (link to playground):

import { Decoration, EditorView, keymap } from "@codemirror/view"
import { StateField, StateEffect } from "@codemirror/state"

const addUnderline = StateEffect.define({
  map: ({from, to}, change) => ({from: change.mapPos(from), to: change.mapPos(to)})
})

const underlineField = StateField.define({
  create() {
    return Decoration.none
  },
  update(underlines, tr) {
    underlines = underlines.map(tr.changes)
    for (let e of tr.effects) if (e.is(addUnderline)) {
      underlines = underlines.update({
        add: [underlineMark.range(e.value.from, e.value.to)]
      })
    }
    return underlines
  },
  provide: f => EditorView.decorations.from(f)
})

const underlineMark = Decoration.mark({class: "cm-underline"})
const underlineTheme = EditorView.baseTheme({
  ".cm-underline": { textDecoration: "underline 3px red" }
})

export function underlineSelection(view) {
  let effects = view.state.selection.ranges
    .filter(r => !r.empty)
    .map(({from, to}) => addUnderline.of({from, to}))
  if (!effects.length) return false

  if (!view.state.field(underlineField, false))
    effects.push(StateEffect.appendConfig.of([underlineField,
                                              underlineTheme]))
  console.log('will blur', document.activeElement)
  document.activeElement.blur();
  console.log('active element before dispatch:', document.activeElement)
  view.dispatch({effects});
  console.log('active element after dispatch:', document.activeElement)
  return true
}

export const underlineKeymap = keymap.of([{
  key: "Mod-h",
  preventDefault: true,
  run: underlineSelection
}])

new EditorView({
  doc: "Select text and press Ctrl-h (Cmd-h) to add an underline\nto it.\n",
  extensions: [underlineKeymap],
  parent: document.body
})

After underlining a selection it logs:

will blur HTMLDivElement {cmView: DocView {parent: null, …}}
active element before dispatch: HTMLBodyElement {}
active element after dispatch: HTMLDivElement {cmView: DocView {parent: null, …}}

But I expected it to be:

will blur HTMLDivElement {cmView: DocView {parent: null, …}}
active element before dispatch: HTMLBodyElement {}
active element after dispatch: HTMLBodyElement {}

Am I missing something, or is this a buglet?

Upon further testing, version 6.0.0 did not have this issue, but 6.0.1 does. 6.0.1 release notes describe only two changes, including:

Avoid DOM selection corruption when the editor doesn’t have focus but has selection and updates its content.

So this appears relevant. There are three commits modifying selections in between releases:

That fix was developed in response to Selections and Life Cycle.

My current understanding is that the current behaviour is a side-effect of another fix, and not necessarily by design. I still don’t know how to fix/workaround this. My problem is that I have a focus listener that is beyond my control and cannot just blur the editor again, as once the focus event was emitted it is already too late.

What was going on was that the DOM changes needed to apply that update messed up the DOM selection, which was still in the editor despite it no longer having focus. And restoring a proper DOM selection apparently randomly moves focus back into the editor. This patch tries to detect and undo this phenomenon.

1 Like