Trình soạn thảo WYSIWYG là một công cụ cực kỳ hữu ích cung cấp cho người sử dụng khả năng chỉnh sửa và cập nhật nội dung trên website của họ mà không cần làm phiền đến các lập trình viên. Trong trình soạn thảo WYSIWYG, nội dung đã chỉnh sửa dù là văn bản hay đồ họa, sẽ xuất hiện ở dạng gần giống với sản phẩm cuối cùng. Trong bài viết này tôi sẽ hướng dẫn bạn tạo dựng một trình soạn thảo wysiwyg dưới dạng blade component sử dụng thư viện Tiptap.
Nhắc lại bài trước sau khi chúng ta hoàn tất việc xây dựng tính năng quản lý thông tin sản phẩm. Bạn có thể dễ dàng nhận thấy tại phần mô tả thông tin sản phẩm của chúng ta hiện tại đang là một textarea thông thường. Mặc dù với các website bán hàng không đòi hỏi nhiều về phần mô tả này thì một textarea là có thể đáp ứng đủ nhu cầu đưa ra. Tuy nhiên với các website bán hàng lớn thì việc mô ta chi tiết thông tin sản phẩm, kèm theo với các bố cục nổi bật và tách biệt nhau thì HTML là ngôn ngữ không thể thiếu. Và hầu hết người dùng của chúng ta là những người trực tiếp nhập liệu thì không phải ai cũng biết đến HTML, đó chính là lý do cần đến một trình soạn thảo WYSIWYG.
Bạn hoàn toàn có thể nhập HTML vào textarea sẵn có mà chúng ta đã tạo, tuy nhiên sẽ rất khó để mường tượng được ra những gì sẽ được thể hiện trên trang thông tin sản phẩm của chúng ta, và cũng rất mất thời gian khi chỉnh sửa thông tin này.
Tiptap editor là một thư viện hỗ trợ xây dựng trình soạn thảo WYSIWYG cung cấp cho bạn toàn quyền kiểm soát mọi khía cạnh trong trải nghiệm soạn thảo văn bản. Nó có thể tùy chỉnh, với rất nhiều tiện ích mở rộng, và là mã nguồn mở có tài liệu hướng dẫn phong phú.
Ngoài ra Tiptap còn là một headless editor, có nghĩa là nó không đi kèm với bất kỳ một giao diện sẵn có nào cả, do đó bạn có toàn quyền kiểm soát cách thức nó thể hiện.
Trong dự án này chúng ta sử dụng Tailwind CSS và ngoài việc là một css framework thì họ còn cung cấp cho chúng ta một thư viện Typography giúp cho việc thể hiện văn bản một cách đơn giản và rất đẹp. Việc kết hợp giữa Tiptap và Tailwind CSS hết sức dễ dàng, hãy cùng đọc và làm theo nhé!
Trước hết hãy cài đặt thư viện Tiptap editor bằng cách sử dụng lệnh npm install
:
npm install @tiptap/core @tiptap/starter-kit @tailwindcss/typography
Trên đây tôi tiến hành cài đặt bộ lõi của Tiptap và StarterKit có chứa tất cả các tiện ích mở rộng phổ biến nhất, ngoài ra thì tôi cũng cài đặt luôn thư viện Typography của Taiwind CSS.
Sau khi hoàn tất hãy mở file tailwind.config.js
và thêm thư viện Typography vào phần plugins như sau:
module.exports = {
...
plugins: [
require('@tailwindcss/typography'),
],
};
Tiếp đến chúng ta bắt đầu tạo một file javascript mới trong resources/js
và đặt tên nó là tiptap.js
với nội dung như sau:
import {Editor} from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
document.addEventListener('alpine:init', () => {
Alpine.data('setupEditor', (content) => {
let editor;
return {
editor: null,
content: content,
updatedAt: Date.now(),
isActive(type, opts = {}) {
return editor.isActive(type, opts);
},
setParagraph() {
editor.chain().focus().setParagraph().run();
},
toggleBold() {
editor.chain().toggleBold().focus().run();
},
toggleItalic() {
editor.chain().focus().toggleItalic().run();
},
init(element) {
editor = new Editor({
element: element,
content: this.content,
extensions: [
StarterKit,
],
editorProps: {
attributes: {
class: 'focus:outline-none',
},
},
onUpdate: ({editor}) => {
this.content = editor.getHTML()
},
onTransaction: () => {
this.updatedAt = Date.now()
},
onSelectionUpdate: ({ editor}) => {
this.updatedAt = Date.now()
},
});
},
};
});
});
Và đừng quên khai báo trong resources/js/app.js
của chúng ta nhé!
require('./bootstrap');
require('./tiptap');
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
Và tạo một blade component mới tên là tiptap.blade.php
trong resources/views/components
với nội dung như sau:
<div
x-data="setupEditor(@entangle($attributes->wire('model')).defer)"
x-init="() => init($refs.element)"
wire:ignore
{{ $attributes->whereDoesntStartWith('wire:model') }}
>
<div class="pb-5 space-y-1 border-b">
{{--Paragraph--}}
<button
type="button"
@click="setParagraph()"
class="inline-flex items-center p-2 rounded-md border"
:class="{ 'bg-indigo-500 text-white border-transparent hover:bg-indigo-600': isActive('paragraph', updatedAt), 'hover:bg-gray-100': !isActive('paragraph', updatedAt) }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
class="h-3 w-3"
aria-hidden="true"
>
<path
fill="currentColor"
d="M448 63.1C448 81.67 433.7 96 416 96H384v352c0 17.67-14.33 32-31.1 32S320 465.7 320 448V96h-32v352c0 17.67-14.33 32-31.1 32S224 465.7 224 448v-96H198.9c-83.57 0-158.2-61.11-166.1-144.3C23.66 112.3 98.44 32 191.1 32h224C433.7 32 448 46.33 448 63.1z"
/>
</svg>
<span class="sr-only">paragraph</span>
</button>
{{--Bold--}}
<button
type="button"
@click="toggleBold()"
class="inline-flex items-center p-2 rounded-md border"
:class="{ 'bg-indigo-500 text-white border-transparent hover:bg-indigo-600': isActive('bold', updatedAt), 'hover:bg-gray-100': !isActive('bold', updatedAt) }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
class="w-3 h-3"
aria-hidden="true"
>
<path
fill="currentColor"
d="M321.1 242.4C340.1 220.1 352 191.6 352 160c0-70.59-57.42-128-128-128L32 32.01c-17.67 0-32 14.31-32 32s14.33 32 32 32h16v320H32c-17.67 0-32 14.31-32 32s14.33 32 32 32h224c70.58 0 128-57.41 128-128C384 305.3 358.6 264.8 321.1 242.4zM112 96.01H224c35.3 0 64 28.72 64 64s-28.7 64-64 64H112V96.01zM256 416H112v-128H256c35.3 0 64 28.71 64 63.1S291.3 416 256 416z"
/>
</svg>
<span class="sr-only">bold</span>
</button>
{{--Italic--}}
<button
type="button"
@click="toggleItalic()"
class="inline-flex items-center p-2 rounded-md border"
:class="{ 'bg-indigo-500 text-white border-transparent hover:bg-indigo-600': isActive('italic', updatedAt), 'hover:bg-gray-100': !isActive('italic', updatedAt) }"
>
<svg
class="w-3 h-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
aria-hidden="true"
>
<path
fill="currentColor"
d="M384 64.01c0 17.69-14.31 32-32 32h-58.67l-133.3 320H224c17.69 0 32 14.31 32 32s-14.31 32-32 32H32c-17.69 0-32-14.31-32-32s14.31-32 32-32h58.67l133.3-320H160c-17.69 0-32-14.31-32-32s14.31-32 32-32h192C369.7 32.01 384 46.33 384 64.01z"
/>
</svg>
<span class="sr-only">italic</span>
</button>
</div>
<div
x-ref="element"
class="overflow-y-scroll prose sm:prose-sm md:prose-base max-h-[500px] px-1"
></div>
</div>
Ok, vậy là trình soạn thảo của chúng ta đã tạm sẵn sàng, mặc dù mới chỉ có 3 nút bấm điều chỉnh cơ bản là Văn bản, In đậm và In nghiêng nhưng thử sử dụng xem thế nào nhỉ? Tại phần view của component ProductInformationForm chúng ta tiến hành thay thế textarea component bằng tiptap component vừa được tạo:
<div>
<x-label for="description" value="{{ __('Description') }}" />
<div class="mt-1 border rounded-md p-4 shadow-sm">
<x-tiptap wire:model.defer="product.description" />
</div>
<x-input-error for="product.description" class="mt-2" />
</div>
Tiếp tục truy cập vào một sản phẩm bất kỳ để trải nghiệm trình soạn thảo mới của chúng ta:
Ra được như vậy là ngon rồi đấy, tiếp đến hãy quay trở lại Tiptap component để thêm các nút bấm tùy chỉnh phù hợp với nhu cầu của bạn nhé! Với tôi thì một tập hợp các nút bấm như hình dưới là đủ, tôi sẽ không đăng toàn bộ mã nguồn lên đây vì nó sẽ rất dài.
Bạn có thể truy cập vào https://tiptap.dev/introduction để tìm hiểu thêm về các gói mở rộng cũng như việc tùy chỉnh Tiptap editor. Hy vọng bài viết ngắn này sẽ giúp bạn có thể tạo được một trình soạn thảo văn bản WYSIWYG ưng ý, hẹn gặp lại các bạn ở các bài tiếp theo!