142 lines
4.5 KiB
JavaScript
Executable File
142 lines
4.5 KiB
JavaScript
Executable File
import { Extension } from "@tiptap/core";
|
|
import Suggestion from "@tiptap/suggestion";
|
|
import { ref, h } from "vue";
|
|
|
|
export const SlashCommand = Extension.create({
|
|
name: "slash-command",
|
|
|
|
addOptions() {
|
|
return {
|
|
suggestion: {
|
|
char: "/",
|
|
startOfLine: true,
|
|
command: ({ editor, range, props }) => {
|
|
// props.type => 'paragraph' | 'heading' | 'todo'
|
|
if (props.type === "paragraph") {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertContent({
|
|
type: "paragraph",
|
|
content: [{ type: "text", text: "" }],
|
|
})
|
|
.run();
|
|
}
|
|
|
|
if (props.type === "heading") {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setNode("heading", { level: 2 })
|
|
.run();
|
|
}
|
|
|
|
if (props.type === "todo") {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertContent({
|
|
type: "paragraph",
|
|
content: [{ type: "text", text: "☑️ Todo " }],
|
|
})
|
|
.run();
|
|
}
|
|
},
|
|
items: ({ query }) => {
|
|
const all = [
|
|
{ title: "Text", type: "paragraph" },
|
|
{ title: "Heading", type: "heading" },
|
|
{ title: "Todo", type: "todo" },
|
|
];
|
|
return all.filter((item) =>
|
|
item.title.toLowerCase().startsWith(query.toLowerCase())
|
|
);
|
|
},
|
|
render: () => {
|
|
let component;
|
|
let popup;
|
|
|
|
return {
|
|
onStart: (props) => {
|
|
component = ref({
|
|
items: props.items,
|
|
command: props.command,
|
|
selected: 0,
|
|
});
|
|
popup = document.createElement("div");
|
|
popup.style.position = "absolute";
|
|
popup.style.background = "white";
|
|
popup.style.border = "1px solid #ddd";
|
|
popup.style.borderRadius = "6px";
|
|
popup.style.padding = "4px 0";
|
|
popup.style.zIndex = 100;
|
|
document.body.appendChild(popup);
|
|
update();
|
|
function update() {
|
|
popup.innerHTML = "";
|
|
component.value.items.forEach((item, i) => {
|
|
const div = document.createElement("div");
|
|
div.textContent = item.title;
|
|
div.style.padding = "4px 12px";
|
|
div.style.cursor = "pointer";
|
|
div.style.background =
|
|
i === component.value.selected ? "#f0f0f0" : "white";
|
|
div.onclick = () =>
|
|
component.value.command({
|
|
editor: props.editor,
|
|
range: props.range,
|
|
props: item,
|
|
});
|
|
popup.appendChild(div);
|
|
});
|
|
}
|
|
component.value.update = update;
|
|
},
|
|
onUpdate: (props) => {
|
|
component.value.items = props.items;
|
|
component.value.update();
|
|
},
|
|
onKeyDown: (props) => {
|
|
const event = props.event;
|
|
if (event.key === "ArrowDown") {
|
|
component.value.selected =
|
|
(component.value.selected + 1) % component.value.items.length;
|
|
component.value.update();
|
|
return true;
|
|
}
|
|
if (event.key === "ArrowUp") {
|
|
component.value.selected =
|
|
(component.value.selected -
|
|
1 +
|
|
component.value.items.length) %
|
|
component.value.items.length;
|
|
component.value.update();
|
|
return true;
|
|
}
|
|
if (event.key === "Enter") {
|
|
component.value.command({
|
|
editor: props.editor,
|
|
range: props.range,
|
|
props: component.value.items[component.value.selected],
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
onExit: () => {
|
|
popup.remove();
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
},
|
|
|
|
addProseMirrorPlugins() {
|
|
return [Suggestion(this.options.suggestion)];
|
|
},
|
|
});
|