-
-
Save MrMooky/e6c0f91eea9013aa37077b1d80a1f20e to your computer and use it in GitHub Desktop.
Alpine tiptap editor + livewire
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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