EditorToolbar

GitHub
A customizable toolbar for editor actions that can be displayed as fixed, bubble, or floating menu.

Usage

The EditorToolbar component displays a toolbar of formatting buttons that automatically sync their active state with the editor content. It supports three layout modes using the @tiptap/vue-3/menus package:

  • fixed (always visible)
  • bubble (appears on text selection)
  • floating (appears on empty lines)
It must be used inside an Editor component's default slot to have access to the editor instance.
<script setup lang="ts">
import type { EditorToolbarItem } from 'pohon-ui'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: Array<Array<EditorToolbarItem>> = [
  [
    {
      icon: 'i-lucide-heading',
      tooltip: { text: 'Headings' },
      content: {
        align: 'start'
      },
      items: [
        {
          kind: 'heading',
          level: 1,
          icon: 'i-lucide-heading-1',
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          icon: 'i-lucide-heading-2',
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          icon: 'i-lucide-heading-3',
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          icon: 'i-lucide-heading-4',
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: 'i-lucide-bold',
      tooltip: { text: 'Bold' }
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: 'i-lucide-italic',
      tooltip: { text: 'Italic' }
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: 'i-lucide-underline',
      tooltip: { text: 'Underline' }
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: 'i-lucide-strikethrough',
      tooltip: { text: 'Strikethrough' }
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: 'i-lucide-code',
      tooltip: { text: 'Code' }
    }
  ]
]
</script>

<template>
  <PEditor v-slot="{ editor }" v-model="value" content-type="markdown" class="min-h-21 w-full">
    <PEditorToolbar :editor="editor" :items="items" layout="bubble" />
  </PEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorToolbarItem } from 'pohon-ui'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: Array<Array<EditorToolbarItem>> = [
  [
    {
      icon: 'i-lucide-heading',
      tooltip: { text: 'Headings' },
      content: {
        align: 'start'
      },
      items: [
        {
          kind: 'heading',
          level: 1,
          icon: 'i-lucide-heading-1',
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          icon: 'i-lucide-heading-2',
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          icon: 'i-lucide-heading-3',
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          icon: 'i-lucide-heading-4',
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: 'i-lucide-bold',
      tooltip: { text: 'Bold' }
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: 'i-lucide-italic',
      tooltip: { text: 'Italic' }
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: 'i-lucide-underline',
      tooltip: { text: 'Underline' }
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: 'i-lucide-strikethrough',
      tooltip: { text: 'Strikethrough' }
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: 'i-lucide-code',
      tooltip: { text: 'Code' }
    }
  ]
]
</script>

<template>
  <PEditor v-slot="{ editor }" v-model="value" content-type="markdown" class="min-h-21 w-full">
    <PEditorToolbar :editor="editor" :items="items" layout="bubble" />
  </PEditor>
</template>
The bubble and floating layouts use TipTap's BubbleMenu and FloatingMenu extensions.

Items

Use the items prop as an array of objects with the following properties:

You can pass any property from the Button component such as color, variant, size, etc.

<script setup lang="ts">
import type { EditorToolbarItem } from 'pohon-ui';
import { TextAlign } from '@tiptap/extension-text-align';

const value = ref(`This toolbar showcases **all available formatting options** using built-in handlers. Try the different controls to see them in action!

You can apply **bold**, *italic*, <u>underline</u>, ~~strikethrough~~, and \`inline code\` formatting to your text.
`);

const items: Array<Array<EditorToolbarItem>> = [
  // History controls
  [{
    kind: 'undo',
    icon: 'i-lucide-undo',
    tooltip: { text: 'Undo' },
  }, {
    kind: 'redo',
    icon: 'i-lucide-redo',
    tooltip: { text: 'Redo' },
  }],
  // Block types
  [{
    icon: 'i-lucide-heading',
    tooltip: { text: 'Headings' },
    content: {
      align: 'start',
    },
    items: [{
      kind: 'heading',
      level: 1,
      icon: 'i-lucide-heading-1',
      label: 'Heading 1',
    }, {
      kind: 'heading',
      level: 2,
      icon: 'i-lucide-heading-2',
      label: 'Heading 2',
    }, {
      kind: 'heading',
      level: 3,
      icon: 'i-lucide-heading-3',
      label: 'Heading 3',
    }, {
      kind: 'heading',
      level: 4,
      icon: 'i-lucide-heading-4',
      label: 'Heading 4',
    }],
  }, {
    icon: 'i-lucide-list',
    tooltip: { text: 'Lists' },
    content: {
      align: 'start',
    },
    items: [{
      kind: 'bulletList',
      icon: 'i-lucide-list',
      label: 'Bullet List',
    }, {
      kind: 'orderedList',
      icon: 'i-lucide-list-ordered',
      label: 'Ordered List',
    }],
  }, {
    kind: 'blockquote',
    icon: 'i-lucide-text-quote',
    tooltip: { text: 'Blockquote' },
  }, {
    kind: 'codeBlock',
    icon: 'i-lucide-square-code',
    tooltip: { text: 'Code Block' },
  }, {
    kind: 'horizontalRule',
    icon: 'i-lucide-separator-horizontal',
    tooltip: { text: 'Horizontal Rule' },
  }],
  // Text formatting
  [{
    kind: 'mark',
    mark: 'bold',
    icon: 'i-lucide-bold',
    tooltip: { text: 'Bold' },
  }, {
    kind: 'mark',
    mark: 'italic',
    icon: 'i-lucide-italic',
    tooltip: { text: 'Italic' },
  }, {
    kind: 'mark',
    mark: 'underline',
    icon: 'i-lucide-underline',
    tooltip: { text: 'Underline' },
  }, {
    kind: 'mark',
    mark: 'strike',
    icon: 'i-lucide-strikethrough',
    tooltip: { text: 'Strikethrough' },
  }, {
    kind: 'mark',
    mark: 'code',
    icon: 'i-lucide-code',
    tooltip: { text: 'Code' },
  }],
  // Link
  [{
    kind: 'link',
    icon: 'i-lucide-link',
    tooltip: { text: 'Link' },
  }],
  // Text alignment
  [{
    icon: 'i-lucide-align-justify',
    tooltip: { text: 'Text Align' },
    content: {
      align: 'end',
    },
    items: [{
      kind: 'textAlign',
      align: 'left',
      icon: 'i-lucide-align-left',
      label: 'Align Left',
    }, {
      kind: 'textAlign',
      align: 'center',
      icon: 'i-lucide-align-center',
      label: 'Align Center',
    }, {
      kind: 'textAlign',
      align: 'right',
      icon: 'i-lucide-align-right',
      label: 'Align Right',
    }, {
      kind: 'textAlign',
      align: 'justify',
      icon: 'i-lucide-align-justify',
      label: 'Align Justify',
    }],
  }],
];
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] })]"
    class="flex flex-col gap-4 min-h-37 w-full"
  >
    <PEditorToolbar
      :editor="editor"
      :items="items"
      class="overflow-x-auto sm:px-8"
    />
  </PEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorToolbarItem } from 'pohon-ui';
