diff --git a/components/editor/live-editor.jsx b/components/editor/live-editor.jsx
index 2b7f706..0569765 100644
--- a/components/editor/live-editor.jsx
+++ b/components/editor/live-editor.jsx
@@ -8,7 +8,8 @@ import { MenuBar } from './menu-bar'
import MarkdownIt from 'markdown-it'
import { AiBubbleMenu } from './ai-bubble-menu'
-import { CommandsList, SlashCommands } from './slash-commands'
+import { SlashCommands } from './slash-commands'
+import { SlashMenuContainer } from './slash-menu-view'
const md = new MarkdownIt()
const CustomCommands = Extension.create({
@@ -40,10 +41,9 @@ const extensions = [
},
}),
SlashCommands.configure({
- commands: [
- { title: "续写内容", options: { command: "continue-writing" } }
- ],
- component: CommandsList
+ items: [{
+ title: "continue"
+ }]
}),
Color.configure({ types: [TextStyle.name, ListItem.name] }),
// @ts-ignore
@@ -73,11 +73,11 @@ const LiveEditor = () => {
})
return (
- <>
+
{ editor &&
}
{ editor && }
- >
+
)
}
diff --git a/components/editor/slash-commands.jsx b/components/editor/slash-commands.jsx
index 29c4bab..2074143 100644
--- a/components/editor/slash-commands.jsx
+++ b/components/editor/slash-commands.jsx
@@ -1,9 +1,12 @@
import { Extension, ReactRenderer } from '@tiptap/react'
-import React from 'react'
import { Suggestion } from '@tiptap/suggestion'
import tippy from 'tippy.js'
+import { SlashMenuContainer } from './slash-menu-view'
+import { useRef } from 'react'
// or try SlashCommands: https://github.com/ueberdosis/tiptap/issues/1508
+const extensionName = `ai-insert`
+
export const SlashCommands = Extension.create({
name: 'slash-command',
addOptions () {
@@ -17,8 +20,8 @@ export const SlashCommands = Extension.create({
Suggestion({
editor: this.editor,
char: this.options.char,
+
command: ({ editor, props }) => {
- console.log('ss')
const { state, dispatch } = editor.view
const { $head, $from } = state.selection
@@ -36,33 +39,28 @@ export const SlashCommands = Extension.create({
editor?.view?.focus()
},
items: ({ query }) => {
- return [
- {
- title: 'H1',
- command: ({ editor, range }) => {
- editor
- .chain()
- .focus()
- .deleteRange(range)
- .setNode('heading', { level: 1 })
- .run()
- },
- },
- ].filter(item => item.title)
+ // todo: match fo query
+ return this.options.items
},
render: () => {
let component
let popup
+ let isEditable
return {
- onStart: props => {
- component = new ReactRenderer(CommandsList, {
+ onStart: (props) => {
+ isEditable = props.editor.isEditable
+ if (!isEditable) return
+
+ component = new ReactRenderer(SlashMenuContainer, {
props,
editor: props.editor,
})
+ console.log(component.element)
+
popup = tippy('body', {
- getReferenceClientRect: props.clientRect,
+ getReferenceClientRect: props.clientRect || (() => props.editor.storage[extensionName].rect),
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
@@ -73,29 +71,28 @@ export const SlashCommands = Extension.create({
},
onUpdate (props) {
- component.updateProps(props)
-
- if (!props.clientRect) {
- return
- }
+ if (!isEditable) return
+ component.updateProps(props)
+ props.editor.storage[extensionName].rect = props.clientRect()
popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown (props) {
+ if (!isEditable) return
+
if (props.event.key === 'Escape') {
popup[0].hide()
-
return true
}
-
return component.ref?.onKeyDown(props)
},
onExit () {
- popup[0].destroy()
+ if (!isEditable) return
+ popup && popup[0].destroy()
component.destroy()
},
}
@@ -105,16 +102,3 @@ export const SlashCommands = Extension.create({
},
})
-export function CommandsList (props) {
- const { items, selectedIndex, selectItem } = props
-
- return (
-
- {items.map(({ title }, idx) => (
- - selectItem(idx)}>
- {title}
-
- ))}
-
- )
-}
diff --git a/components/editor/slash-menu-view.jsx b/components/editor/slash-menu-view.jsx
new file mode 100644
index 0000000..90fafae
--- /dev/null
+++ b/components/editor/slash-menu-view.jsx
@@ -0,0 +1,95 @@
+/**
+ * based on:
+ * https://github.com/ueberdosis/tiptap/issues/1508
+ * MIT License https://github.com/fantasticit/think/blob/main/packages/client/src/tiptap/core/extensions/slash.ts#L11
+ * https://github.com/fantasticit/magic-editor/blob/main/src/extensions/slash/slash-menu-view.tsx#L68
+ */
+
+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]
+
+ if (command) {
+ props.command(command)
+ }
+ }
+
+ const upHandler = () => {
+ setSelectedIndex(
+ (selectedIndex + props.items.length - 1) % props.items.length
+ )
+ }
+
+ const downHandler = () => {
+ setSelectedIndex((selectedIndex + 1) % props.items.length)
+ }
+
+ const enterHandler = () => {
+ selectItem(selectedIndex)
+ }
+
+ 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
+ }
+ }))
+
+ 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
diff --git a/package-lock.json b/package-lock.json
index f244683..39268af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -58,6 +58,7 @@
"next": "latest",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "scroll-into-view-if-needed": "^3.1.0",
"tippy.js": "^6.3.7"
},
"devDependencies": {
@@ -1910,6 +1911,11 @@
"node": ">= 6"
}
},
+ "node_modules/compute-scroll-into-view": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
+ "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg=="
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3100,6 +3106,14 @@
"loose-envify": "^1.1.0"
}
},
+ "node_modules/scroll-into-view-if-needed": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+ "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+ "dependencies": {
+ "compute-scroll-into-view": "^3.0.2"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
diff --git a/package.json b/package.json
index 6d13b72..9ad7f0d 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"next": "latest",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "scroll-into-view-if-needed": "^3.1.0",
"tippy.js": "^6.3.7"
},
"engines": {