Skip to content

Commit

Permalink
feat: slash command groups
Browse files Browse the repository at this point in the history
  • Loading branch information
malezjaa committed Jan 27, 2024
1 parent 6f38b88 commit dafdc1d
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 138 deletions.
16 changes: 10 additions & 6 deletions apps/docs/src/docs/guides/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ export default function Page() {
slashMenuCommands={[
...defaultSlashCommands,
{
title: "Heading 4",
command: (editor) =>
editor.chain().focus().setNode("heading", { level: 3 }).run(),
description: "Really small heading",
icon: AlertCircle,
},
title: "Custom group",
commands: [
{
title: "Image",
command: (editor) => /** Add an image */,
description: "Add image",
icon: AlertCircle,
}
]
}
]}
/>
);
Expand Down
10 changes: 1 addition & 9 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,7 @@ export default function Page() {
shikiji: data,
}),
]}
slashMenuCommands={[
...defaultSlashCommands,
{
title: "Test",
command: () => console.log("test"),
description: "Do this do get amazing funcion",
icon: AlertCircle,
},
]}
slashMenuCommands={[...defaultSlashCommands]}
theme={theme}
limit={3000}
placeholder={{
Expand Down
146 changes: 76 additions & 70 deletions packages/eddies/src/components/slash-command/default-items.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Editor, Range } from "@tiptap/core";
import { SlashCommandItem } from "./slash-command";
import { SlashCommandGroup, SlashCommandItem } from "./slash-command";
import {
Heading1,
Heading2,
Expand All @@ -11,79 +11,85 @@ import {
Code,
} from "lucide-react";

export const defaultSlashCommands: SlashCommandItem[] = [
export const defaultSlashCommands: SlashCommandGroup[] = [
{
title: "Text",
description: "Just start typing with plain text.",
alias: ["p", "paragraph"],
icon: Text,
command: (editor) => {
editor.chain().focus().toggleNode("paragraph", "paragraph").run();
},
title: "Headings",
commands: [
{
title: "Heading 1",
description: "Big section heading.",
alias: ["title", "big", "large"],
icon: Heading1,
command: (editor) => {
editor.chain().focus().setNode("heading", { level: 1 }).run();
},
},
{
title: "Heading 2",
description: "Medium section heading.",
alias: ["subtitle", "medium"],
icon: Heading2,
command: (editor) => {
editor.chain().focus().setNode("heading", { level: 2 }).run();
},
},
{
title: "Heading 3",
description: "Small section heading.",
alias: ["subtitle", "small"],
icon: Heading3,
command: (editor) => {
editor.chain().focus().setNode("heading", { level: 3 }).run();
},
},
],
},
{
title: "Heading 1",
description: "Big section heading.",
alias: ["title", "big", "large"],
icon: Heading1,
command: (editor) => {
editor.chain().focus().setNode("heading", { level: 1 }).run();
},
title: "Lists",
commands: [
{
title: "Bullet List",
description: "Create a simple bullet list.",
alias: ["unordered", "point"],
icon: List,
command: (editor) => {
editor.chain().focus().toggleBulletList().run();
},
},
{
title: "Numbered List",
description: "Create a list with numbering.",
alias: ["ordered"],
icon: ListOrdered,
command: (editor) => {
editor.chain().focus().toggleOrderedList().run();
},
},
],
},
{
title: "Heading 2",
description: "Medium section heading.",
alias: ["subtitle", "medium"],
icon: Heading2,
command: (editor) => {
editor.chain().focus().setNode("heading", { level: 2 }).run();
},
},
{
title: "Heading 3",
description: "Small section heading.",
alias: ["subtitle", "small"],
icon: Heading3,
command: (editor) => {
editor.chain().focus().setNode("heading", { level: 3 }).run();
},
},
{
title: "Bullet List",
description: "Create a simple bullet list.",
alias: ["unordered", "point"],
icon: List,
command: (editor) => {
editor.chain().focus().toggleBulletList().run();
},
},
{
title: "Numbered List",
description: "Create a list with numbering.",
alias: ["ordered"],
icon: ListOrdered,
command: (editor) => {
editor.chain().focus().toggleOrderedList().run();
},
},
{
title: "Quote",
description: "Capture a quote.",
alias: ["blockquote"],
icon: TextQuote,
command: (editor) =>
editor
.chain()
.focus()
.toggleNode("paragraph", "paragraph")
.toggleBlockquote()
.run(),
},
{
title: "Code",
description: "Create a code snippet.",
alias: ["codeblock"],
icon: Code,
command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
title: "Formatting",
commands: [
{
title: "Quote",
description: "Capture a quote.",
alias: ["blockquote"],
icon: TextQuote,
command: (editor) =>
editor
.chain()
.focus()
.toggleNode("paragraph", "paragraph")
.toggleBlockquote()
.run(),
},
{
title: "Code",
description: "Create a code snippet.",
alias: ["codeblock"],
icon: Code,
command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
},
],
},
];
111 changes: 80 additions & 31 deletions packages/eddies/src/components/slash-command/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
useRef,
useLayoutEffect,
} from "react";
import { SlashCommandItem } from "./slash-command";
import { SlashCommandGroup, SlashCommandItem } from "./slash-command";
//https://github.com/steven-tey/novel from steven-tey helped a lot with this implmentation

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

if (top < container.scrollTop) {
container.scrollTop -= container.scrollTop - top + 5;
container.scrollTop -= container.scrollTop - top + 15;
} else if (bottom > containerHeight + container.scrollTop) {
container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
container.scrollTop += bottom - containerHeight - container.scrollTop + 15;
}
};

Expand All @@ -30,17 +30,18 @@ export const CommandMenu = React.forwardRef(
command,
editor,
}: {
items: SlashCommandItem[];
items: SlashCommandGroup[];
command: (item: SlashCommandItem) => void;
editor: any;
},
ref
) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [groupIndex, setGroupIndex] = useState(0);

