Skip to content

Instantly share code, notes, and snippets.

@robertdrakedennis
Created December 16, 2021 05:14
Show Gist options
  • Save robertdrakedennis/c1d22f2aa25cd123b17a618f0c1db573 to your computer and use it in GitHub Desktop.
Save robertdrakedennis/c1d22f2aa25cd123b17a618f0c1db573 to your computer and use it in GitHub Desktop.
Alpine tiptap editor + livewire
<div x-data="setupEditor(@entangle($attributes->wire('model')).defer)" x-init="() => init($refs.editor)" wire:ignore
{{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'editor !w-full !max-w-full']) }}>
<template x-if="editor">
<div class="flex space-x-4 items-center dark:text-neutral-100 fill-current py-2">
<button @click.prevent="Alpine.raw(editor).chain().toggleBold().focus().run()">
<x-icon-bold class="w-4 h-4" />
</button>
<button @click.prevent="Alpine.raw(editor).chain().toggleItalic().focus().run()">
<x-icon-italic class="w-4 h-4" />
</button>
<button @click.prevent="showLinkPrompt(Alpine.raw(editor), Alpine.raw(editor).getAttributes('link').href)">
<x-icon-link class="w-4 h-4" />
</button>
<button @click.prevent="showImagePrompt(Alpine.raw(editor))">
<x-icon-photo class="w-4 h-4" />
</button>
<x-dropdown triggerClasses="relative inline-flex" align="right" width="48">
<x-slot name="trigger">
<button type="button" class="inline-flex w-4 h-4 m-auto">
<x-icon-h1 class="w-4 h-4" />
</button>
</x-slot>
<x-slot name="content">
<div class="space-x-2">
<button @click.prevent="Alpine.raw(editor).chain().toggleHeading({ level: 1 }).focus().run()">
<x-icon-h1 class="w-4 h-4" />
</button>
<button @click.prevent="Alpine.raw(editor).chain().toggleHeading({ level: 2 }).focus().run()">
<x-icon-h2 class="w-4 h-4" />
</button>
<button @click.prevent="Alpine.raw(editor).chain().toggleHeading({ level: 3 }).focus().run()">
<x-icon-h3 class="w-4 h-4" />
</button>
</div>
</x-slot>
</x-dropdown>
<x-dropdown triggerClasses="relative inline-flex" align="right">
<x-slot name="trigger">
<button type="button" class="inline-flex w-4 h-4 m-auto">
<x-icon-align-left class="w-4 h-4" />
</button>
</x-slot>
<x-slot name="content">
<div class="space-x-2">
<button @click.prevent="Alpine.raw(editor).chain().setTextAlign('left').focus().run()">
<x-icon-align-left class="w-4 h-4" />
</button>
<button @click.prevent="Alpine.raw(editor).chain().setTextAlign('center').focus().run()">
<x-icon-align-center class="w-4 h-4" />
</button>
<button @click.prevent="Alpine.raw(editor).chain().setTextAlign('right').focus().run()">
<x-icon-align-right class="w-4 h-4" />
</button>
</div>
</x-slot>
</x-dropdown>
<input
class="bg-transparent border-transparent shadow-none w-6"
type="color"
x-on:input="event => Alpine.raw(editor).chain().focus().setColor(event.target.value).run()"
:value="Alpine.raw(editor).getAttributes('textStyle').color"
/>
<button x-data="{ tooltip: 'Reset selected text color' }" x-tooltip="tooltip" @click.prevent="Alpine.raw(editor).chain().focus().unsetColor().run()">
<x-iconic-eye-off class="w-4 h-4" />
</button>
<button class="hidden md:block" @click.prevent="Alpine.raw(editor).chain().toggleCode().focus().run()">
<x-icon-code class="w-4 h-4" />
</button>
<button class="hidden md:block" @click.prevent="Alpine.raw(editor).chain().toggleOrderedList().focus().run()">
<x-icon-ol class="w-4 h-4" />
</button>
<button class="hidden md:block" @click.prevent="Alpine.raw(editor).chain().toggleBulletList().focus().run()">
<x-icon-ul class="w-4 h-4" />
</button>
<button class="hidden md:block" @click.prevent="Alpine.raw(editor).chain().toggleUndo().focus().run()">
<x-icon-undo class="w-4 h-4" />
</button>
<button class="hidden md:block" @click.prevent="Alpine.raw(editor).chain().toggleRedo().focus().run()">
<x-icon-redo class="w-4 h-4" />
</button>
</div>
</template>
<div x-ref="editor"></div>
</div>
@once
@push('scripts')
<script defer type="module">
import {
Editor,
mergeAttributes
} from 'https://cdn.skypack.dev/@tiptap/core?min';
import StarterKit from 'https://cdn.skypack.dev/@tiptap/starter-kit?min';
import Link from 'https://cdn.skypack.dev/@tiptap/extension-link?min';
import Image from 'https://cdn.skypack.dev/@tiptap/extension-image?min';
import TextAlign from 'https://cdn.skypack.dev/@tiptap/extension-text-align?min';
import Color from 'https://cdn.skypack.dev/@tiptap/extension-color?min';
import TextStyle from 'https://cdn.skypack.dev/@tiptap/extension-text-style?min';
window.setupEditor = function(content) {
return {
editor: null,
content: content,
updatedAt: Date.now(),
init(element) {
this.editor = new Editor({
element: element,
extensions: [
StarterKit,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Link.configure({
openOnClick: false,
}).extend({
addAttributes() {
// Return an object with attribute configuration
return {
...this.parent?.(),
rel: {
default: 'noopener noreferrer nofollow',
},
}
},
}),
Image,
Color,
TextStyle
],
editorProps: {
attributes: {
class: "min-h-[8rem] max-h-[100vh] pt-2 prose prose-dark overflow-y-auto focus:outline-none !w-full !max-w-full dark:prose-light resize-y"
}
},
content: this.content === "" ? '' : JSON.parse(this.content),
onUpdate: ({ editor }) => {
this.content = editor.getJSON();
},
onBlur: ({ editor }) => {
Livewire.emit('editorUpdated');
},
onSelectionUpdate: () => {
this.updatedAt = Date.now()
},
})
},
}
}
window.showLinkPrompt = function(editor, link) {
const src = prompt('Enter the url of your link here', link)
if (src == null || src === '') {
Alpine.raw(editor).chain().extendMarkRange('link').unsetLink().focus().run()
} else {
Alpine.raw(editor).chain().extendMarkRange('link').setLink({
href: src
}).focus().run()
}
}
window.showImagePrompt = function(editor) {
const src = prompt('Enter the url of your image here')
if (src !== null && src !== '') {
Alpine.raw(editor).chain().setImage({
src: src
}).focus().run()
}
}
</script>
@endpush
@endonce
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment