Skip to content

Commit

Permalink
fix: clashing heading markdown command with mention/suggestion used w…
Browse files Browse the repository at this point in the history
…ith #.
  • Loading branch information
VV-EE committed Mar 7, 2024
1 parent b3ef574 commit 327928e
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 4 deletions.
70 changes: 70 additions & 0 deletions demos/src/Examples/HeadingWIthMention/React/MentionList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import './MentionList.scss'

import React, {
forwardRef, useEffect, useImperativeHandle,
useState,
} from 'react'

export default forwardRef((props, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0)

const selectItem = index => {
const item = props.items[index]

if (item) {
props.command({ id: item })
}
}

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])

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 className="items">
{props.items.length
? props.items.map((item, index) => (
<button
className={`item ${index === selectedIndex ? 'is-selected' : ''}`}
key={index}
onClick={() => selectItem(index)}
>
{item}
</button>
))
: <div className="item">No result</div>
}
</div>
)
})
25 changes: 25 additions & 0 deletions demos/src/Examples/HeadingWIthMention/React/MentionList.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.items {
background: #fff;
border-radius: 0.5rem;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.8);
font-size: 0.9rem;
overflow: hidden;
padding: 0.2rem;
position: relative;
}

.item {
background: transparent;
border: 1px solid transparent;
border-radius: 0.4rem;
display: block;
margin: 0;
padding: 0.2rem 0.4rem;
text-align: left;
width: 100%;

&.is-selected {
border-color: #000;
}
}
Empty file.
35 changes: 35 additions & 0 deletions demos/src/Examples/HeadingWIthMention/React/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import './styles.scss'

import Mention from '@tiptap/extension-mention'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'

import suggestion from './suggestion.js'

export default () => {
const editor = useEditor({
extensions: [
StarterKit,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
suggestion,
}),
],
content: `
<p>Hi everyone! Don’t forget the daily stand up at 8 AM.</p>
<p><span data-type="mention" data-id="Jennifer Grey"></span> Would you mind to share what you’ve been working on lately? We fear not much happened since Dirty Dancing.
<p><span data-type="mention" data-id="Winona Ryder"></span> <span data-type="mention" data-id="Axl Rose"></span> Let’s go through your most important points quickly.</p>
<p>I have a meeting with <span data-type="mention" data-id="Christina Applegate"></span> and don’t want to come late.</p>
<p>– Thanks, your big boss</p>
`,
})

if (!editor) {
return null
}

return <EditorContent editor={editor} />
}
27 changes: 27 additions & 0 deletions demos/src/Examples/HeadingWIthMention/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
context('/src/Examples/HeadingWithMention/React/', () => {
before(() => {
cy.visit('/src/Examples/HeadingWithMention/React/')
})
beforeEach(() => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.setContent('<p></p>')
})
})

it('selecting element from mention doesn\'t trigget heading', () => {
cy.get('.tiptap')
.type('\n#')
.type('{downarrow}')
.type('{enter}')
.should('contain', 'Cyndi Lauper')
.get('h1')
.should('not.exist')
})
it('can still trigger heading if nothing is selected from mention', () => {
cy.get('.tiptap')
.type('\n# Heading!')
.get('h1')
.should('contain', 'Heading!')

})
})
12 changes: 12 additions & 0 deletions demos/src/Examples/HeadingWIthMention/React/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.tiptap {
> * + * {
margin-top: 0.75em;
}
}

.mention {
border: 1px solid #000;
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
}
94 changes: 94 additions & 0 deletions demos/src/Examples/HeadingWIthMention/React/suggestion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { ReactRenderer } from '@tiptap/react'
import tippy from 'tippy.js'

import MentionList from './MentionList.jsx'

export default {
char: '#',
items: ({ query }) => {
return [
'Lea Thompson',
'Cyndi Lauper',
'Tom Cruise',
'Madonna',
'Jerry Hall',
'Joan Collins',
'Winona Ryder',
'Christina Applegate',
'Alyssa Milano',
'Molly Ringwald',
'Ally Sheedy',
'Debbie Harry',
'Olivia Newton-John',
'Elton John',
'Michael J. Fox',
'Axl Rose',
'Emilio Estevez',
'Ralph Macchio',
'Rob Lowe',
'Jennifer Grey',
'Mickey Rourke',
'John Cusack',
'Matthew Broderick',
'Justine Bateman',
'Lisa Bonet',
]
.filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5)
},

render: () => {
let component
let popup

return {
onStart: props => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
})

if (!props.clientRect) {
return
}

popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})
},

onUpdate(props) {
component.updateProps(props)

if (!props.clientRect) {
return
}

popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},

onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide()

return true
}

return component.ref?.onKeyDown(props)
},

onExit() {
popup[0].destroy()
component.destroy()
},
}
},
}
4 changes: 4 additions & 0 deletions docs/api/nodes/mention.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ Mention.configure({
## Usage
https://embed.tiptap.dev/preview/Nodes/Mention
### Notes
The mention node has higher priority ( 101 instead of the default 100 ) so it can get priority over heading markdown command when used with `#`.
12 changes: 9 additions & 3 deletions packages/extension-heading/src/heading.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { mergeAttributes, Node, textblockTypeInputRule } from '@tiptap/core'
import {
inputRulesPlugin, mergeAttributes, Node, textblockTypeInputRule,
} from '@tiptap/core'

export type Level = 1 | 2 | 3 | 4 | 5 | 6

Expand Down Expand Up @@ -92,8 +94,10 @@ export const Heading = Node.create<HeadingOptions>({
}), {})
},

addInputRules() {
return this.options.levels.map(level => {
// This is added as a PM plugin instead of an input rule so it can be pre-empted by other plugins with higher priority
// Fixes the bug where the '#' char couldn't get to the Suggestion plugin
addProseMirrorPlugins() {
const rules = this.options.levels.map(level => {
return textblockTypeInputRule({
find: new RegExp(`^(#{1,${level}})\\s$`),
type: this.type,
Expand All @@ -102,5 +106,7 @@ export const Heading = Node.create<HeadingOptions>({
},
})
})

return [inputRulesPlugin({ editor: this.editor, rules })]
},
})
2 changes: 1 addition & 1 deletion packages/extension-mention/src/mention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const MentionPluginKey = new PluginKey('mention')

export const Mention = Node.create<MentionOptions>({
name: 'mention',

priority: 101, // Fixes collision with Heading '#' input rule
addOptions() {
return {
HTMLAttributes: {},
Expand Down

0 comments on commit 327928e

Please sign in to comment.