Remove files for Tiptap mention editor
The interactions on this were a huge pain in the ass
This commit is contained in:
parent
608f89c788
commit
3b32c5a730
3 changed files with 0 additions and 490 deletions
|
|
@ -1,211 +0,0 @@
|
||||||
.wrapper {
|
|
||||||
border-radius: $input-corner;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.editor {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--text-primary);
|
|
||||||
display: block;
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: $font-regular;
|
|
||||||
overflow: scroll;
|
|
||||||
padding: ($unit * 1.5) $unit-2x;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
// border: 2px solid $blue;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.empty:first-child::before {
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
float: left;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bound {
|
|
||||||
background-color: var(--input-bound-bg);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--input-bound-bg-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mention {
|
|
||||||
border-radius: $item-corner-small;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--null-shadow);
|
|
||||||
background: var(--null-bg);
|
|
||||||
display: inline-flex;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: $medium;
|
|
||||||
font-size: 15px;
|
|
||||||
padding: 1px $unit-half;
|
|
||||||
margin: $unit-fourth;
|
|
||||||
transition: all 0.1s ease-out;
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
display: none;
|
|
||||||
padding: 0 $unit-half;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--null-bg-hover);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--null-shadow-hover);
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid blue;
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-element='fire'] {
|
|
||||||
background: var(--fire-bg);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--fire-shadow);
|
|
||||||
color: var(--fire-text);
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
color: var(--fire-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--fire-bg-hover);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--fire-shadow-hover);
|
|
||||||
color: var(--fire-text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--fire-shadow-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-element='water'] {
|
|
||||||
background: var(--water-bg);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--water-shadow);
|
|
||||||
color: var(--water-text);
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
color: var(--water-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--water-bg-hover);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--water-shadow-hover);
|
|
||||||
color: var(--water-text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--water-shadow-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-element='earth'] {
|
|
||||||
background: var(--earth-bg);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--earth-shadow);
|
|
||||||
color: var(--earth-text);
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
color: var(--earth-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--earth-bg-hover);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--earth-shadow-hover);
|
|
||||||
color: var(--earth-text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--earth-shadow-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-element='wind'] {
|
|
||||||
background: var(--wind-bg);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--wind-shadow);
|
|
||||||
color: var(--wind-text);
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
color: var(--wind-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--wind-shadow-hover);
|
|
||||||
color: var(--wind-text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--wind-shadow-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-element='dark'] {
|
|
||||||
background: var(--dark-bg);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--dark-shadow);
|
|
||||||
color: var(--dark-text);
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--dark-bg-hover);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--dark-shadow-hover);
|
|
||||||
color: var(--dark-text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--dark-shadow-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-element='light'] {
|
|
||||||
background: var(--light-bg);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--light-shadow);
|
|
||||||
color: var(--light-text);
|
|
||||||
|
|
||||||
:global(.remove) {
|
|
||||||
color: var(--light-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--light-bg-hover);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
|
||||||
0 1px 0px var(--light-shadow-hover);
|
|
||||||
color: var(--light-text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--light-shadow-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import { ComponentProps, useCallback, useEffect } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import type { JSONContent } from '@tiptap/core'
|
|
||||||
import { useEditor, EditorContent } from '@tiptap/react'
|
|
||||||
import { useTranslation } from 'next-i18next'
|
|
||||||
|
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
|
||||||
import FilterMention from '~extensions/FilterMention'
|
|
||||||
import NoNewLine from '~extensions/NoNewLine'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
import { mentionSuggestionOptions } from '~utils/mentionSuggestions'
|
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
|
|
||||||
interface Props extends ComponentProps<'div'> {
|
|
||||||
bound?: boolean
|
|
||||||
placeholder?: string
|
|
||||||
onUpdate?: (content: string[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const MentionEditor = ({ bound, placeholder, onUpdate, ...props }: Props) => {
|
|
||||||
const locale = useRouter().locale || 'en'
|
|
||||||
const { t } = useTranslation('common')
|
|
||||||
|
|
||||||
// Setup: Editor
|
|
||||||
const editor = useEditor({
|
|
||||||
content: '',
|
|
||||||
editable: true,
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: classNames({
|
|
||||||
[styles.editor]: true,
|
|
||||||
[styles.bound]: bound,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
Placeholder.configure({
|
|
||||||
emptyEditorClass: styles.empty,
|
|
||||||
placeholder: placeholder,
|
|
||||||
}),
|
|
||||||
NoNewLine,
|
|
||||||
FilterMention.configure({
|
|
||||||
renderLabel({ options, node }) {
|
|
||||||
return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}`
|
|
||||||
},
|
|
||||||
suggestion: mentionSuggestionOptions,
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: classNames({
|
|
||||||
[styles.mention]: true,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onFocus: ({ editor }) => {
|
|
||||||
console.log('Editor reporting that is focused')
|
|
||||||
},
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
const mentions = parseMentions(editor.getJSON())
|
|
||||||
if (onUpdate) onUpdate(mentions)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.wrapper}>
|
|
||||||
<EditorContent editor={editor} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMentions(data: JSONContent) {
|
|
||||||
const mentions: string[] = (data.content || []).flatMap(parseMentions)
|
|
||||||
if (data.type === 'mention') {
|
|
||||||
const granblueId = data.attrs?.id.granblue_id
|
|
||||||
mentions.push(granblueId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...new Set(mentions)]
|
|
||||||
}
|
|
||||||
|
|
||||||
MentionEditor.defaultProps = {
|
|
||||||
bound: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MentionEditor
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
import { mergeAttributes, Node } from '@tiptap/core'
|
|
||||||
import {
|
|
||||||
CustomSuggestion,
|
|
||||||
SuggestionOptions,
|
|
||||||
} from '~extensions/CustomSuggestion'
|
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
||||||
import { PluginKey } from '@tiptap/pm/state'
|
|
||||||
|
|
||||||
export type MentionOptions = {
|
|
||||||
HTMLAttributes: Record<string, any>
|
|
||||||
renderLabel: (props: {
|
|
||||||
options: MentionOptions
|
|
||||||
node: ProseMirrorNode
|
|
||||||
}) => string
|
|
||||||
suggestion: Omit<SuggestionOptions, 'editor'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MentionPluginKey = new PluginKey('mention')
|
|
||||||
|
|
||||||
export const FilterMention = Node.create<MentionOptions>({
|
|
||||||
name: 'mention',
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {},
|
|
||||||
renderLabel({ options, node }) {
|
|
||||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
|
|
||||||
},
|
|
||||||
suggestion: {
|
|
||||||
char: '@',
|
|
||||||
pluginKey: MentionPluginKey,
|
|
||||||
command: ({ editor, range, props }) => {
|
|
||||||
// increase range.to by one when the next node is of type "text"
|
|
||||||
// and starts with a space character
|
|
||||||
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
|
||||||
const overrideSpace = nodeAfter?.text?.startsWith(' ')
|
|
||||||
|
|
||||||
if (overrideSpace) {
|
|
||||||
range.to += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.insertContentAt(range, [
|
|
||||||
{
|
|
||||||
type: this.name,
|
|
||||||
attrs: props,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: ' ',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
.run()
|
|
||||||
|
|
||||||
window.getSelection()?.collapseToEnd()
|
|
||||||
},
|
|
||||||
allow: ({ state, range }) => {
|
|
||||||
const $from = state.doc.resolve(range.from)
|
|
||||||
const type = state.schema.nodes[this.name]
|
|
||||||
const allow = !!$from.parent.type.contentMatch.matchType(type)
|
|
||||||
|
|
||||||
return allow
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
group: 'inline',
|
|
||||||
|
|
||||||
inline: true,
|
|
||||||
|
|
||||||
selectable: false,
|
|
||||||
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
id: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (element) => element.getAttribute('data-id'),
|
|
||||||
renderHTML: (attributes) => {
|
|
||||||
if (!attributes.id) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'data-id': attributes.id,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
label: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (element) => element.getAttribute('data-label'),
|
|
||||||
renderHTML: (attributes) => {
|
|
||||||
if (!attributes.label) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'data-label': attributes.label,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: `span[data-type="${this.name}"]`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ node, HTMLAttributes }) {
|
|
||||||
const removeButton = [
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'remove',
|
|
||||||
onclick: () => {
|
|
||||||
// Add functionality for the button click here
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'\u00D7', // Unicode for the multiplication symbol
|
|
||||||
]
|
|
||||||
|
|
||||||
return [
|
|
||||||
'div',
|
|
||||||
mergeAttributes(
|
|
||||||
{ 'data-type': this.name },
|
|
||||||
{ 'data-element': node.attrs.id.element.slug },
|
|
||||||
{ tabindex: -1 },
|
|
||||||
this.options.HTMLAttributes,
|
|
||||||
HTMLAttributes
|
|
||||||
),
|
|
||||||
this.options.renderLabel({
|
|
||||||
options: this.options,
|
|
||||||
node,
|
|
||||||
}),
|
|
||||||
removeButton,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
renderText({ node }) {
|
|
||||||
return this.options.renderLabel({
|
|
||||||
options: this.options,
|
|
||||||
node,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
addKeyboardShortcuts() {
|
|
||||||
return {
|
|
||||||
Backspace: () =>
|
|
||||||
this.editor.commands.command(({ tr, state }) => {
|
|
||||||
let isMention = false
|
|
||||||
const { selection } = state
|
|
||||||
const { empty, anchor } = selection
|
|
||||||
|
|
||||||
if (!empty) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
|
|
||||||
if (node.type.name === this.name) {
|
|
||||||
isMention = true
|
|
||||||
tr.insertText('', pos, pos + node.nodeSize)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return isMention
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
CustomSuggestion({
|
|
||||||
editor: this.editor,
|
|
||||||
...this.options.suggestion,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default FilterMention
|
|
||||||
Loading…
Reference in a new issue