mirror of https://github.com/nocodb/nocodb
DarkPhoenix2704
2 weeks ago
3 changed files with 206 additions and 6 deletions
@ -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> |
@ -1,10 +1,33 @@
|
||||
import * as TipTapMention from '@tiptap/extension-mention' |
||||
|
||||
export const Mention = TipTapMention.Mention.extend({ |
||||
renderHTML({ HTMLAttributes: _ }) { |
||||
return ['span'] |
||||
renderHTML({ HTMLAttributes }) { |
||||
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: _ }) { |
||||
return '' |
||||
renderText({ node }) { |
||||
return `@${node.attrs.id.name || node.attrs.id.email || node.attrs.id.id}` |
||||
}, |
||||
deleteTriggerWithBackspace: true, |
||||
}) |
||||
|
@ -1,9 +1,61 @@
|
||||
import { VueRenderer } from '@tiptap/vue-3' |
||||
import tippy from 'tippy.js' |
||||
|
||||
import MentionList from './MentionList.vue' |
||||
|
||||
export default { |
||||
render: () => { |
||||
let component: VueRenderer |
||||
let popup: any |
||||
|
||||
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…
Reference in new issue