import { TextAlign } from '@tiptap/extension-text-align';

const value = ref(`This toolbar showcases **all available formatting options** using built-in handlers. Try the different controls to see them in action!

You can apply **bold**, *italic*, <u>underline</u>, ~~strikethrough~~, and \`inline code\` formatting to your text.
`);

const items: Array<Array<EditorToolbarItem>> = [
  // History controls
  [{
    kind: 'undo',
    icon: 'i-lucide-undo',
    tooltip: { text: 'Undo' },
  }, {
    kind: 'redo',
    icon: 'i-lucide-redo',
    tooltip: { text: 'Redo' },
  }],
  // Block types
  [{
    icon: 'i-lucide-heading',
    tooltip: { text: 'Headings' },
    content: {
      align: 'start',
    },
    items: [{
      kind: 'heading',
      level: 1,
      icon: 'i-lucide-heading-1',
      label: 'Heading 1',
    }, {
      kind: 'heading',
      level: 2,
      icon: 'i-lucide-heading-2',
      label: 'Heading 2',
    }, {
      kind: 'heading',
      level: 3,
      icon: 'i-lucide-heading-3',
      label: 'Heading 3',
    }, {
      kind: 'heading',
      level: 4,
      icon: 'i-lucide-heading-4',
      label: 'Heading 4',
    }],
  }, {
    icon: 'i-lucide-list',
    tooltip: { text: 'Lists' },
    content: {
      align: 'start',
    },
    items: [{
      kind: 'bulletList',
      icon: 'i-lucide-list',
      label: 'Bullet List',
    }, {
      kind: 'orderedList',
      icon: 'i-lucide-list-ordered',
      label: 'Ordered List',
    }],
  }, {
    kind: 'blockquote',
    icon: 'i-lucide-text-quote',
    tooltip: { text: 'Blockquote' },
  }, {
    kind: 'codeBlock',
    icon: 'i-lucide-square-code',
    tooltip: { text: 'Code Block' },
  }, {
    kind: 'horizontalRule',
    icon: 'i-lucide-separator-horizontal',
    tooltip: { text: 'Horizontal Rule' },
  }],
  // Text formatting
  [{
    kind: 'mark',
    mark: 'bold',
    icon: 'i-lucide-bold',
    tooltip: { text: 'Bold' },
  }, {
    kind: 'mark',
    mark: 'italic',
    icon: 'i-lucide-italic',
    tooltip: { text: 'Italic' },
  }, {
    kind: 'mark',
    mark: 'underline',
    icon: 'i-lucide-underline',
    tooltip: { text: 'Underline' },
  }, {
    kind: 'mark',
    mark: 'strike',
    icon: 'i-lucide-strikethrough',
    tooltip: { text: 'Strikethrough' },
  }, {
    kind: 'mark',
    mark: 'code',
    icon: 'i-lucide-code',
    tooltip: { text: 'Code' },
  }],
  // Link
  [{
    kind: 'link',
    icon: 'i-lucide-link',
    tooltip: { text: 'Link' },
  }],
  // Text alignment
  [{
    icon: 'i-lucide-align-justify',
    tooltip: { text: 'Text Align' },
    content: {
      align: 'end',
    },
    items: [{
      kind: 'textAlign',
      align: 'left',
      icon: 'i-lucide-align-left',
      label: 'Align Left',
    }, {
      kind: 'textAlign',
      align: 'center',
      icon: 'i-lucide-align-center',
      label: 'Align Center',
    }, {
      kind: 'textAlign',
      align: 'right',
      icon: 'i-lucide-align-right',
      label: 'Align Right',
    }, {
      kind: 'textAlign',
      align: 'justify',
      icon: 'i-lucide-align-justify',
      label: 'Align Justify',
    }],
  }],
];
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] })]"
    class="flex flex-col gap-4 min-h-37 w-full"
  >
    <PEditorToolbar
      :editor="editor"
      :items="items"
      class="overflow-x-auto sm:px-8"
    />
  </PEditor>
