CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tiptap--vue-3

Vue 3 components and composables for building rich text editors with tiptap

Pending
Overview
Eval results
Files

menu-components.mddocs/

Menu Components

Interactive menu components that float above or beside editor content, providing contextual editing tools and actions.

Capabilities

BubbleMenu Component

A floating menu that appears when text is selected, providing quick access to formatting options.

/**
 * Floating bubble menu that appears on text selection
 * Uses Vue Teleport to render outside component tree
 */
const BubbleMenu: DefineComponent<{
  /** Plugin identifier for registration */
  pluginKey?: {
    type: PropType<BubbleMenuPluginProps['pluginKey']>;
    default: 'bubbleMenu';
  };
  
  /** Editor instance (required) */
  editor: {
    type: PropType<Editor>;
    required: true;
  };
  
  /** Delay in milliseconds before updating position */
  updateDelay?: {
    type: PropType<number>;
    default: undefined;
  };
  
  /** Delay in milliseconds before updating on resize */
  resizeDelay?: {
    type: PropType<number>;
    default: undefined;
  };
  
  /** Additional plugin options */
  options?: {
    type: PropType<BubbleMenuPluginOptions>;
    default: () => ({});
  };
  
  /** Custom function to determine when menu should show */
  shouldShow?: {
    type: PropType<(props: { editor: Editor; view: EditorView; state: EditorState; oldState?: EditorState; from: number; to: number; }) => boolean>;
    default: null;
  };
}>;

interface BubbleMenuPluginOptions {
  /** Custom positioning element */
  element?: HTMLElement;
  /** Custom tippyjs options */
  tippyOptions?: Record<string, any>;
  /** Custom placement */
  placement?: 'top' | 'bottom' | 'left' | 'right';
}

Key Features:

  • Automatically positions near text selection
  • Uses Vue Teleport to render in document body
  • Customizable show/hide logic
  • Configurable update and resize delays
  • Integrates with @floating-ui/dom for positioning

Usage Examples:

<template>
  <div v-if="editor">
    <BubbleMenu :editor="editor" :updateDelay="100">
      <div class="bubble-menu">
        <button
          @click="editor.chain().focus().toggleBold().run()"
          :class="{ 'is-active': editor.isActive('bold') }"
        >
          Bold
        </button>
        <button
          @click="editor.chain().focus().toggleItalic().run()"
          :class="{ 'is-active': editor.isActive('italic') }"
        >
          Italic
        </button>
        <button
          @click="editor.chain().focus().toggleStrike().run()"
          :class="{ 'is-active': editor.isActive('strike') }"
        >
          Strike
        </button>
      </div>
    </BubbleMenu>
    <EditorContent :editor="editor" />
  </div>
</template>

<script setup>
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";

const editor = useEditor({
  content: '<p>Select some text to see the bubble menu!</p>',
  extensions: [StarterKit],
});
</script>

<style>
.bubble-menu {
  display: flex;
  background: white;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.1);
  border-radius: 0.5rem;
  overflow: hidden;
}

.bubble-menu button {
  border: none;
  background: none;
  padding: 0.5rem;
  cursor: pointer;
}

.bubble-menu button.is-active {
  background: #e5e7eb;
}
</style>

Custom shouldShow Logic:

<template>
  <BubbleMenu 
    :editor="editor" 
    :shouldShow="customShouldShow"
  >
    <div class="custom-bubble-menu">
      <!-- Menu content -->
    </div>
  </BubbleMenu>
</template>

<script setup>
const customShouldShow = ({ editor, view, state, from, to }) => {
  // Only show for text selections (not node selections)
  if (from === to) return false;
  
  // Only show if selection contains bold text
  return editor.isActive('bold');
};
</script>

FloatingMenu Component

A floating menu that appears on empty lines, providing quick access to block-level formatting options.

/**
 * Floating menu that appears on empty lines
 * Uses Vue Teleport to render outside component tree
 */
const FloatingMenu: DefineComponent<{
  /** Plugin identifier for registration */
  pluginKey?: {
    type: null;
    default: 'floatingMenu';
  };
  
  /** Editor instance (required) */
  editor: {
    type: PropType<Editor>;
    required: true;
  };
  
  /** Additional plugin options */
  options?: {
    type: PropType<FloatingMenuPluginOptions>;
    default: () => ({});
  };
  
  /** Custom function to determine when menu should show */
  shouldShow?: {
    type: PropType<(props: { editor: Editor; view: EditorView; state: EditorState; oldState?: EditorState; }) => boolean>;
    default: null;
  };
}>;

