Browse Source

fix: mentions separation

pull/9766/head
DarkPhoenix2704 2 weeks ago
parent
commit
8f76549750
  1. 125
      packages/nc-gui/helpers/tiptapExtensions/mention/MentionList.vue
  2. 31
      packages/nc-gui/helpers/tiptapExtensions/mention/index.ts
  3. 56
      packages/nc-gui/helpers/tiptapExtensions/mention/suggestion.ts

125
packages/nc-gui/helpers/tiptapExtensions/mention/MentionList.vue

@ -0,0 +1,125 @@
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
},
data() {
return {
selectedIndex: 0,
user: null,
}
},
watch: {
items() {
this.selectedIndex = 0
},
selectedIndex() {
nextTick(() => {
this.scrollToSelected()
})
},
},
created() {
const { user } = useGlobal()
this.user = user
},
methods: {
onKeyDown({ event }) {
if (event.key === 'ArrowUp') {
this.upHandler()
return true
}
if (event.key === 'ArrowDown') {
this.downHandler()
return true
}
if (event.key === 'Enter') {
event.stopPropagation()
event.preventDefault()
this.enterHandler(event)
return true
}
if (event.key === 'Tab') {
this.selectItem(this.selectedIndex)
return true
}
return false
},
scrollToSelected() {
const selectedElement = this.$el.querySelector('.is-selected')
if (selectedElement) {
selectedElement.scrollIntoView({ block: 'nearest' })
}
},
upHandler() {
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
},
enterHandler(e) {
this.selectItem(this.selectedIndex, e)
},
selectItem(index, _e) {
const item = this.items[index]
if (item) {
this.command({
id: {
...item,
isSameUser: `${item?.id === this.user?.id}`,
},
})
}
},
},
}
</script>
<template>
<div class="w-64 bg-white scroll-smooth nc-mention-list nc-scrollbar-md border-1 border-gray-200 rounded-lg max-h-64 !py-2">
<template v-if="items.length">
<div
v-for="(item, index) in items"
:key="index"
:class="{ 'is-selected': index === selectedIndex }"
class="py-2 flex hover:bg-gray-100 transition-all cursor-pointer items-center text-gray-800 pl-4"
@click="selectItem(index, $event)"
>
<GeneralUserIcon :email="item.email" :name="item.name" class="w-4 h-4 mr-2" size="medium" />
<div class="max-w-64 truncate">
{{ item.name && item.name.length > 0 ? item.name : item.email }}
</div>
</div>
</template>
<div v-else class="px-4">No users</div>
</div>
</template>
<style lang="scss" scoped>
.is-selected {
@apply bg-gray-100;
}
</style>

31
packages/nc-gui/helpers/tiptapExtensions/mention/index.ts

@ -1,10 +1,33 @@
import * as TipTapMention from '@tiptap/extension-mention' import * as TipTapMention from '@tiptap/extension-mention'
export const Mention = TipTapMention.Mention.extend({ export const Mention = TipTapMention.Mention.extend({
renderHTML({ HTMLAttributes: _ }) { renderHTML({ HTMLAttributes }) {
return ['span'] const attributes =
typeof HTMLAttributes['data-id'] !== 'object' ? JSON.parse(HTMLAttributes['data-id']) : HTMLAttributes['data-id']
const innerText = attributes.name && attributes.name.length > 0 ? attributes.name : attributes.email
const styles = attributes.isSameUser === 'true' ? 'bg-[#D4F7E0] text-[#17803D]' : 'bg-brand-50 text-brand-500'
return [
'span',
{
'class': `mention font-semibold ${styles} rounded-md px-1`,
'data-type': 'mention',
'data-id': JSON.stringify(HTMLAttributes['data-id']),
},
[
'span',
{
style: 'font-weight: 800;',
},
'@',
],
innerText,
]
}, },
renderText({ node: _ }) { renderText({ node }) {
return '' return `@${node.attrs.id.name || node.attrs.id.email || node.attrs.id.id}`
}, },
deleteTriggerWithBackspace: true,
}) })

56
packages/nc-gui/helpers/tiptapExtensions/mention/suggestion.ts

@ -1,9 +1,61 @@
import { VueRenderer } from '@tiptap/vue-3'
import tippy from 'tippy.js'
import MentionList from './MentionList.vue'
export default { export default {
render: () => { render: () => {
let component: VueRenderer
let popup: any
return { return {
onStart: (_props: Record<string, any>) => {}, onStart: (props: Record<string, any>) => {
component = new VueRenderer(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: Record<string, any>) {
component.updateProps(props)
if (!props.clientRect) {
return
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},
onUpdate(_props: Record<string, any>) {}, onKeyDown(props: Record<string, any>) {
if (props.event.key === 'Escape') {
popup[0].hide()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
popup[0].destroy()
component.destroy()
},
} }
}, },
} }

Loading…
Cancel
Save