</template>
You can also pass an array of arrays to the items prop to create separated groups of items.
Each item can take an items array of objects with the same properties as the items prop to create a DropdownMenu.

Layout

Use the layout prop to change how the toolbar is displayed. Defaults to fixed.

<script setup lang="ts">
import type { EditorToolbarItem } from 'pohon-ui';

defineProps<{
  layout: 'fixed' | 'bubble' | 'floating';
}>();

const value = ref(`Switch between layouts to see the different toolbar modes.

The **fixed** layout displays the toolbar above the editor. The **bubble** layout shows the toolbar when you select text. The **floating** layout appears on empty lines.`);

const items: Array<Array<EditorToolbarItem>> = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code',
}]];
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="flex flex-col gap-4 min-h-26 w-full"
  >
    <PEditorToolbar
      :key="layout"
      :editor="editor"
      :items="items"
      :layout="layout"
      :data-layout="layout"
      class="data-[layout=fixed]:sm:px-8"
    />
  </PEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorToolbarItem } from 'pohon-ui';

defineProps<{
  layout: 'fixed' | 'bubble' | 'floating';
}>();

const value = ref(`Switch between layouts to see the different toolbar modes.

The **fixed** layout displays the toolbar above the editor. The **bubble** layout shows the toolbar when you select text. The **floating** layout appears on empty lines.`);

const items: Array<Array<EditorToolbarItem>> = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code',
}]];
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="flex flex-col gap-4 min-h-26 w-full"
  >
    <PEditorToolbar
      :key="layout"
      :editor="editor"
      :items="items"
      :layout="layout"
      :data-layout="layout"
      class="data-[layout=fixed]:sm:px-8"
    />
  </PEditor>