interface FloatingMenuPluginOptions {
  /** Custom positioning element */
  element?: HTMLElement;
  /** Custom tippyjs options */
  tippyOptions?: Record<string, any>;
  /** Custom placement */
  placement?: 'top' | 'bottom' | 'left' | 'right';
}

Key Features:

  • Appears on empty paragraphs or lines
  • Uses Vue Teleport for body-level rendering
  • Customizable visibility conditions
  • Integrates with @floating-ui/dom for positioning

Usage Examples:

<template>
  <div v-if="editor">
    <FloatingMenu :editor="editor">
      <div class="floating-menu">
        <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
          H1
        </button>
        <button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">
          H2
        </button>
        <button @click="editor.chain().focus().toggleBulletList().run()">
          Bullet List
        </button>
        <button @click="editor.chain().focus().toggleOrderedList().run()">
          Ordered List
        </button>
      </div>
    </FloatingMenu>
    <EditorContent :editor="editor" />
  </div>
</template>

<script setup>
import { useEditor, EditorContent, FloatingMenu } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";

const editor = useEditor({
  content: '<p></p>',
  extensions: [StarterKit],
});
</script>

<style>
.floating-menu {
  display: flex;
  background: white;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.1);
  border-radius: 0.5rem;
  overflow: hidden;
}

.floating-menu button {
  border: none;
  background: none;
  padding: 0.5rem;
  cursor: pointer;
}
</style>

Custom shouldShow for FloatingMenu:

<template>
  <FloatingMenu 
    :editor="editor" 
    :shouldShow="customShouldShow"
  >
    <div class="floating-menu">
      <!-- Menu content -->
    </div>
  </FloatingMenu>
</template>

<script setup>
const customShouldShow = ({ editor, view, state }) => {
  const { selection } = state;
  const { $anchor, empty } = selection;
  
  // Only show on empty paragraphs
  if (!empty) return false;
  
  // Check if current node is a paragraph
  if ($anchor.parent.type.name !== 'paragraph') return false;
  
  // Only show if paragraph is empty
  return $anchor.parent.textContent === '';
};
</script>

Menu Styling and Positioning

Advanced Styling:

<template>
  <BubbleMenu 
    :editor="editor"
    :options="{ 
      placement: 'top',
      tippyOptions: { 
        duration: 100,
        animation: 'shift-away' 
      }
    }"
  >
    <div class="menu-container">
      <!-- Menu buttons -->
    </div>
  </BubbleMenu>
</template>

<style>
.menu-container {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 8px;
  padding: 8px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
}
</style>

Multiple Menu Integration:

<template>
  <div v-if="editor">
    <!-- Bubble menu for text formatting -->
    <BubbleMenu :editor="editor" pluginKey="textBubbleMenu">
      <div class="text-menu">
        <button @click="toggleBold">Bold</button>
        <button @click="toggleItalic">Italic</button>
      </div>
    </BubbleMenu>
    
    <!-- Floating menu for block elements -->
    <FloatingMenu :editor="editor" pluginKey="blockFloatingMenu">
      <div class="block-menu">
        <button @click="addHeading">Heading</button>
        <button @click="addList">List</button>
      </div>
    </FloatingMenu>
    
    <EditorContent :editor="editor" />
  </div>
</template>

Types

interface BubbleMenuPluginProps {
  pluginKey: string | PluginKey;
  editor: Editor;
  element: HTMLElement;
  updateDelay?: number;
  resizeDelay?: number;
  options?: Record<string, any>;
  shouldShow?: ((props: {
    editor: Editor;
    view: EditorView;
    state: EditorState;
    oldState?: EditorState;
    from: number;
    to: number;
  }) => boolean) | null;
}

interface FloatingMenuPluginProps {
  pluginKey: string | PluginKey;
  editor: Editor;
  element: HTMLElement;
  options?: Record<string, any>;
  shouldShow?: ((props: {
    editor: Editor;
    view: EditorView;
    state: EditorState;
    oldState?: EditorState;
  }) => boolean) | null;
}

Install with Tessl CLI

npx tessl i tessl/npm-tiptap--vue-3

docs

core-editor-api.md

editor-management.md

index.md

menu-components.md

vue-components.md

vue-renderers.md

tile.json