Xây dựng website bán hàng bằng Laravel - Tạo trình soạn thảo WYSIWYG với tiptap

Ngày đăng
Tác giả
Tran Thanh Long

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.


E-commerce với Laravel là loạt bài ghi lại toàn bộ quá trình xây dựng hệ thống thương mại điện tử được thực hiện bởi Transmoni team nhắm hướng dẫn mọi người làm quen với Laravel, Livewire, Alpine.js và Tailwind CSS. Hiện dự án đang được cập nhật liên tục, toàn bộ mã nguồn của dự án sẽ được công khai miễn phí theo hình thức mã nguồn mở trên trang Github của Transmoni sau khi hoàn tất loạt bài hướng dẫn này.

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:

Trình soạn thảo WYSIWYG với Tiptap

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.

Trình soạn thảo WYSIWYG với Tiptap

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!

Thông báo biến động số dư qua API & Webhook.

Hỗ trợ tích hợp giao dịch chuyển khoản vào hệ thống thanh toán trực tuyến Tự động, Nhanh chóng, Chính xác.

Đăng ký dùng thử trong 14 ngày
Bài viết mới nhất

Xây dựng website bán hàng bằng Laravel - Dựng trang thông tin Sản phẩm

Để Khách hàng có thể xem và tìm hiểu kỹ hơn về thông tin của một Sản phẩm bao gồm giá bán, số lượng tồn kho hay các mô tả về những đặc tính kỹ thuật ví dụ chất liệu, kiểu dáng... chúng ta sẽ cần đến một trang riêng cho mỗi sản phẩm, hãy cùng tìm hiểu cách để tạo một trang như vậy trong bài viết này.

Xây dựng website bán hàng bằng Laravel - Thiết lập và quản lý đơn hàng

Quản lý đơn hàng là quy trình back-end để quản lý và kiện toàn các đơn hàng trực tuyến. Việc này bao gồm mọi thứ từ định tuyến đơn hàng, in nhãn vận chuyển cho đến đến trả hàng cho khách. Trong bài viết của ngày hôm nay chúng ta sẽ cùng tìm hiểu cách để tạo và quản lý các đơn đặt hàng.