</template>

Options

When using bubble or floating layouts, use the options prop to customize the positioning behavior using Floating UI options.

<template>
  <PEditor v-slot="{ editor }">
    <PEditorToolbar
      :editor="editor"
      :items="items"
      layout="bubble"
      :options="{
        placement: 'top',
        offset: 8,
        flip: { padding: 8 },
        shift: { padding: 8 },
      }"
    />
  </PEditor>
</template>

Should Show

When using bubble or floating layouts, use the should-show prop to control when the toolbar appears. This function receives context about the editor state and returns a boolean.

<template>
  <PEditor v-slot="{ editor }">
    <PEditorToolbar
      :editor="editor"
      :items="items"
      layout="bubble"
      :should-show="({ view, state }) => {
        const { selection } = state
        const { from, to } = selection
        const text = state.doc.textBetween(from, to)
        return view.hasFocus() && !selection.empty && text.length > 10
      }"
    />
  </PEditor>
</template>

Examples

With image toolbar

Use the should-show prop to create context-specific toolbars that appear only for certain node types. This example shows a bubble toolbar with download and delete actions that only appears when an image is selected.

<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3';
import type { EditorToolbarItem } from 'pohon-ui';

const value = ref(`Click on the image below to see the image-specific toolbar:

![Image Placeholder](/placeholder.jpeg)`);

function items(editor: Editor): Array<Array<EditorToolbarItem>> {
  const node = editor.state.doc.nodeAt(editor.state.selection.from);

  return [[{
    icon: 'i-lucide-download',
    to: node?.attrs?.src,
    download: true,
    tooltip: { text: 'Download' },
  }], [{
    icon: 'i-lucide-trash',
    tooltip: { text: 'Delete' },
    onClick: () => {
      const { state } = editor;
      const { selection } = state;

      const pos = selection.from;
      const node = state.doc.nodeAt(pos);

      if (node && node.type.name === 'image') {
        editor.chain().focus().deleteRange({ from: pos, to: pos + node.nodeSize }).run();
      }
    },
  }]];
}
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="min-h-113 w-full"
  >
    <PEditorToolbar
      :editor="editor"
      :items="items(editor)"
      layout="bubble"
      :should-show="({ editor, view }) => {
        return editor.isActive('image') && view.hasFocus()
      }"
    />
  </PEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { Editor } from '@tiptap/vue-3';
import type { EditorToolbarItem } from 'pohon-ui';

const value = ref(`Click on the image below to see the image-specific toolbar:

![Image Placeholder](/placeholder.jpeg)`);

function items(editor: Editor): Array<Array<EditorToolbarItem>> {
  const node = editor.state.doc.nodeAt(editor.state.selection.from);

  return [[{
    icon: 'i-lucide-download',
    to: node?.attrs?.src,
    download: true,
    tooltip: { text: 'Download' },
  }], [{
    icon: 'i-lucide-trash',
    tooltip: { text: 'Delete' },
    onClick: () => {
      const { state } = editor;
      const { selection } = state;

      const pos = selection.from;
      const node = state.doc.nodeAt(pos);

      if (node && node.type.name === 'image') {
        editor.chain().focus().deleteRange({ from: pos, to: pos + node.nodeSize }).run();
      }
    },
  }]];
}
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="min-h-113 w-full"
  >
    <PEditorToolbar
      :editor="editor"
      :items="items(editor)"
      layout="bubble"
      :should-show="({ editor, view }) => {
        return editor.isActive('image') && view.hasFocus()
      }"
    />
  </PEditor>
</template>

This example demonstrates how to create a custom link popover using the slot property on toolbar items and the Popover component.

  1. Create a Vue component that wraps a Popover with link editing functionality:
EditorLinkPopover.vue
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3';

const props = defineProps<{
  editor: Editor;
  autoOpen?: boolean;
}>();

const open = ref(false);
const url = ref('');

const active = computed(() => props.editor.isActive('link'));
const disabled = computed(() => {
  if (!props.editor.isEditable) {
    return true;
  }
  const { selection } = props.editor.state;
  return selection.empty && !props.editor.isActive('link');
});

