vue.jstiptap

Trying to make buttons active in Tiptap Vue


I'm trying to create buttons for the menu bar on my rich text editor being built with Tiptap Vue 3. I'm having a hard time with the isActive() method because I'm getting an error with the Promise under it, to be more specific: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'isActive')

Here is the code for MenuItem.vue:

<template>
  <button
    class="menu-item"
    :class="{ 'is-active bg-zinc-800 rounded-sm': isActive(editor) }"
    @click="(e) => {
      e.preventDefault();
      action();
    }"
    :title="title"
  >
    <i :class="`ri-${icon} ri-fw`"></i>
  </button>
</template>

<script>
import 'remixicon/fonts/remixicon.css';
export default {
  props: {
    icon: {
      type: String,
      required: true,
    },
    title: {
      type: String,
      required: true,
    },
    action: {
      type: Function,
      required: true,
    },
    editor: {
      type: Object,
      required: true,
    }
  },
  methods: {
    async isActive(editor){
      return await editor.isActive('bold')
    }
  }
}
</script>

MenuBar.vue:

<template>
  <div class="text-xl">
    <template v-for="(item, index) in items">
      <span
          class="divider"
          v-if="item.type === 'divider'"
          :key="`divider${index}`"
      >
        |
      </span>
      <menu-item class="p-1" :editor="editor" v-else :key="index" v-bind="item"/>
    </template>
  </div>
</template>

<script>
import MenuItem from "@/components/new_text_editor/MenuItem.vue";

export default {
  components: {
    MenuItem,
  },
  props: {
    editor: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      items: [
        {
          icon: "bold",
          title: "Bold",
          action: () => this.editor.chain().focus().toggleBold().run(),
        },
        {
          icon: "italic",
          title: "Italic",
          action: () => this.editor.chain().focus().toggleItalic().run(),
        },
      ]
    }
  },
}
</script>

Editor.vue:

<template>
  <menu-bar :editor="editor"></menu-bar>
  <editor-content class="border rounded-md" :editor="editor" />
</template>

<script setup>
import {EditorContent, useEditor} from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import TableHeader from "@tiptap/extension-table-header";
import Link from "@tiptap/extension-link";
import MenuBar from "@/components/new_text_editor/MenuBar.vue";
import BulletList from "@tiptap/extension-bullet-list";
import Bold from "@tiptap/extension-bold";

const CustomBulletList = BulletList.extend({
  addCommands() {
    return {
      ...this.parent?.(),
      toggleBulletClass: (className) => ({ commands }) => {
        return commands.updateAttributes("bulletList", { class: className });
      },
    };
  },
});

const CustomTableHeader = TableHeader.extend({
  addAttributes() {
    return {
      ...this.parent?.(),
      colwidth: {
        default: null,
        parseHTML: (element) => {
          const colwidth = element.getAttribute("colwidth");
          return colwidth ? [parseInt(colwidth, 10)] : null;
        },
        renderHTML: (attributes) => {
          return {
            colwidth: attributes.colwidth,
            style: attributes.colwidth
              ? `width: ${attributes.colwidth}px`
              : null,
          };
        },
      },
    };
  },
});

const CustomLink = Link.extend({
  addAttributes() {
    return {
      ...this.parent?.(),
      rel: {
        default: null,
      },
    };
  },
});

const editor = useEditor({
  content: '',
  extensions: [
    StarterKit,
    CustomBulletList,
    CustomTableHeader,
    CustomLink,
    Bold,
  ],
  editorProps: {
    attributes: {
      class: 'prose dark:prose-invert prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none'
    }
  }
})
</script>

Solution

    1. You cannot use an async method in this case (or at least, you cannot use the promise returned from it). Your isActive method should be synchronous.
    methods: {
      isActive(){
        return this.editor.isActive(this.title.toLowerCase())
      }
    }
    

    Also, you can call the editor directly in template:

      <button
        class="menu-item"
        :class="{ 'is-active bg-zinc-800 rounded-sm': editor.isActive(title.toLowerCase()) }"
        @click="(e) => {
          e.preventDefault();
          action();
        }"
        :title="title"
      >
        <i :class="`ri-${icon} ri-fw`"></i>
      </button>
    
    1. The error is stating that the editor is undefined. You have two options:

    Make sure the editor is initiated before mounting the menu-bar.

    <menu-bar v-if="editor" :editor="editor"></menu-bar>
    

    Or, check if the editor is defined before calling it.

    isActive() {
      return !!this.editor && this.editor.isActive(this.title.toLowerCase());
    }
    

    Working sandbox.