Display placeholder in the same style of textarea

Hi there :wave:

I’ve been trying to add a placeholder to the editor, but it doesn’t behave like I want it to. My goal is to have the placeholder behave like a normal Textarea:

Default Textarea Behavior

When I use the placeholder extension, I get a default behavior that includes a newline when the placeholder is shown:

Default Placeholder behavior

My first thought was to try to copy the code out of the placeholder extension from GitHub, but that was more of a step backwards since now the cursor ends up after the placeholder text:

Code
class Placeholder extends WidgetType {
  constructor(readonly content: string | HTMLElement) {
    super();
  }

  toDOM() {
    const wrap = document.createElement('span');
    wrap.className = styles.Placeholder;
    wrap.style.pointerEvents = 'none';
    wrap.appendChild(
      typeof this.content === 'string'
        ? document.createTextNode(this.content)
        : this.content,
    );
    if (typeof this.content === 'string') {
      wrap.setAttribute('aria-label', `placeholder ${this.content}`);
    } else {
      wrap.setAttribute('aria-hidden', 'true');
    }
    return wrap;
  }

  ignoreEvent() {
    return false;
  }
}

// / Extension that enables a placeholder—a piece of example content
// / to show when the editor is empty.
export function placeholder(content: string | HTMLElement): Extension {
  return ViewPlugin.fromClass(
    class {
      placeholder: DecorationSet;

      constructor(readonly view: EditorView) {
        this.placeholder = Decoration.set([
          Decoration.widget({
            widget: new Placeholder(content),
            side: -1,
          }).range(0),
        ]);
      }

      // Kludge to convince TypeScript that this is a plugin value
      // eslint-disable-next-line @typescript-eslint/member-ordering
      update!: () => void;

      get decorations() {
        return this.view.state.doc.length ? Decoration.none : this.placeholder;
      }
    },
    {decorations: (view) => view.decorations},
  );
}

Forked Placeholder Behavior

I then tried to style the placeholder to be absolutely positioned–my thinking is that it would show up on the first line and the extra margin/padding would collapse. Doing this basically got me back to step one of the default placeholder:

Behavior with absolute position

After that I tried setting this as a block-level decoration (which required me to set a value in state), since maybe that would give me more options…it didn’t, and definitely was worse:

Code

class Placeholder extends WidgetType {
  constructor(readonly content: string | HTMLElement) {
    super();
  }

  toDOM() {
    const wrap = document.createElement('span');
    wrap.className = styles.Placeholder;
    wrap.style.pointerEvents = 'none';
    wrap.appendChild(
      typeof this.content === 'string'
        ? document.createTextNode(this.content)
        : this.content,
    );
    if (typeof this.content === 'string') {
      wrap.setAttribute('aria-label', `placeholder ${this.content}`);
    } else {
      wrap.setAttribute('aria-hidden', 'true');
    }
    return wrap;
  }

  ignoreEvent() {
    return false;
  }
}


export const placeholderWithState = (): Extension => {
  const placeholderDecoration = () =>
    Decoration.widget({
      widget: new Placeholder('Start typing...'),
      side: -1,
      block: true,
    });

  const decorate = (state: EditorState) => {
    const widgets: Range<Decoration>[] = [];

    if (state.doc.length === 0) {
      widgets.push(placeholderDecoration().range(0));
    }

    return widgets.length > 0 ? RangeSet.of(widgets) : Decoration.none;
  };

  const placeholderField = StateField.define<DecorationSet>({
    create(state) {
      return decorate(state);
    },
    update(decos, transaction) {
      if (transaction.docChanged) {
        return decorate(transaction.state);
      }

      return decos.map(transaction.changes);
    },
    provide(field) {
      return EditorView.decorations.from(field);
    },
  });

  return [placeholderField];
};

Block level behavior

When I look at the markup rendered in the DOM, I observe that a <br /> tag is added when the document is empty and I can’t seem to figure out a way to remove that tag. Am I missing something? I think that <br /> tag is the cause of all my troubles.

I’m not seeing this behavior — if I have an empty editor with a placeholder, there’s no extra room below it. A <br> tag at the end of a block element will do nothing. Could it be that you have a trailing newline in your placeholder text, or some CSS that is interfering with the editor content?

Thank you for your response–apparently there was a global selector on img that was breaking this, scoping that selector properly resolved this.