import { COLORS, fsm2 } from "@heydovetail/ui-components"; import { EditorState, Plugin } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; import { style } from "typestyle-react"; import { el } from "../util/dom"; import { PluginDescriptor } from "../util/PluginDescriptor"; export interface PluginState { readonly type: "default"; readonly text: string; readonly isBlank: (editorState: EditorState) => boolean; } export interface PlaceholderPluginOptions { // Determine if an editor state is blank. If true, the placeholder is rendered. isBlank: PluginState["isBlank"]; // The placeholder text to render. text: PluginState["text"]; } const transition = fsm2.createTransition()({ default: { withOptions: (prev, options: Partial) => ({ ...prev, ...options }) } }); const { key, getPluginState, setPluginState, getPluginStateOrThrow } = new PluginDescriptor("PlaceholderPlugin"); export { setPluginState, transition }; export class PlaceholderPlugin extends Plugin { constructor(options: PlaceholderPluginOptions) { // Use a pseudo-element to render the placeholder text. const className = style({ $nest: { ".ProseMirror &::before": { color: COLORS.i40, // Using attr() avoids needing to CSS-string encode the text value // (TypeStyle does not do this automatically). content: "attr(data-text)", cursor: "text", position: "absolute", zIndex: -1 } } }); super({ key: key, state: { init(): PluginState { return { type: "default", ...options }; }, apply(tr, cur: PluginState): PluginState { const nextPluginState = getPluginState(tr); return nextPluginState !== null ? nextPluginState : cur; } }, props: { decorations(state) { const { isBlank, text } = getPluginStateOrThrow(state); if (isBlank(state)) { const placeholder = el("span", className); placeholder.setAttribute("data-text", text); // Chrome renders the caret in a different position during mouse-down // (than after mouseup) for paragraphs that have placeholder.innerText = "\u200C"; return DecorationSet.create(state.doc, [ Decoration.widget(1 /* inside first empty paragraph */, placeholder, { // Performance optimisation key: text }) ]); } return null; } } }); } }