const selectItem = useCallback(
(index: number) => {
const item = items[index];
(index: number, groupIndex: number) => {
const item = items[groupIndex]?.commands[index];
if (item) {
command(item);
}
Expand All @@ -51,11 +52,11 @@ export const CommandMenu = React.forwardRef(
React.useImperativeHandle(ref, () => ({
onKeyDown: ({ event }: { event: React.KeyboardEvent }) => {
if (event.key === "Enter") {
if (!items.length || selectedIndex === -1) {
if (!items.length || selectedIndex === -1 || groupIndex === -1) {
return false;
}

selectItem(selectedIndex);
selectItem(selectedIndex, groupIndex);

return true;
}
Expand All @@ -68,12 +69,44 @@ export const CommandMenu = React.forwardRef(
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex((selectedIndex + items.length - 1) % items.length);
let newCommandIndex = selectedIndex - 1;
let newGroupIndex = groupIndex;

if (newCommandIndex < 0) {
newGroupIndex = groupIndex - 1;
newCommandIndex = items[newGroupIndex]?.commands.length - 1 || 0;
}

if (newGroupIndex < 0) {
newGroupIndex = items.length - 1;
newCommandIndex = items[newGroupIndex].commands.length - 1;
}

setSelectedIndex(newCommandIndex);
setGroupIndex(newGroupIndex);

return true;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex((selectedIndex + 1) % items.length);

const commands = items[groupIndex].commands;

let newCommandIndex = selectedIndex + 1;
let newGroupIndex = groupIndex;

if (commands.length - 1 < newCommandIndex) {
newCommandIndex = 0;
newGroupIndex = groupIndex + 1;
}

if (items.length - 1 < newGroupIndex) {
newGroupIndex = 0;
}

setSelectedIndex(newCommandIndex);
setGroupIndex(newGroupIndex);

return true;
}
return false;
Expand All @@ -86,44 +119,60 @@ export const CommandMenu = React.forwardRef(

useEffect(() => {
setSelectedIndex(0);
setGroupIndex(0);
}, [items]);

const commandListContainer = useRef<HTMLDivElement>(null);

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

const item = container?.children[selectedIndex] as HTMLElement;
const item = container?.children[groupIndex]?.children[selectedIndex];

if (item && container) updateScrollView(container, item);
if (container && item) {
updateScrollView(container, item as HTMLElement);
}
}, [selectedIndex]);

return items.length > 0 ? (
<div
id="slash-command"
ref={commandListContainer}
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"
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"
>
{items.map((item: SlashCommandItem, index: number) => {
{items.map((group: SlashCommandGroup, mainIndex: number) => {
return (
<button
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 ${
index === selectedIndex
? "eddies-bg-stone-100 eddies-text-color-text-secondary"
: ""
}`}
key={index}
onClick={() => selectItem(index)}
>
<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">
{/* @ts-ignore */}
<item.icon className={"eddies-h-5.5 eddies-w-5.5"} />
</div>
<div>
<p className="eddies-font-medium">{item.title}</p>
<p className="eddies-text-xs">{item.description}</p>
<div key={mainIndex} className="eddies-flex eddies-flex-col">
<p className="eddies-text-xs eddies-font-medium eddies-text-color-text-secondary eddies-pl-2.5">
{group.title}
</p>
<div className="eddies-mt-2">
{group.commands.map((item, index) => (
<button
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 ${
index === selectedIndex && groupIndex === mainIndex
? "eddies-bg-color-bg eddies-text-color-text-secondary"
: "hover:eddies-bg-color-bg"
}`}
key={index}
onClick={() => selectItem(index, groupIndex)}
>
<div className="eddies-flex eddies-h-10 eddies-w-10 eddies-items-center eddies-justify-center eddies-rounded-[4px] eddies-bg-color-bg">
{/* @ts-ignore */}
<item.icon className={"eddies-h-5.5 eddies-w-5.5"} />
</div>
<div>
<p className="eddies-text-base eddies-font-medium">
{item.title}
</p>
<p className="eddies-text-xs eddies-font-light">
{item.description}
</p>
</div>
</button>
))}
</div>
</button>
</div>
);
})}
</div>
Expand Down
Loading

1 comment on commit dafdc1d

@vercel
Copy link

@vercel vercel bot commented on dafdc1d Jan 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.