Skip to content

Commit

Permalink
feat: add slash menu (#2)
Browse files Browse the repository at this point in the history
* feat: add slash menu

* fix divider format
  • Loading branch information
Sachin-chaurasiya authored Nov 28, 2023
1 parent 4e67319 commit 6eebebd
Show file tree
Hide file tree
Showing 8 changed files with 614 additions and 9 deletions.
9 changes: 9 additions & 0 deletions lib/components/BlockEditor/extension/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Placeholder from '@tiptap/extension-placeholder';
import StarterKit from '@tiptap/starter-kit';
import { Focus } from './focus';
import { SlashCommand } from './slashCommand';
import { getSuggestionItems } from './slashCommand/items';
import renderItems from './slashCommand/renderItems';

export const extensions = [
StarterKit,
Expand All @@ -22,4 +25,10 @@ export const extensions = [
},
}),
Focus.configure({ mode: 'deepest' }),
SlashCommand.configure({
slashSuggestion: {
items: getSuggestionItems,
render: renderItems,
},
}),
];
118 changes: 118 additions & 0 deletions lib/components/BlockEditor/extension/slashCommand/SlashCommandList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';
import { isEmpty, noop } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react';
import { isInViewport } from '../../../../utils';

export interface SlashCommandRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean;
}

export const SlashCommandList = forwardRef<SlashCommandRef, SuggestionProps>(
(props, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const { items, command } = props;

const selectItem = (index: number) => {
const item = items[index];

if (item) {
command(item);
}
};

const upHandler = () => {
setSelectedIndex((prev) => {
const newIndex = (prev + items.length - 1) % items.length;
const commandListing = document.getElementById(
`editor-command-${items[newIndex].title}`
);
const commandList = document.getElementById('editor-commands-viewport');
if (
commandList &&
commandListing &&
!isInViewport(commandListing, commandList)
) {
commandListing.scrollIntoView();
}

return newIndex;
});
};

const downHandler = () => {
setSelectedIndex((prev) => {
const newIndex = (prev + 1) % items.length;
const commandListing = document.getElementById(
`editor-command-${items[newIndex].title}`
);
const commandList = document.getElementById('editor-commands-viewport');
if (
commandList &&
commandListing &&
!isInViewport(commandListing, commandList)
) {
commandListing.scrollIntoView();
}

return newIndex;
});
};

const enterHandler = () => {
selectItem(selectedIndex);
};

useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();

return true;
}

if (event.key === 'ArrowDown') {
downHandler();

return true;
}

if (event.key === 'Enter') {
enterHandler();

return true;
}

return false;
},
}));

if (isEmpty(items)) {
return null;
}

return (
<div
className="flex flex-col slash-menu-wrapper"
id="editor-commands-viewport"
>
{items.map((item, index) => (
<div
className={`w-full cursor-pointer p-[12px] flex items-center gap-x-4 ${
index === selectedIndex ? 'bg-gray-200' : ''
}`}
id={`editor-command-${item.title}`}
key={item.title}
onClick={() => selectItem(index)}
onKeyDown={noop}
>
<div className="border rounded-[4px] p-2">{item?.icon}</div>
<div className="flex flex-col">
<span className="font-bold">{item.title}</span>
<span>{item.description}</span>
</div>
</div>
))}
</div>
);
}
);
32 changes: 32 additions & 0 deletions lib/components/BlockEditor/extension/slashCommand/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Extension } from '@tiptap/core';
import { PluginKey } from '@tiptap/pm/state';
import Suggestion, { SuggestionOptions } from '@tiptap/suggestion';

export const slashMenuPluginKey = new PluginKey('slashSuggestion');

export const SlashCommand = Extension.create({
name: 'slashCommand',

addOptions() {
return {
...this.parent?.(),
slashSuggestion: {
char: '/',
startOfLine: true,
command: ({ editor, range, props }) => {
props.command({ editor, range, props });
},
} as Partial<SuggestionOptions>,
};
},

addProseMirrorPlugins() {
return [
Suggestion({
pluginKey: slashMenuPluginKey,
...this.options.slashSuggestion,
editor: this.editor,
}),
];
},
});
Loading

0 comments on commit 6eebebd

Please sign in to comment.