Hi there
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:
When I use the placeholder
extension, I get a default behavior that includes a newline when the placeholder is shown:
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},
);
}
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:
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];
};
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.