User Mentions in editor and tasks #2641
@ -78,6 +78,7 @@
|
||||
"@tiptap/extension-italic": "2.5.4",
|
||||
"@tiptap/extension-link": "2.5.4",
|
||||
"@tiptap/extension-list-item": "2.5.4",
|
||||
"@tiptap/extension-mention": "2.5.4",
|
||||
"@tiptap/extension-ordered-list": "2.5.4",
|
||||
"@tiptap/extension-paragraph": "2.5.4",
|
||||
"@tiptap/extension-placeholder": "2.5.4",
|
||||
|
16
frontend/pnpm-lock.yaml
generated
16
frontend/pnpm-lock.yaml
generated
@ -100,6 +100,9 @@ importers:
|
||||
'@tiptap/extension-list-item':
|
||||
specifier: 2.5.4
|
||||
version: 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))
|
||||
'@tiptap/extension-mention':
|
||||
specifier: 2.5.4
|
||||
version: 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4)(@tiptap/suggestion@2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4))
|
||||
'@tiptap/extension-ordered-list':
|
||||
specifier: 2.5.4
|
||||
version: 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))
|
||||
@ -2179,6 +2182,13 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.5.4
|
||||
|
||||
'@tiptap/extension-mention@2.5.4':
|
||||
resolution: {integrity: sha512-U5Kqjhs7FraJzopZydy14/v0+X6unmfYYt42QHhVeSEdZ8y7QtyFigJktJUBzE12CpwGkyh8e3xI9Ozi7lFb0w==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.5.4
|
||||
'@tiptap/pm': ^2.5.4
|
||||
'@tiptap/suggestion': ^2.5.4
|
||||
|
||||
'@tiptap/extension-ordered-list@2.5.4':
|
||||
resolution: {integrity: sha512-cl3cTJitY6yDUmxqgjDUtDWCyX1VVsZNJ6i9yiPeARcxvzFc81KmUJxTGl8WPT5TjqmM+TleRkZjsxgvXX57+Q==}
|
||||
peerDependencies:
|
||||
@ -8161,6 +8171,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/core': 2.5.4(@tiptap/pm@2.5.4)
|
||||
|
||||
'@tiptap/extension-mention@2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4)(@tiptap/suggestion@2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.5.4(@tiptap/pm@2.5.4)
|
||||
'@tiptap/pm': 2.5.4
|
||||
'@tiptap/suggestion': 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4)
|
||||
|
||||
'@tiptap/extension-ordered-list@2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.5.4(@tiptap/pm@2.5.4)
|
||||
|
144
frontend/src/components/input/editor/MentionList.vue
Normal file
144
frontend/src/components/input/editor/MentionList.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="items">
|
||||
<template v-if="items.length">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="item"
|
||||
:class="{ 'is-selected': index === selectedIndex }"
|
||||
@click="selectItem(index)"
|
||||
>
|
||||
<p class="username">
|
||||
@{{ item.username }}
|
||||
</p>
|
||||
<p class="name">
|
||||
{{ item.name }}
|
||||
</p>
|
||||
</button>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="item"
|
||||
>
|
||||
No result
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/* eslint-disable vue/component-api-style */
|
||||
|
||||
export default {
|
||||
konrad
commented
Please use the vue composition API so that it is consistent with the rest of the codebase. Please use the vue composition API so that it is consistent with the rest of the codebase.
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
editor: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
command: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
items() {
|
||||
this.selectedIndex = 0
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown({event}) {
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
upHandler() {
|
||||
this.selectedIndex = ((this.selectedIndex + this.items.length) - 1) % this.items.length
|
||||
},
|
||||
|
||||
downHandler() {
|
||||
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
|
||||
},
|
||||
|
||||
enterHandler() {
|
||||
this.selectItem(this.selectedIndex)
|
||||
},
|
||||
|
||||
selectItem(index) {
|
||||
const item = this.items[index]
|
||||
this.command({id: item.username})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.items {
|
||||
padding: 0.2rem;
|
||||
position: relative;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--white);
|
||||
color: var(--grey-900);
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border-radius: $radius;
|
||||
border: 0;
|
||||
padding: 0.2rem 0.4rem;
|
||||
transition: background-color $transition;
|
||||
|
||||
&.is-selected, &:hover {
|
||||
background: var(--grey-100);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.item .name {
|
||||
font-size: .8rem;
|
||||
color: var(--grey-600);
|
||||
}
|
||||
|
||||
.item p {
|
||||
|
||||
font-size: .9rem;
|
||||
color: var(--grey-800);
|
||||
|
||||
|
||||
}
|
||||
</style>
|
@ -168,6 +168,7 @@ import {History} from '@tiptap/extension-history'
|
||||
import {HorizontalRule} from '@tiptap/extension-horizontal-rule'
|
||||
import {Italic} from '@tiptap/extension-italic'
|
||||
import {ListItem} from '@tiptap/extension-list-item'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import {OrderedList} from '@tiptap/extension-ordered-list'
|
||||
import {Paragraph} from '@tiptap/extension-paragraph'
|
||||
import {Strike} from '@tiptap/extension-strike'
|
||||
@ -177,6 +178,8 @@ import {Node} from '@tiptap/pm/model'
|
||||
|
||||
import Commands from './commands'
|
||||
import suggestionSetup from './suggestion'
|
||||
import mentionSuggestionSetup from './mention'
|
||||
|
||||
|
||||
import {lowlight} from 'lowlight'
|
||||
|
||||
@ -392,6 +395,12 @@ const extensions : Extensions = [
|
||||
)).test(href),
|
||||
protocols: additionalLinkProtocols,
|
||||
}),
|
||||
Mention.configure({
|
||||
suggestion: mentionSuggestionSetup(t),
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
@ -1025,4 +1034,10 @@ ul.tiptap__editor-actions {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.mention {
|
||||
background: var(--grey-200);
|
||||
border-radius: $radius;
|
||||
padding: .15rem .25rem;
|
||||
}
|
||||
</style>
|
||||
|
85
frontend/src/components/input/editor/mention.ts
Normal file
85
frontend/src/components/input/editor/mention.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {VueRenderer} from '@tiptap/vue-3'
|
||||
import tippy from 'tippy.js'
|
||||
import UserService from '@/services/user'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
|
||||
import MentionList from './MentionList.vue'
|
||||
|
||||
export default function mentionSuggestionSetup() {
|
||||
|
||||
const userService = new UserService()
|
||||
|
||||
return {
|
||||
items: async ({query}: { query: string }) => {
|
||||
|
||||
if (query === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await userService.getAll({}, {s: query}) as IUser[]
|
||||
|
||||
|
||||
|
||||
return response.filter(item => item.username.toLowerCase().startsWith(query.toLowerCase()))
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let component: VueRenderer
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
|
||||
component = new VueRenderer(MentionList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
konrad
commented
Please remove this commented code Please remove this commented code
|
||||
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()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user
This should be translated.