DEV Community

Cover image for Adding mentions to Flux UI editor
Sergio Peris
Sergio Peris

Posted on • Originally published at sertxu.dev

Adding mentions to Flux UI editor

Flux UI, the official Livewire component library has a Rich text editor built using ProseMirror and Tiptap.

At the time of writing this tutorial, there's no first-party support for mentions onto the text editor, so we need to interact with the underhood Tiptap component.

Tiptap has an official mention extension that we'll use to add mention functionality to Flux's text editor.
You can read more about this extension on the official docs: https://tiptap.dev/docs/editor/extensions/nodes/mention

First, we need to install the Tiptap's mention extension.

npm install @tiptap/extension-mention
Enter fullscreen mode Exit fullscreen mode

We need to also install some dependencies.

npm install tippy.js
npm install @tiptap/suggestion
npm install @tiptap/vue-3
Enter fullscreen mode Exit fullscreen mode

The official docs only provide code samples to React and Vue, as those are the currently only supported way to configure it. We're going to integrate our Flux rich editor with mentions using a Vue 3 component.

The official Flux docs explains how to register custom Tiptap extensions: https://fluxui.dev/components/editor#registering-extensions

Taking that in mind, we're going to register the new mention extension.

import { Mention } from "@tiptap/extension-mention"

document.addEventListener('flux:editor', (e) => {
    e.detail.registerExtensions([
        Mention.configure({ /* ... */ }),
    ])
})
Enter fullscreen mode Exit fullscreen mode

Now that the mention extension is registered, we can start configuring it.

This extension allows us to configure using two different syntax:

  • Single suggestion type, for example: @ for users or # for issues.
  • Multiple suggestion types, for example: @ for users and/or # for issues.

We're going to configure it using the multiple type mention syntax as it supports one or many suggestion types, so it's easier to expand the functionality of mentions when required.

We should provide an array with the different suggestion types.

Mention.configure({
    suggestions: [
        {
            char: '@',
            // Other options of the Suggestion utility
        },
        {
            char: '#',
            // Other options of the Suggestion utility
        },
    ],
})
Enter fullscreen mode Exit fullscreen mode

We're going to focus on the users mentions using the @ character.

Apart from the mention character, we should provide the items method that will return the users that match the user input.

Mention.configure({
    suggestions: [
        {
            char: '@',
            items: ({ query }) => {
                return ['Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna']
                    .filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
                    .slice(0, 5)
            },
        },
...
Enter fullscreen mode Exit fullscreen mode

You can also use Axios to fetch the users from an API endpoint.

Mention.configure({
    suggestions: [
        {
            char: '@',
            items: async ({query}) => {
                const response = await axios.get('/suggestions/users?q=' + query);

                return response.data.data.map(e => ({
                    id: e.id,
                    display: `${e.name} ${e.surname}`,
                    label: `${e.username}`
                }))
            },
        },
...
Enter fullscreen mode Exit fullscreen mode

Next, we should configure the suggestion list that shows the users while typing.

import { mergeAttributes, VueRenderer } from '@tiptap/vue-3'
import tippy from 'tippy.js'
import MentionList from './MentionList.vue'

Mention.configure({
    suggestions: [
        {
            char: '@',
            items: async ({query}) => { /* ... */ },

            render: () => {
                let component
                let popup

                return {
                    onStart(props) {
                        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) {
                        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()
                    },
                }
            },
        },
...
Enter fullscreen mode Exit fullscreen mode

As you can see, we've referenced the MentionList Vue component, this component will be the dynamic suggestion list.

This Vue component will have the following code to render the list, you can adapt it to your needs.

<template>
    <div class="mention-dropdown-menu">
        <template v-if="items.length">
            <button
                :class="{ 'is-selected': index === selectedIndex }"
                v-for="(item, index) in items"
                :key="index"
                @click="selectItem(index)"
            >
                {{ item.display ?? item.label ?? item.id }}
            </button>
        </template>
        <div class="text-sm" v-else>
            No result
        </div>
    </div>
</template>

<script>
export default {
    props: {
        items: {
            type: Array,
            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]

            if (item) {
                this.command(item)
            }
        },
    },
}
</script>
Enter fullscreen mode Exit fullscreen mode

As you're now using Vue 3 in your project, you might need to add the ViteJS Vue plugin.

npm install '@vitejs/plugin-vue'
Enter fullscreen mode Exit fullscreen mode
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
        }),
        vue(),
        tailwindcss(),
    ]
})
Enter fullscreen mode Exit fullscreen mode

With all these pieces in place, you should be able to start mentioning users using Flux rich editor.

Tiptap's mention extension allows you to customize the HTML element should be rendered when you mention a user.

Feel free to try other settings and expand this functionality to more suggestion types!

Top comments (0)