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' |
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, |
||||||
}) |
}) |
||||||
|
@ -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…
Reference in new issue