mirror of
https://github.com/outline/outline.git
synced 2026-01-05 18:49:53 -06:00
* Add document context to allow accessing editor in header, modals, and elsewhere * lint * framework * Hacking together fast * Insights * Spacing tweak, docs
175 lines
4.9 KiB
TypeScript
175 lines
4.9 KiB
TypeScript
import { MarkSpec } from "prosemirror-model";
|
|
import { Plugin, TextSelection } from "prosemirror-state";
|
|
import getMarkRange from "../queries/getMarkRange";
|
|
import markRule from "../rules/mark";
|
|
import Mark from "./Mark";
|
|
|
|
export default class Placeholder extends Mark {
|
|
get name() {
|
|
return "placeholder";
|
|
}
|
|
|
|
get schema(): MarkSpec {
|
|
return {
|
|
parseDOM: [{ tag: "span.template-placeholder" }],
|
|
toDOM: () => ["span", { class: "template-placeholder" }],
|
|
toPlainText: () => "",
|
|
};
|
|
}
|
|
|
|
get rulePlugins() {
|
|
return [markRule({ delim: "!!", mark: "placeholder" })];
|
|
}
|
|
|
|
toMarkdown() {
|
|
return {
|
|
open: "!!",
|
|
close: "!!",
|
|
mixable: true,
|
|
expelEnclosingWhitespace: true,
|
|
};
|
|
}
|
|
|
|
parseMarkdown() {
|
|
return { mark: "placeholder" };
|
|
}
|
|
|
|
get plugins() {
|
|
return [
|
|
new Plugin({
|
|
props: {
|
|
handleTextInput: (view, from, to, text) => {
|
|
if (this.editor.props.template) {
|
|
return false;
|
|
}
|
|
|
|
const { state, dispatch } = view;
|
|
const $from = state.doc.resolve(from);
|
|
|
|
const range = getMarkRange($from, state.schema.marks.placeholder);
|
|
if (!range) {
|
|
return false;
|
|
}
|
|
|
|
const selectionStart = Math.min(from, range.from);
|
|
const selectionEnd = Math.max(to, range.to);
|
|
|
|
dispatch(
|
|
state.tr
|
|
.removeMark(
|
|
range.from,
|
|
range.to,
|
|
state.schema.marks.placeholder
|
|
)
|
|
.insertText(text, selectionStart, selectionEnd)
|
|
);
|
|
|
|
const $to = view.state.doc.resolve(selectionStart + text.length);
|
|
dispatch(view.state.tr.setSelection(TextSelection.near($to)));
|
|
|
|
return true;
|
|
},
|
|
handleKeyDown: (view, event: KeyboardEvent) => {
|
|
if (!view.props.editable || !view.props.editable(view.state)) {
|
|
return false;
|
|
}
|
|
if (this.editor.props.template) {
|
|
return false;
|
|
}
|
|
if (
|
|
event.key !== "ArrowLeft" &&
|
|
event.key !== "ArrowRight" &&
|
|
event.key !== "Backspace"
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const { state, dispatch } = view;
|
|
|
|
if (event.key === "Backspace") {
|
|
const range = getMarkRange(
|
|
state.doc.resolve(Math.max(0, state.selection.from - 1)),
|
|
state.schema.marks.placeholder
|
|
);
|
|
if (!range) {
|
|
return false;
|
|
}
|
|
|
|
dispatch(
|
|
state.tr
|
|
.removeMark(
|
|
range.from,
|
|
range.to,
|
|
state.schema.marks.placeholder
|
|
)
|
|
.insertText("", range.from, range.to)
|
|
);
|
|
return true;
|
|
}
|
|
|
|
if (event.key === "ArrowLeft") {
|
|
const range = getMarkRange(
|
|
state.doc.resolve(Math.max(0, state.selection.from - 1)),
|
|
state.schema.marks.placeholder
|
|
);
|
|
if (!range) {
|
|
return false;
|
|
}
|
|
|
|
const startOfMark = state.doc.resolve(range.from);
|
|
dispatch(state.tr.setSelection(TextSelection.near(startOfMark)));
|
|
return true;
|
|
}
|
|
|
|
if (event.key === "ArrowRight") {
|
|
const range = getMarkRange(
|
|
state.selection.$from,
|
|
state.schema.marks.placeholder
|
|
);
|
|
if (!range) {
|
|
return false;
|
|
}
|
|
|
|
const endOfMark = state.doc.resolve(range.to);
|
|
dispatch(state.tr.setSelection(TextSelection.near(endOfMark)));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
handleClick: (view, pos, event: MouseEvent) => {
|
|
if (!view.props.editable || !view.props.editable(view.state)) {
|
|
return false;
|
|
}
|
|
if (this.editor.props.template) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
event.target instanceof HTMLSpanElement &&
|
|
event.target.className.includes("template-placeholder")
|
|
) {
|
|
const { state, dispatch } = view;
|
|
const range = getMarkRange(
|
|
state.selection.$from,
|
|
state.schema.marks.placeholder
|
|
);
|
|
if (!range) {
|
|
return false;
|
|
}
|
|
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
const startOfMark = state.doc.resolve(range.from);
|
|
dispatch(state.tr.setSelection(TextSelection.near(startOfMark)));
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
}
|
|
}
|