Skip to content

Commit dafdc1d

Browse files
committed
feat: slash command groups
1 parent 6f38b88 commit dafdc1d

File tree

7 files changed

+205
-138
lines changed

7 files changed

+205
-138
lines changed

apps/docs/src/docs/guides/slash-commands.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ export default function Page() {
2626
slashMenuCommands={[
2727
...defaultSlashCommands,
2828
{
29-
title: "Heading 4",
30-
command: (editor) =>
31-
editor.chain().focus().setNode("heading", { level: 3 }).run(),
32-
description: "Really small heading",
33-
icon: AlertCircle,
34-
},
29+
title: "Custom group",
30+
commands: [
31+
{
32+
title: "Image",
33+
command: (editor) => /** Add an image */,
34+
description: "Add image",
35+
icon: AlertCircle,
36+
}
37+
]
38+
}
3539
]}
3640
/>
3741
);

apps/web/app/page.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,7 @@ export default function Page() {
5959
shikiji: data,
6060
}),
6161
]}
62-
slashMenuCommands={[
63-
...defaultSlashCommands,
64-
{
65-
title: "Test",
66-
command: () => console.log("test"),
67-
description: "Do this do get amazing funcion",
68-
icon: AlertCircle,
69-
},
70-
]}
62+
slashMenuCommands={[...defaultSlashCommands]}
7163
theme={theme}
7264
limit={3000}
7365
placeholder={{
Lines changed: 76 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Editor, Range } from "@tiptap/core";
2-
import { SlashCommandItem } from "./slash-command";
2+
import { SlashCommandGroup, SlashCommandItem } from "./slash-command";
33
import {
44
Heading1,
55
Heading2,
@@ -11,79 +11,85 @@ import {
1111
Code,
1212
} from "lucide-react";
1313

14-
export const defaultSlashCommands: SlashCommandItem[] = [
14+
export const defaultSlashCommands: SlashCommandGroup[] = [
1515
{
16-
title: "Text",
17-
description: "Just start typing with plain text.",
18-
alias: ["p", "paragraph"],
19-
icon: Text,
20-
command: (editor) => {
21-
editor.chain().focus().toggleNode("paragraph", "paragraph").run();
22-
},
16+
title: "Headings",
17+
commands: [
18+
{
19+
title: "Heading 1",
20+
description: "Big section heading.",
21+
alias: ["title", "big", "large"],
22+
icon: Heading1,
23+
command: (editor) => {
24+
editor.chain().focus().setNode("heading", { level: 1 }).run();
25+
},
26+
},
27+
{
28+
title: "Heading 2",
29+
description: "Medium section heading.",
30+
alias: ["subtitle", "medium"],
31+
icon: Heading2,
32+
command: (editor) => {
33+
editor.chain().focus().setNode("heading", { level: 2 }).run();
34+
},
35+
},
36+
{
37+
title: "Heading 3",
38+
description: "Small section heading.",
39+
alias: ["subtitle", "small"],
40+
icon: Heading3,
41+
command: (editor) => {
42+
editor.chain().focus().setNode("heading", { level: 3 }).run();
43+
},
44+
},
45+
],
2346
},
2447
{
25-
title: "Heading 1",
26-
description: "Big section heading.",
27-
alias: ["title", "big", "large"],
28-
icon: Heading1,
29-
command: (editor) => {
30-
editor.chain().focus().setNode("heading", { level: 1 }).run();
31-
},
48+
title: "Lists",
49+
commands: [
50+
{
51+
title: "Bullet List",
52+
description: "Create a simple bullet list.",
53+
alias: ["unordered", "point"],
54+
icon: List,
55+
command: (editor) => {
56+
editor.chain().focus().toggleBulletList().run();
57+
},
58+
},
59+
{
60+
title: "Numbered List",
61+
description: "Create a list with numbering.",
62+
alias: ["ordered"],
63+
icon: ListOrdered,
64+
command: (editor) => {
65+
editor.chain().focus().toggleOrderedList().run();
66+
},
67+
},
68+
],
3269
},
3370
{
34-
title: "Heading 2",
35-
description: "Medium section heading.",
36-
alias: ["subtitle", "medium"],
37-
icon: Heading2,
38-
command: (editor) => {
39-
editor.chain().focus().setNode("heading", { level: 2 }).run();
40-
},
41-
},
42-
{
43-
title: "Heading 3",
44-
description: "Small section heading.",
45-
alias: ["subtitle", "small"],
46-
icon: Heading3,
47-
command: (editor) => {
48-
editor.chain().focus().setNode("heading", { level: 3 }).run();
49-
},
50-
},
51-
{
52-
title: "Bullet List",
53-
description: "Create a simple bullet list.",
54-
alias: ["unordered", "point"],
55-
icon: List,
56-
command: (editor) => {
57-
editor.chain().focus().toggleBulletList().run();
58-
},
59-
},
60-
{
61-
title: "Numbered List",
62-
description: "Create a list with numbering.",
63-
alias: ["ordered"],
64-
icon: ListOrdered,
65-
command: (editor) => {
66-
editor.chain().focus().toggleOrderedList().run();
67-
},
68-
},
69-
{
70-
title: "Quote",
71-
description: "Capture a quote.",
72-
alias: ["blockquote"],
73-
icon: TextQuote,
74-
command: (editor) =>
75-
editor
76-
.chain()
77-
.focus()
78-
.toggleNode("paragraph", "paragraph")
79-
.toggleBlockquote()
80-
.run(),
81-
},
82-
{
83-
title: "Code",
84-
description: "Create a code snippet.",
85-
alias: ["codeblock"],
86-
icon: Code,
87-
command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
71+
title: "Formatting",
72+
commands: [
73+
{
74+
title: "Quote",
75+
description: "Capture a quote.",
76+
alias: ["blockquote"],
77+
icon: TextQuote,
78+
command: (editor) =>
79+
editor
80+
.chain()
81+
.focus()
82+
.toggleNode("paragraph", "paragraph")
83+
.toggleBlockquote()
84+
.run(),
85+
},
86+
{
87+
title: "Code",
88+
description: "Create a code snippet.",
89+
alias: ["codeblock"],
90+
icon: Code,
91+
command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
92+
},
93+
],
8894
},
8995
];

packages/eddies/src/components/slash-command/menu.tsx

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
useRef,
77
useLayoutEffect,
88
} from "react";
9-
import { SlashCommandItem } from "./slash-command";
9+
import { SlashCommandGroup, SlashCommandItem } from "./slash-command";
1010
//https://github.com/steven-tey/novel from steven-tey helped a lot with this implmentation
1111

1212
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
@@ -17,9 +17,9 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
1717
const bottom = top + itemHeight;
1818

1919
if (top < container.scrollTop) {
20-
container.scrollTop -= container.scrollTop - top + 5;
20+
container.scrollTop -= container.scrollTop - top + 15;
2121
} else if (bottom > containerHeight + container.scrollTop) {
22-
container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
22+
container.scrollTop += bottom - containerHeight - container.scrollTop + 15;
2323
}
2424
};
2525

@@ -30,17 +30,18 @@ export const CommandMenu = React.forwardRef(
3030
command,
3131
editor,
3232
}: {
33-
items: SlashCommandItem[];
33+
items: SlashCommandGroup[];
3434
command: (item: SlashCommandItem) => void;
3535
editor: any;
3636
},
3737
ref
3838
) => {
3939
const [selectedIndex, setSelectedIndex] = useState(0);
40+
const [groupIndex, setGroupIndex] = useState(0);
4041

4142
const selectItem = useCallback(
42-
(index: number) => {
43-
const item = items[index];
43+
(index: number, groupIndex: number) => {
44+
const item = items[groupIndex]?.commands[index];
4445
if (item) {
4546
command(item);
4647
}
@@ -51,11 +52,11 @@ export const CommandMenu = React.forwardRef(
5152
React.useImperativeHandle(ref, () => ({
5253
onKeyDown: ({ event }: { event: React.KeyboardEvent }) => {
5354
if (event.key === "Enter") {
54-
if (!items.length || selectedIndex === -1) {
55+
if (!items.length || selectedIndex === -1 || groupIndex === -1) {
5556
return false;
5657
}
5758

58-
selectItem(selectedIndex);
59+
selectItem(selectedIndex, groupIndex);
5960

6061
return true;
6162
}
@@ -68,12 +69,44 @@ export const CommandMenu = React.forwardRef(
6869
const onKeyDown = (e: KeyboardEvent) => {
6970
if (e.key === "ArrowUp") {
7071
e.preventDefault();
71-
setSelectedIndex((selectedIndex + items.length - 1) % items.length);
72+
let newCommandIndex = selectedIndex - 1;
73+
let newGroupIndex = groupIndex;
74+
75+
if (newCommandIndex < 0) {
76+
newGroupIndex = groupIndex - 1;
77+
newCommandIndex = items[newGroupIndex]?.commands.length - 1 || 0;
78+
}
79+
80+
if (newGroupIndex < 0) {
81+
newGroupIndex = items.length - 1;
82+
newCommandIndex = items[newGroupIndex].commands.length - 1;
83+
}
84+
85+
setSelectedIndex(newCommandIndex);
86+
setGroupIndex(newGroupIndex);
87+
7288
return true;
7389
}
7490
if (e.key === "ArrowDown") {
7591
e.preventDefault();
76-
setSelectedIndex((selectedIndex + 1) % items.length);
92+
93+
const commands = items[groupIndex].commands;
94+
95+
let newCommandIndex = selectedIndex + 1;
96+
let newGroupIndex = groupIndex;
97+
98+
if (commands.length - 1 < newCommandIndex) {
99+
newCommandIndex = 0;
100+
newGroupIndex = groupIndex + 1;
101+
}
102+
103+
if (items.length - 1 < newGroupIndex) {
104+
newGroupIndex = 0;
105+
}
106+
107+
setSelectedIndex(newCommandIndex);
108+
setGroupIndex(newGroupIndex);
109+
77110
return true;
78111
}
79112
return false;
@@ -86,44 +119,60 @@ export const CommandMenu = React.forwardRef(
86119

87120
useEffect(() => {
88121
setSelectedIndex(0);
122+
setGroupIndex(0);
89123
}, [items]);
90124

91125
const commandListContainer = useRef<HTMLDivElement>(null);
92126

93127
useLayoutEffect(() => {
94128
const container = commandListContainer?.current;
95129

96-
const item = container?.children[selectedIndex] as HTMLElement;
130+
const item = container?.children[groupIndex]?.children[selectedIndex];
97131

98-
if (item && container) updateScrollView(container, item);
132+
if (container && item) {
133+
updateScrollView(container, item as HTMLElement);
134+
}
99135
}, [selectedIndex]);
100136

101137
return items.length > 0 ? (
102138
<div
103139
id="slash-command"
104140
ref={commandListContainer}
105-
className="eddies-z-50 eddies-h-auto eddies-max-h-[330px] eddies-w-72 eddies-overflow-y-auto eddies-rounded-md eddies-bg-color-bg eddies-px-1 eddies-py-2 eddies-transition-all"
141+
className="eddies-z-50 eddies-h-auto eddies-max-h-[330px] eddies-w-72 eddies-overflow-y-auto eddies-rounded-md eddies-bg-color-bg-secondary eddies-px-2 eddies-py-3 eddies-transition-all"
106142
>
107-
{items.map((item: SlashCommandItem, index: number) => {
143+
{items.map((group: SlashCommandGroup, mainIndex: number) => {
108144
return (
109-
<button
110-
className={`eddies-flex eddies-w-full eddies-items-center eddies-space-x-2 eddies-rounded-md eddies-px-2 eddies-py-1 eddies-text-left eddies-text-sm eddies-text-color-text hover:eddies-bg-color-bg-secondary ${
111-
index === selectedIndex
112-
? "eddies-bg-stone-100 eddies-text-color-text-secondary"
113-
: ""
114-
}`}
115-
key={index}
116-
onClick={() => selectItem(index)}
117-
>
118-
<div className="eddies-flex eddies-h-10 eddies-w-10 eddies-items-center eddies-justify-center eddies-rounded-md eddies-border eddies-border-border eddies-bg-color-bg-secondary">
119-
{/* @ts-ignore */}
120-
<item.icon className={"eddies-h-5.5 eddies-w-5.5"} />
121-
</div>
122-
<div>
123-
<p className="eddies-font-medium">{item.title}</p>
124-
<p className="eddies-text-xs">{item.description}</p>
145+
<div key={mainIndex} className="eddies-flex eddies-flex-col">
146+
<p className="eddies-text-xs eddies-font-medium eddies-text-color-text-secondary eddies-pl-2.5">
147+
{group.title}
148+
</p>
149+
<div className="eddies-mt-2">
150+
{group.commands.map((item, index) => (
151+
<button
152+
className={`eddies-mb-2 eddies-flex eddies-w-full eddies-items-center eddies-space-x-2 eddies-rounded-[4px] eddies-px-2.5 eddies-py-1.5 eddies-text-left eddies-text-sm eddies-text-color-text ${
153+
index === selectedIndex && groupIndex === mainIndex
154+
? "eddies-bg-color-bg eddies-text-color-text-secondary"
155+
: "hover:eddies-bg-color-bg"
156+
}`}
157+
key={index}
158+
onClick={() => selectItem(index, groupIndex)}
159+
>
160+
<div className="eddies-flex eddies-h-10 eddies-w-10 eddies-items-center eddies-justify-center eddies-rounded-[4px] eddies-bg-color-bg">
161+
{/* @ts-ignore */}
162+
<item.icon className={"eddies-h-5.5 eddies-w-5.5"} />
163+
</div>
164+
<div>
165+
<p className="eddies-text-base eddies-font-medium">
166+
{item.title}
167+
</p>
168+
<p className="eddies-text-xs eddies-font-light">
169+
{item.description}
170+
</p>
171+
</div>
172+
</button>
173+
))}
125174
</div>
126-
</button>
175+
</div>
127176
);
128177
})}
129178
</div>

0 commit comments

Comments
 (0)