watch(() => props.editor, (editor, _, onCleanup) => {
  if (!editor) {
    return;
  }

  const updateUrl = () => {
    const { href } = editor.getAttributes('link');
    url.value = href || '';
  };

  updateUrl();
  editor.on('selectionUpdate', updateUrl);

  onCleanup(() => {
    editor.off('selectionUpdate', updateUrl);
  });
}, { immediate: true });

watch(active, (isActive) => {
  if (isActive && props.autoOpen) {
    open.value = true;
  }
});

function setLink() {
  if (!url.value) {
    return;
  }

  const { selection } = props.editor.state;
  const isEmpty = selection.empty;
  const hasCode = props.editor.isActive('code');

  let chain = props.editor.chain().focus();

  // When linking code, extend the code mark range first to select the full code
  if (hasCode && !isEmpty) {
    chain = chain.extendMarkRange('code').setLink({ href: url.value });
  } else {
    chain = chain.extendMarkRange('link').setLink({ href: url.value });

    if (isEmpty) {
      chain = chain.insertContent({ type: 'text', text: url.value });
    }
  }

  chain.run();
  open.value = false;
}

function removeLink() {
  props.editor
    .chain()
    .focus()
    .extendMarkRange('link')
    .unsetLink()
    .setMeta('preventAutolink', true)
    .run();

  url.value = '';
  open.value = false;
}

function openLink() {
  if (!url.value) {
    return;
  }
  window.open(url.value, '_blank', 'noopener,noreferrer');
}

function handleKeyDown(event: KeyboardEvent) {
  if (event.key === 'Enter') {
    event.preventDefault();
    setLink();
  }
}
</script>

<template>
  <PPopover
    v-model:open="open"
    :ui="{ content: 'p-0.5' }"
  >
    <PTooltip text="Link">
      <PButton
        icon="i-lucide-link"
        color="neutral"
        active-color="primary"
        variant="ghost"
        active-variant="soft"
        size="sm"
        :active="active"
        :disabled="disabled"
      />
    </PTooltip>

    <template #content>
      <PInput
        v-model="url"
        autofocus
        name="url"
        type="url"
        variant="none"
        placeholder="Paste a link..."
        @keydown="handleKeyDown"
      >
        <div class="mr-0.5 flex items-center">
          <PButton
            icon="i-lucide-corner-down-left"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Apply link"
            @click="setLink"
          />

          <PSeparator
            orientation="vertical"
            class="mx-1 h-6"
          />

          <PButton
            icon="i-lucide-external-link"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Open in new window"
            @click="openLink"
          />

          <PButton
            icon="i-lucide-trash"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Remove link"
            @click="removeLink"
          />
        </div>
      </PInput>
    </template>
  </PPopover>
</template>
EditorLinkPopover.vue
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { Editor } from '@tiptap/vue-3';

const props = defineProps<{
  editor: Editor;
  autoOpen?: boolean;
}>();

const open = ref(false);
const url = ref('');

const active = computed(() => props.editor.isActive('link'));
const disabled = computed(() => {
  if (!props.editor.isEditable) {
    return true;
  }
  const { selection } = props.editor.state;
  return selection.empty && !props.editor.isActive('link');
});

watch(() => props.editor, (editor, _, onCleanup) => {
  if (!editor) {
    return;
  }

  const updateUrl = () => {
    const { href } = editor.getAttributes('link');
    url.value = href || '';
  };

  updateUrl();
  editor.on('selectionUpdate', updateUrl);

  onCleanup(() => {
    editor.off('selectionUpdate', updateUrl);
  });
}, { immediate: true });

watch(active, (isActive) => {
  if (isActive && props.autoOpen) {
    open.value = true;
  }
});

function setLink() {
  if (!url.value) {
    return;
  }

  const { selection } = props.editor.state;
  const isEmpty = selection.empty;
  const hasCode = props.editor.isActive('code');

  let chain = props.editor.chain().focus();

  // When linking code, extend the code mark range first to select the full code
  if (hasCode && !isEmpty) {
    chain = chain.extendMarkRange('code').setLink({ href: url.value });
  } else {
    chain = chain.extendMarkRange('link').setLink({ href: url.value });

    if (isEmpty) {
      chain = chain.insertContent({ type: 'text', text: url.value });
    }
  }

  chain.run();
  open.value = false;
}

