Skip to content

Commit

Permalink
feat: try to merge for slash command
Browse files Browse the repository at this point in the history
  • Loading branch information
phodal committed Nov 21, 2023
1 parent 3010d70 commit 8202680
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 46 deletions.
14 changes: 7 additions & 7 deletions components/editor/live-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -73,11 +73,11 @@ const LiveEditor = () => {
})

return (
<>
<div>
{ editor && <MenuBar editor={editor}/> }
<EditorContent editor={editor}/>
{ editor && <AiBubbleMenu editor={editor}/> }
</>
</div>
)
}

Expand Down
62 changes: 23 additions & 39 deletions components/editor/slash-commands.jsx
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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()
},
}
Expand All @@ -105,16 +102,3 @@ export const SlashCommands = Extension.create({
},
})

export function CommandsList (props) {
const { items, selectedIndex, selectItem } = props

return (
<ul>
{items.map(({ title }, idx) => (
<li key={idx} onClick={() => selectItem(idx)}>
{title}
</li>
))}
</ul>
)
}
95 changes: 95 additions & 0 deletions components/editor/slash-menu-view.jsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div ref={$container}>
{props.items.length ? (
props.items.map((item, index) => {
return 'divider' in item ? (
<div className="slash-menu-item">{item.title}</div>
) : (
<div
className="slash-menu-item"
active={selectedIndex === index}
onClick={() => selectItem(index)}>
<div>
{item.icon}
<p>{item.text}</p>
</div>
<div>
<p>{item.slash}</p>
</div>
</div>
)
})
) : (
<p>Not Found</p>
)}
</div>
)
})
SlashMenuContainer.displayName = "SlashMenuContainer"
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit 8202680

Please sign in to comment.