← Quay lại
11 tháng 01, 2026 6 phút đọc

Thêm nút định dạng code vào Trix editor

Mình viết blog này bằng Rails nên dùng luôn trình soạn thảo mặc định của nó là Action Text (AT) để viết bài. AT tới lượt nó lại dùng Trix, editor viết bằng JavaScript (JS) cây nhà lá vườn của công ty 37signals. Thanh toolbar mặc định gồm những nút chức năng như sau:

Toolbar mặc định của Trix


Nhóm 4 nút bên trái để định dạng nội dung được chọn. 7 nút tiếp theo định dạng khối văn bản. Nút hình kẹp để thêm file. 2 nút mũi tên nằm cuối để hoàn tác qua lại. Nhưng lại thiếu mất một chức năng mình cần đó là định dạng code trên dòng. Vì là blog kĩ thuật hay viết code từa lưa mà thiếu nó thì khá cùi. Vậy nên hôm nay mình quyết định thêm chức năng này và sẵn tiện bỏ luôn 2 nút thụt lề trong nhóm thứ 2. Kết quả cuối cùng sẽ như này:

Thanh toolbar mới


Để ý nút thứ 3 hình chữ C bên trong hình vuông, chính nó là đối tượng của bài viết hôm nay đấy.

Mình cần liệt kê các tech stack đang dùng để tiện các bạn theo dõi: Rails 8.1, Tailwind 4, Action Text + Trix editor và Stimulus.

Bản thân AT không hỗ trợ custom toolbar sẵn nên mình phải dùng JS để hack phá nó. Muốn vậy mình phải có một controller kết nối với DOM. Tạo sườn với hàm custom():

// app/javascript/controllers/trix_custom_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  custom() {}
}

Và kết nối nó tới view:

<div data-controller="trix-custom">
  <%= form.rich_text_area :content %>
</div>

Hàm custom được dùng để điều chỉnh toolbar theo ý mình. Giờ mình cần phải bắt được sự kiện khi Trix khởi tạo để có thể sửa nó trước khi render toolbar. Để làm được điều này, trước tiên mình phải đọc tài liệu!

Và event đó là trix-before-initialize. Mình nhắc qua về cách bắt sự kiện bằng Stimulus, thêm một data attribute vào DOM phát sự kiện (ở đây là form.rich_text_area cung cấp bởi AT) với cú pháp data-action="event->controller#function". Áp dụng:

<%= form.rich_text_area :content, data: { action: "trix-before-initialize->trix-custom#custom" } %>

Giờ là phần chính. Thêm nút với chức năng vào toolbar như thế nào? Trong tài liệu có đoạn:

To change the toolbar without modifying Trix, you can overwrite the Trix.config.toolbar.getDefaultHTML() function. The default toolbar HTML is in config/toolbar.js. Trix uses data attributes to determine how to respond to a toolbar button click.

Có 2 manh mối:
  • Thay đổi toolbar bằng cách viết đè getDefaultHTML() với nội dung nằm trong file config/toolbar.js
  • Trix dùng data attribute để xác định cách xử lý một nút nhấn

Vào file toolbar.js sẽ thấy HTML mặc định, copy về và viết đè hàm trong controller:

custom() {
  const lang = Trix.config.lang;

  Trix.config.toolbar.getDefaultHTML = function () {
    return `<div class="trix-button-row">
      <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${lang.bold}" tabindex="-1">${lang.bold}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${lang.italic}" tabindex="-1">${lang.italic}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-inline-code" data-trix-attribute="inlineCode" data-trix-key="shift+c" title="Inline code" tabindex="-1">Inline code</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${lang.strike}" tabindex="-1">${lang.strike}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${lang.link}" tabindex="-1">${lang.link}</button>
      </span>
      // Nội dung còn lại trong file và đã bỏ đi 2 nút thụt lề
      // ...
    </div>`;
  };
}

