iliyaZelenko / tiptap-vuetify

Vuetify editor. Component simplifies integration tiptap editor with vuetify.
https://iliyazelenko.github.io/tiptap-vuetify-demo/
807 stars 126 forks source link

[feature request] Support for Mentions #115

Open deluciame opened 4 years ago

deluciame commented 4 years ago

Hi Ilya,

First of all, amazing work, thank you!

Do you have plan to support the Suggestion (mentions, hastags) feature as described here: https://tiptap.scrumpy.io/suggestions ?

Cheers!

toddb commented 4 years ago

@deluciame It took me a while but I worked out that this already exists. You need to use 'nativeExtensions'. Below is an example I created (inside the cloned project so you'll see MAIN_MODULE.then(({ TiptapVuetify }) => TiptapVuetify) which can be replaced with a general import('tiptap-vuetify'). Take a look at comments throughout as much as a reminder to myself.

After looking at the code, I can see that the approach is reasonably documented rather than explained (ie the approach doesn't jump out at an initial glance — in the end I had to read the code to understand the mechanisms intent :-) ). Still, IMHO it is a solid approach because I can now write extensions back at the tiptap level rather than look to extend tiptap-vuetify.

<template>
  <div>
    <!--
       A reference to editor is returned from tiptap-vuetify
       and now then we get 'commands' through the scoped slot

          Note: this example has a hard-code value (compare with 
                     the tiptap suggestions example https://tiptap.scrumpy.io/suggestions
       -->
    <editor-menu-bar
      v-slot="{ commands }"
      :editor="editor"
    >
      <div class="menubar">
       <!--  
                see orginal suggestions example for toolbar mention that directly injects 
                https://tiptap.scrumpy.io/suggestions
       -->
       <v-btn
          class="menubar__button"
          @click="commands.mention({ id: 1, label: 'Fred Kühn' })"
        >
          <v-icon left>@</v-icon>
          <span>Mention</span>
        </v-btn>
      </div>
    </editor-menu-bar>

    <!--

      1. ensure that native-extensions are set with the mention extension
      2. use the @init to get a reference to the editor

    -->
    <tiptap-vuetify
      v-model="content"
      placeholder="Write something …"
      :extensions="extensions"
      :native-extensions="nativeExtensions"
      output-format="json"
      @init="onInit"
    />
  </div>
</template>

<script>
import { EditorMenuBar, Editor } from 'tiptap'
import { MAIN_MODULE } from '../config'
import { Mention } from 'tiptap-extensions'

export default {
  components: {
    EditorMenuBar,
    // TiptapVuetify: () => import('tiptap-vuetify')
    TiptapVuetify: () => MAIN_MODULE.then(({ TiptapVuetify }) => TiptapVuetify)
  },
  data () {
    return {
      /** A reference to the {@link Editor} as required to instantiate the {@link EditorMenuBar} */
      editor: null,
      /** Editor content as rendered */
      content: null,
      /**
       * Configuration of the extensions in the toolbar and available to other areas eg {@link EditorMenuBar}
       *
       *  Note: these extensions are Tiptap Vuetify extensions
       */
      extensions: null,
      /**
       * Any extensions from the original tiptap library to be included into the {@link Editor} inside tiptap-vuetify
       */
      nativeExtensions: null
    }
  },
  async created () {
    const {
      Heading, Bold, Italic, Strike, Underline, Code, CodeBlock, Paragraph, BulletList, OrderedList, ListItem,
      Link, Blockquote, HardBreak, HorizontalRule, History, Image
    } = await MAIN_MODULE

    this.content = `
          <h2>
            Suggestions
          </h2>
          <p>
            Sometimes it's useful to <strong>mention</strong> someone. That's a feature we're very used to. Under the hood this technique can also be used for other features likes <strong>hashtags</strong> and <strong>commands</strong> – lets call it <em>suggestions</em>.
          </p>
          <p>
            This is an example how to mention some users like <span data-mention-id="1">Philipp Kühn</span> or <span data-mention-id="2">Hans Pagel</span>. Try to type <code>@</code> and a popup (rendered with tippy.js) will appear. You can navigate with arrow keys through a list of suggestions.
          </p>
        `

    // extensions in the two factory forms
    //  1. Class without options (the factory will new it)
    //  2. Class WITH options (factory form of an array with options as the second item)
    this.extensions = [
      History,
      Blockquote,
      Link,
      Underline,
      Strike,
      Italic,
      ListItem,
      BulletList,
      OrderedList,
      [Heading, {             // note: the array form to factory up with options
        options: {
          levels: [1, 2, 3]
        }
      }],
      Bold,
      Code,
      HorizontalRule,
      Paragraph,
      HardBreak
    ]

    // other extensions not found in tiptap-vuetify to be made available
    // on this case, we can then get to the commands through the {@link EditorMenuBar} scoped slot (see template above)
    this.nativeExtensions = [
      new Mention({
        // a list of all suggested items
        items: () => [
          { id: 1, name: 'Philipp Kühn' },
          { id: 2, name: 'Hans Pagel' },
          { id: 3, name: 'Kris Siepert' },
          { id: 4, name: 'Justin Schueler' }
        ]
      })
    ]
  },
  methods: {
    /**
     * NOTE: destructure the editor!
     */
    onInit ({ editor }) {
      this.editor = editor
    }
  }
}
</script>
deluciame commented 4 years ago

@toddb Many thanks for looking it up! I'll try that on my end. Cheers!

ghost commented 3 years ago

@toddb, will this be supported as an extension soon? i'm having trouble implementing this feature

<template>
  <div>
    <ClientOnly>
      <editor-menu-bar v-slot="{ commands }" :editor="editor">
        <div class="menubar">
          <!--  
                see orginal suggestions example for toolbar mention that directly injects 
                https://tiptap.scrumpy.io/suggestions
       -->
          <v-btn class="menubar__button" @click="commands.mention({ id: 1, label: 'Fred Kühn' })">
            <v-icon left>@</v-icon>
            <span>Mention</span>
          </v-btn>
        </div>
      </editor-menu-bar>

      <tiptap-vuetify v-model="localValue" :extensions="extensions" :toolbar-attributes="{ color: 'grey' }" />

      <template #placeholder> Write something.. </template>
    </ClientOnly>
  </div>
</template>

<script>
// import the component and the necessary extensions
import {
  EditorMenuBar,
  TiptapVuetify,
  Heading,
  Bold,
  Italic,
  Strike,
  Underline,
  Code,
  CodeBlock,
  Image,
  Paragraph,
  BulletList,
  OrderedList,
  ListItem,
  Link,
  Blockquote,
  HardBreak,
  HorizontalRule,
  History,
} from "tiptap-vuetify";

import { Mention } from 'tiptap-extensions'

export default {
  components: { TiptapVuetify },
  props: {
    value: {
      type: String,
      default: "",
    },
  },
  data: () => ({
    // ***
    editor: null,

    // declare extensions you want to use
    extensions: [
      History,
      Blockquote,
      Link,
      Underline,
      Strike,
      Italic,
      ListItem,
      BulletList,
      OrderedList,
      [
        Heading,
        {
          options: {
            levels: [1, 2, 3],
          },
        },
      ],
      Bold,
      Link,
      Code,
      CodeBlock,
      Image,
      HorizontalRule,
      Paragraph,
      HardBreak,
    ],
  }),
  computed: {
    localValue: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit("input", value);
      },
    },
  },
};
</script>

and err Unknown custom element: <editor-menu-bar> - did you register the component correctly?

toddb commented 3 years ago

@tomelic I have just been looking at my original comments. My thought was that it was already supported as a pluggable extension rather than out of the box. But IMHO the learning curve is steep to learn all the wiring.

In terms of your message, this is Vue level registration-type issue. You have ClientOnly component that looks globally registered.

In terms of Mention registration, you've got to get on top of registration. One key issues is that the editor instance needs to be shared between components. It is created in tiptap-vuetify but needs to get back into your toolbar. Hence the @init event passing around in the editor instance.

I hope this helps. Personally, each time I come back to using editor components, I have to rebuild my mental model to make sure I have everything clear—hacking a solution has always lead to tears (mine!).

ghost commented 3 years ago

@toddb I removed the ClientOnly, and replicated your example

Did you ever get it to work with the keyboard typed suggestion? (and not just the UI insertion you put in the previous example)?

toddb commented 3 years ago

@tomelic so I take that means you've got a working example. Good work.

re: keyboard. No but didn't try. My use of the mention was quite different and a starting point to understanding the extension and interception points—and be too lazy to fully understand prosemirror from scratch (which in the end wasn't really that feasible, of course!). If this makes any sense, I have used it to create a type of mail merge tagging system (a wsyisyg variable substitution).

ghost commented 3 years ago

The issue is somewhere in the keyboard handler of tiptap.. the Tippy extension simply don't integrate as expected on vuetify / nuxt