function removeLink() {
  props.editor
    .chain()
    .focus()
    .extendMarkRange('link')
    .unsetLink()
    .setMeta('preventAutolink', true)
    .run();

  url.value = '';
  open.value = false;
}

function openLink() {
  if (!url.value) {
    return;
  }
  window.open(url.value, '_blank', 'noopener,noreferrer');
}

function handleKeyDown(event: KeyboardEvent) {
  if (event.key === 'Enter') {
    event.preventDefault();
    setLink();
  }
}
</script>

<template>
  <PPopover
    v-model:open="open"
    :ui="{ content: 'p-0.5' }"
  >
    <PTooltip text="Link">
      <PButton
        icon="i-lucide-link"
        color="neutral"
        active-color="primary"
        variant="ghost"
        active-variant="soft"
        size="sm"
        :active="active"
        :disabled="disabled"
      />
    </PTooltip>

    <template #content>
      <PInput
        v-model="url"
        autofocus
        name="url"
        type="url"
        variant="none"
        placeholder="Paste a link..."
        @keydown="handleKeyDown"
      >
        <div class="mr-0.5 flex items-center">
          <PButton
            icon="i-lucide-corner-down-left"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Apply link"
            @click="setLink"
          />

          <PSeparator
            orientation="vertical"
            class="mx-1 h-6"
          />

          <PButton
            icon="i-lucide-external-link"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Open in new window"
            @click="openLink"
          />

          <PButton
            icon="i-lucide-trash"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Remove link"
            @click="removeLink"
          />
        </div>
      </PInput>
    </template>
  </PPopover>
</template>
  1. Use the custom component in the toolbar with a named slot:
<script setup lang="ts">
import type { EditorToolbarItem } from 'pohon-ui';
import EditorLinkPopover from './EditorLinkPopover.vue';

const value = ref(`Select text and click the link button to add a link with the custom popover.

You can also edit existing links like [this one](https://pohon.vinicunca.dev).`);

const toolbarItems = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
}, {
  slot: 'link' as const,
}]] satisfies Array<Array<EditorToolbarItem>>;
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="flex flex-col gap-4 min-h-30 w-full"
  >
    <PEditorToolbar
      :editor="editor"
      :items="toolbarItems"
      class="sm:px-8"
    >
      <template #link>
        <EditorLinkPopover
          :editor="editor"
          auto-open
        />
      </template>
    </PEditorToolbar>
  </PEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorToolbarItem } from 'pohon-ui';
import EditorLinkPopover from './EditorLinkPopover.vue';

const value = ref(`Select text and click the link button to add a link with the custom popover.

You can also edit existing links like [this one](https://pohon.vinicunca.dev).`);

const toolbarItems = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
}, {
  slot: 'link' as const,
}]] satisfies Array<Array<EditorToolbarItem>>;
</script>

<template>
  <PEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="flex flex-col gap-4 min-h-30 w-full"
  >
    <PEditorToolbar
      :editor="editor"
      :items="toolbarItems"
      class="sm:px-8"
    >
      <template #link>
        <EditorLinkPopover
          :editor="editor"
          auto-open
        />
      </template>
    </PEditorToolbar>
  </PEditor>
</template>

API

Props

Prop Default Type

Slots

Slot Type

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    editorToolbar: {
      slots: {
        root: 'focus:outline-none',
        base: 'flex items-stretch gap-1.5',
        group: 'flex items-center gap-0.5',
        separator: 'w-px self-stretch bg-border'
      },
      variants: {
        layout: {
          bubble: {
            base: 'bg-background border border-border rounded-lg p-1'
          },
          floating: {
            base: 'bg-background border border-border rounded-lg p-1'
          },
          fixed: {
            base: ''
          }
        }
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        editorToolbar: {
          slots: {
            root: 'focus:outline-none',
            base: 'flex items-stretch gap-1.5',
            group: 'flex items-center gap-0.5',
            separator: 'w-px self-stretch bg-border'
          },
          variants: {
            layout: {
              bubble: {
                base: 'bg-background border border-border rounded-lg p-1'
              },
              floating: {
                base: 'bg-background border border-border rounded-lg p-1'
              },
              fixed: {
                base: ''
              }
            }
          }
        }
      }
    })
  ]
})

Changelog

No recent changes