Placeholders: Caret vanishes in Firefox

I implemented a simple placeholder based on the available examples. Unfortunately, whenever the placeholder text is shown, the cursor caret vanishes in Firefox. Note that it works fine in Chrome.

Here is the placeholder code.

import { DecorationSet, Decoration } from 'prosemirror-view'
import { Plugin } from 'prosemirror-state'

export const placholderPlugin = new Plugin({
  props: {
    decorations(state) {
      const doc = state.doc

      if (!doc.firstChild?.isTextblock || doc.firstChild.content.size > 0) return

      const placeHolder = document.createElement('span')
      placeHolder.classList.add('placeholder')
      placeHolder.setAttribute('data-placeholder', 'Post Title')

      return DecorationSet.create(doc, [Decoration.widget(1, placeHolder)])
    },
  },
})

I hoped to be able to fix the issue with css pseudo classes, but the problem remains:

.ProseMirror span.placeholder::before {
  color: #cecece;
  content: attr(data-placeholder);
  cursor: text;
}

I noticed that Firefox gets confused with the content-editable=false attribute which gets automatically attached for PM decorations. So, when I change content-editable to true, the blinking caret is shown again. This is not a solution, just an observation for tracking down the root cause.

Here is a screencast for Firefox (not working): Peek 2021-02-22 11-28-firefox

And a screencast for Chrome (working as expected): Peek 2021-02-22 11-30-chrome

Does anyone know of a good workaround for Firefox, that also works with other browsers?

Hi, I recently had to implement placeholder and was following examples from this thread: How to: input-like placeholder behavior, have a look at the last post by aeaton. Works in Firefox without any carret issues for me.

@algus: Thanks for referencing the implementation. Yes, this should work as no new DOM elements are created and everything is done with pure CSS. Will test this solution and report back.

@algus: Thanks for that pure css placeholder solution which I can now confirm to work also with Firefox. I made some adjustments to the original solution from @aeaton which I am going to post here for reference.

import { DecorationSet, Decoration } from 'prosemirror-view'
import { Plugin } from 'prosemirror-state'
import { Node } from 'prosemirror-model'

export const placholderPlugin = new Plugin({
  props: {
    decorations(state) {
      const doc = state.doc
      const decorations: Decoration[] = []

      const decorate = (node: Node, pos: number) => {
        const nodeType = node.type
        if (nodeType.isBlock && nodeType.spec.placeholder && node.childCount === 0 && node.content.size === 0) {
          // node restriction
          const placeholderNodeBefore = nodeType.spec.placeholder.nodeBefore
          if (placeholderNodeBefore) {
            const $pos = doc.resolve(pos)
            if ($pos.nodeBefore?.type.name !== placeholderNodeBefore) return
          }
          // selection restriction
          const placeholderNodeFocused = nodeType.spec.placeholder.nodeFocused
          if (placeholderNodeFocused && state.selection.from !== pos + 1) return

          decorations.push(
            Decoration.node(pos, pos + node.nodeSize, {
              class: 'placeholder',
              'data-placeholder': node.type.spec.placeholder.text,
            })
          )
        }
      }

      doc.descendants(decorate)
      return DecorationSet.create(doc, decorations)
    },
  },
})

As you can see I attach a placeholder spec to the schema in order to enforce some rules and also to be able to define the placeholder text. The placeholder spec looks as follows:

export interface PlaceholderSpec {
  text: string
  nodeBefore?: string
  nodeFocused?: boolean
}

Where you can define some conditions under which the placeholder is shown. Finally the css:

.ProseMirror .placeholder::before {
  position: absolute;
  pointer-events: none;
  color: #cecece;
  cursor: text;
  content: attr(data-placeholder);
}

Thus, placeholder text gets injected from the data attribute. Note pointer-events: none; is quite cruical, otherwise selection inside the PM editor does not work where the placeholder text displays.

1 Like