diff --git a/components/editor/live-editor.jsx b/components/editor/live-editor.jsx
index 0569765..6873e36 100644
--- a/components/editor/live-editor.jsx
+++ b/components/editor/live-editor.jsx
@@ -8,7 +8,7 @@ import { MenuBar } from './menu-bar'
import MarkdownIt from 'markdown-it'
import { AiBubbleMenu } from './ai-bubble-menu'
-import { SlashCommands } from './slash-commands'
+import { createSlash, SlashCommands } from './slash-commands'
import { SlashMenuContainer } from './slash-menu-view'
const md = new MarkdownIt()
@@ -40,10 +40,17 @@ const extensions = [
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
},
}),
- SlashCommands.configure({
- items: [{
- title: "continue"
- }]
+ createSlash('ai-slash', {
+ items: [
+ {
+ title: '续写',
+ command: 'continue',
+ },
+ {
+ "title": '总结',
+ "command": 'summarize',
+ }
+ ]
}),
Color.configure({ types: [TextStyle.name, ListItem.name] }),
// @ts-ignore
@@ -74,9 +81,9 @@ const LiveEditor = () => {
return (
- { editor &&
}
+ {editor && }
- { editor && }
+ {editor && }
)
}
diff --git a/components/editor/slash-commands.jsx b/components/editor/slash-commands.jsx
index 2074143..aabee99 100644
--- a/components/editor/slash-commands.jsx
+++ b/components/editor/slash-commands.jsx
@@ -1,104 +1,106 @@
-import { Extension, ReactRenderer } from '@tiptap/react'
+import { ReactRenderer } from '@tiptap/react'
+import { Node } from '@tiptap/core'
import { Suggestion } from '@tiptap/suggestion'
import tippy from 'tippy.js'
-import { SlashMenuContainer } from './slash-menu-view'
-import { useRef } from 'react'
+import SlashMenuContainer from './slash-menu-view'
// or try SlashCommands: https://github.com/ueberdosis/tiptap/issues/1508
const extensionName = `ai-insert`
-export const SlashCommands = Extension.create({
- name: 'slash-command',
- addOptions () {
- return {
- char: '/',
- pluginKey: 'slash-/',
- }
- },
- addProseMirrorPlugins () {
- return [
- Suggestion({
- editor: this.editor,
- char: this.options.char,
+export const createSlash = (name, options) => {
+ return Node.create({
+ name: 'slash-command',
+ addOptions () {
+ return {
+ char: '/',
+ pluginKey: 'slash-/',
+ }
+ },
+ addProseMirrorPlugins () {
+ return [
+ Suggestion({
+ editor: this.editor,
+ char: this.options.char,
- command: ({ editor, props }) => {
- const { state, dispatch } = editor.view
- const { $head, $from } = state.selection
- const end = $from.pos
- const from = $head?.nodeBefore?.text
- ? end -
- $head.nodeBefore.text.substring(
- $head.nodeBefore.text.indexOf('/')
- ).length
- : $from.start()
+ command: ({ editor, props }) => {
+ const { state, dispatch } = editor.view
+ const { $head, $from } = state.selection
- const tr = state.tr.deleteRange(from, end)
- dispatch(tr)
- props?.action?.(editor, props.user)
- editor?.view?.focus()
- },
- items: ({ query }) => {
- // todo: match fo query
- return this.options.items
- },
- render: () => {
- let component
- let popup
- let isEditable
+ const end = $from.pos
+ const from = $head?.nodeBefore?.text
+ ? end -
+ $head.nodeBefore.text.substring(
+ $head.nodeBefore.text.indexOf('/')
+ ).length
+ : $from.start()
- return {
- onStart: (props) => {
- isEditable = props.editor.isEditable
- if (!isEditable) return
+ const tr = state.tr.deleteRange(from, end)
+ dispatch(tr)
+ props?.action?.(editor, props.user)
+ editor?.view?.focus()
+ },
+ items: ({ query }) => {
+ // todo: match fo query
+ return options.items
+ },
+ render: () => {
+ let component
+ let popup
+ let isEditable
- component = new ReactRenderer(SlashMenuContainer, {
- props,
- editor: props.editor,
- })
+ return {
+ onStart: (props) => {
+ isEditable = props.editor.isEditable
+ if (!isEditable) return
- console.log(component.element)
+ component = new ReactRenderer(SlashMenuContainer, {
+ props,
+ editor: props.editor,
+ })
- popup = tippy('body', {
- getReferenceClientRect: props.clientRect || (() => props.editor.storage[extensionName].rect),
- appendTo: () => document.body,
- content: component.element,
- showOnCreate: true,
- interactive: true,
- trigger: 'manual',
- placement: 'bottom-start',
- })
- },
+ console.log(component.element)
- onUpdate (props) {
- if (!isEditable) return
+ popup = tippy('body', {
+ getReferenceClientRect: props.clientRect || (() => props.editor.storage[extensionName].rect),
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ })
+ },
- component.updateProps(props)
- props.editor.storage[extensionName].rect = props.clientRect()
- popup[0].setProps({
- getReferenceClientRect: props.clientRect,
- })
- },
+ onUpdate (props) {
+ if (!isEditable) return
- onKeyDown (props) {
- if (!isEditable) return
+ component.updateProps(props)
+ props.editor.storage[extensionName].rect = props.clientRect()
+ popup[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ })
+ },
- if (props.event.key === 'Escape') {
- popup[0].hide()
- return true
- }
- return component.ref?.onKeyDown(props)
- },
+ onKeyDown (props) {
+ if (!isEditable) return
- onExit () {
- if (!isEditable) return
- popup && popup[0].destroy()
- component.destroy()
- },
- }
- },
- }),
- ]
- },
-})
+ if (props.event.key === 'Escape') {
+ popup[0].hide()
+ return true
+ }
+ return component.ref?.onKeyDown(props)
+ },
+ onExit () {
+ if (!isEditable) return
+ popup && popup[0].destroy()
+ component.destroy()
+ },
+ }
+ },
+ }),
+ ]
+ },
+ })
+}
diff --git a/components/editor/slash-menu-view.jsx b/components/editor/slash-menu-view.jsx
index 90fafae..c5bd94b 100644
--- a/components/editor/slash-menu-view.jsx
+++ b/components/editor/slash-menu-view.jsx
@@ -8,88 +8,103 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import scrollIntoView from 'scroll-into-view-if-needed'
-export const SlashMenuContainer = forwardRef((props, ref) => {
- const $container = useRef(null)
- const [selectedIndex, setSelectedIndex] = useState(0)
- const selectItem = index => {
- const command = props.items[index]
+class SlashMenuContainer extends React.Component {
+ constructor(props) {
+ super(props);
- if (command) {
- props.command(command)
- }
- }
+ console.log(this.props);
- const upHandler = () => {
- setSelectedIndex(
- (selectedIndex + props.items.length - 1) % props.items.length
- )
+ this.$container = React.createRef();
+ this.state = {
+ selectedIndex: 0,
+ };
}
- const downHandler = () => {
- setSelectedIndex((selectedIndex + 1) % props.items.length)
+ selectItem = (index) => {
+ const { items, command } = this.props;
+ const selectedCommand = items[index];
+
+ console.log("selectedCommand", selectedCommand)
+
+ if (selectedCommand) {
+ command(selectedCommand);
+ }
+ };
+
+ upHandler = () => {
+ const { items } = this.props;
+ this.setState((prevState) => ({
+ selectedIndex: (prevState.selectedIndex + items.length - 1) % items.length,
+ }));
+ };
+
+ downHandler = () => {
+ const { items } = this.props;
+ this.setState((prevState) => ({
+ selectedIndex: (prevState.selectedIndex + 1) % items.length,
+ }));
+ };
+
+ enterHandler = () => {
+ this.selectItem(this.state.selectedIndex);
+ };
+
+ componentDidMount() {
+ this.setState({ selectedIndex: 0 });
}
- const enterHandler = () => {
- selectItem(selectedIndex)
+ componentDidUpdate(prevProps) {
+ if (prevProps.items !== this.props.items) {
+ this.setState({ selectedIndex: 0 });
+ }
+
+ const { selectedIndex } = this.state;
+ if (!Number.isNaN(selectedIndex + 1)) {
+ const el = this.$container.current?.querySelector(
+ `.slash-menu-item:nth-of-type(${selectedIndex + 1})`
+ );
+ el && el.scrollIntoView({ behavior: 'smooth', scrollMode: 'if-needed' });
+ }
}
- useEffect(() => setSelectedIndex(0), [props.items])
-
- useEffect(() => {
- if (Number.isNaN(selectedIndex + 1)) return
- const el = $container?.current?.querySelector(
- `.slash-menu-item:nth-of-type(${selectedIndex + 1})`
- )
- el && scrollIntoView(el, { behavior: 'smooth', scrollMode: 'if-needed' })
- }, [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
+ onKeyDown = ({ event }) => {
+ if (event.key === 'ArrowUp') {
+ this.upHandler();
+ return true;
+ }
+
+ if (event.key === 'ArrowDown') {
+ this.downHandler();
+ return true;
+ }
+
+ if (event.key === 'Enter') {
+ this.enterHandler();
+ return true;
}
- }))
-
- return (
-
- {props.items.length ? (
- props.items.map((item, index) => {
- return 'divider' in item ? (
-
{item.title}
- ) : (
-
selectItem(index)}>
-
- {item.icon}
-
{item.text}
-
-
-
- )
- })
- ) : (
-
Not Found
- )}
-
- )
-})
-SlashMenuContainer.displayName = "SlashMenuContainer"
\ No newline at end of file
+
+ return false;
+ };
+
+ render() {
+ const { items } = this.props;
+ const { selectedIndex } = this.state;
+
+ return (
+
+ {items.map(({ title }, idx) => (
+ - this.selectItem(idx)}
+ className={selectedIndex === idx ? "is-active" : ""}
+ >
+ {title}
+
+ ))}
+
+ );
+ }
+}
+
+export default SlashMenuContainer;
\ No newline at end of file
diff --git a/styles/global.css b/styles/global.css
index 17e96ce..ccbdce8 100644
--- a/styles/global.css
+++ b/styles/global.css
@@ -214,3 +214,6 @@ input {
}
}
+.is-active {
+ background-color: var(--violet-3);
+}
\ No newline at end of file