Vì template mặc định dùng lang nên mình phải tạo một biến lang tương ứng. Để ý dòng button thứ 3. Đây là nút mình mới thêm. Có 2 data attribute cần phải sửa lại đó là: 
  • data-trix-attribute với giá trị là inlineCode. Ta cần bảo Trix làm gì khi nút có data được nhấn và inlineCode là cách để xác định.
  • data-trix-key với shift+c. Giá trị này để cài đặt phím tắt meta + shift + C.

Tiếp theo, làm thế nào để Trix biết nên làm gì khi nhấn vào nút này? Tài liệu chỉ ra rằng:

Trix will determine that a range of text is selected and will apply the formatting defined in Trix.config.textAttributes (found in config/text_attributes.js).

Xem thử file text_attributes.js thì thấy Trix định nghĩa các format (ở đây là bold) theo cấu trúc như sau:

bold: {
  tagName: "strong",
  inheritable: true,
  parser(element) {
    const style = window.getComputedStyle(element)
    return style.fontWeight === "bold" || style.fontWeight >= 600
  },
}

Ý nghĩa của các thuộc tính trên và áp dụng nó vào trường hợp của mình:
  • bold là tên định dạng, nó liên kết với data-trix-attribute được định nghĩa ở trên là inlineCode
  • tagName là tên tag dùng để bọc đoạn cần format. Mình cần tag <code>.
  • inheritable để xác định xem khi đang áp dụng format thì nội dung viết tiếp vẫn còn giữ format đó không, mình cứ để true giống với bold.
  • parser dùng để đọc nội dung hiện tại để biết nó có đang được định dạng không bằng cách kiểm tra CSS. Mình không cần.

Vậy giờ chỉ việc thêm config mới cho inlineCode tương tự bold ở trên thôi.

custom() {
  Trix.config.textAttributes.inlineCode = {
    tagName: "code",
    inheritable: true,
  };
  // Code vừa nãy
}

Về mặt chức năng thì đã ổn rồi, nhưng cần cho nút bấm một icon đại diện. Mình nghĩ tới một chữ C đặt trong khung vẽ bằng SVG. Trix dùng CSS để hiện icon này. Bạn có thể xem các icon mặc định trong file app/assets/stylesheets/actiontext.css:

trix-toolbar .trix-button--icon-inline-code::before {
  background-image: url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2048%2048%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22%23000000%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223.3599999999999994%22%3E%3Cg%20id%3D%22SVGRepo_bgCarrier%22%20stroke-width%3D%220%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_tracerCarrier%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_iconCarrier%22%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill%3Anone%3Bstroke%3A%23000000%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Around%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22a%22%20d%3D%22M40.5%2C5.5H7.5a2%2C2%2C0%2C0%2C0-2%2C2v33a2%2C2%2C0%2C0%2C0%2C2%2C2h33a2%2C2%2C0%2C0%2C0%2C2-2V7.5A2%2C2%2C0%2C0%2C0%2C40.5%2C5.5Z%22%3E%3C%2Fpath%3E%3Cpath%20class%3D%22a%22%20d%3D%22M29.1945%2C28.567a5.5585%2C5.5585%2C0%2C0%2C1-4.8284%2C2.8007h0a5.5606%2C5.5606%2C0%2C0%2C1-5.5606-5.56V22.1928a5.5606%2C5.5606%2C0%2C0%2C1%2C5.5606-5.56h0a5.5583%2C5.5583%2C0%2C0%2C1%2C4.8228%2C2.791%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}

Và đây là thành quả, quá đã:

Editor lúc mình đang viết bài này


Tổng kết lại. Để thêm một nút vào toolbar của Trix cần làm 2 điều:
  1. Custom HTML mặc định của function getDefaultHTML() để thêm nút cần với data-trix-attribute="[tên của attribute]"
  2. Thêm [tên của attribute] ở trên vào textAttributes

Không chỉ vậy, còn có thể thay đổi được nhóm nút thứ 2 áp dụng cho một khối văn bản nữa. Mình đang nghĩ tới việc thêm H2, H3. Để thử xem sao.

Chúc các bạn khám phá thành công nhé!
Cảm ơn bạn đã đọc bài viết này.
16