0

Tất cả "Input" đi ra, "contenteditable" ở lại

Tôi đã hiểu vì sao <input> không đủ cho web hiện đại (và đó là lúc contenteditable xuất hiện)


1. Một requirement nhỏ đã phá vỡ suy nghĩ của mình

Có một thời gian mình nghĩ:

“Làm 3 cái lăng nhăng nhập text thì cứ dùng <input> hoặc <textarea> thôi.”

Và thật ra phần lớn tutorial frontend trên mạng cũng khiến mình nghĩ như vậy.

Ban đầu mọi thứ đúng là rất đơn giản:

  • user nhập text
  • Enter để gửi
  • lưu value vào state
  • gọi API

Done.


Cho đến một ngày thằng bạn mình được giao một task (thật ra không có thằng bạn nào cả) - làm một ô chat kiểu Slack/Messenger.

Ban đầu requirement vẫn rất bình thường:

  • nhập message
  • gửi message
  • auto resize textarea

Nhưng rồi requirement bắt đầu tăng dần.

Khách hàng muốn:

  • @ để mention user
  • highlight hashtag
  • paste image trực tiếp
  • render emoji inline
  • bold một đoạn text
  • support markdown preview
  • sau này có thể thêm attachment inline

Lúc đó mình bắt đầu nhận ra:

“Khoan… hình như <input> không sinh ra để làm những thứ này.”


2. <input> thật ra chỉ là một string

Ví dụ:

<input />
<textarea></textarea>

Dù UI có fancy tới đâu thì bên trong nó vẫn chỉ là:

"hello world"

Một string thuần.

Điều đó dẫn đến một giới hạn rất lớn:

Bạn không thể:

  • bôi đậm riêng một đoạn text
  • style nhiều đoạn khác nhau trong cùng một dòng
  • chèn image giữa text
  • render inline component
  • wrap text bằng HTML
  • control selection ở mức DOM

Ví dụ bạn muốn:

hello <strong>world</strong>

Thì với <input>… gần như không có khái niệm đó.

Vì browser chỉ hiểu:

"value"

chứ không hiểu:

  • node
  • element
  • selection range
  • nested structure

Lúc này mình mới hiểu:

<input> được thiết kế cho plain text, không phải rich text.

Và đây là lý do các editor hiện đại gần như không dùng <input> nữa.


3. Vậy browser giải quyết rich text như thế nào?

Đây là lúc mình gặp một thứ khá “ma thuật”:

<div contenteditable="true"></div>

Chỉ một attribute.

Nhưng browser lập tức biến cả DOM thành editable surface.

Bạn có thể:

  • click để nhập text
  • select text
  • copy/paste
  • xuống dòng
  • drag selection
  • undo/redo cơ bản

Tất cả gần như browser làm sẵn.

Lúc đầu mình khá bất ngờ.

Kiểu:

“Ủa… browser có sẵn editor engine luôn à?”

Và câu trả lời là: gần như vậy.


4. contenteditable: khi cả DOM trở thành editor

Điểm khác biệt lớn nhất là:

Với <input>

Editor chỉ là một string:

"hello world"

Với contenteditable

Editor trở thành DOM.

Ví dụ:

<div contenteditable="true">
  hello <strong>world</strong>
</div>

Lúc này browser phải quản lý:

  • text node
  • nested HTML
  • caret position
  • selection range
  • DOM mutation

Đây là lúc mình bắt đầu hiểu:

Rich text editor thật ra là bài toán thao tác DOM realtime.


Ví dụ khi user bôi đen chữ world.

Browser phải biết:

  • selection bắt đầu ở node nào
  • kết thúc ở đâu
  • offset bao nhiêu
  • có nằm trong tag khác không
  • có nested element không

Nghe thì nhỏ.

Nhưng thật ra cực kỳ phức tạp.


5. Tự viết mini editor đầu tiên

Mình thử làm editor đơn giản nhất có thể.

<div id="editor" contenteditable="true">
  hello world
</div>

<button id="bold">Bold</button>
bold.onclick = () => {
  document.execCommand("bold");
};

Sau đó mình:

  1. bôi đen text
  2. click “Bold”

Và browser tự động biến:

hello world

thành:

hello <strong>world</strong>

Khoảnh khắc đó khá thú vị.

Mình kiểu:

“Wait… browser đang tự mutate HTML luôn?”


Mặc dù execCommand giờ đã deprecated.

Nhưng nó là cách rất tốt để hiểu browser editing hoạt động như thế nào.


6. Khoảnh khắc mình hiểu editor khó đến mức nào

Lúc đầu mình nghĩ:

“Dễ vãi nồi, chỉ là editable div thôi. Giờ làm cái CKEditor rồi bán thì bao bú.”

Nhưng càng làm càng thấy editor là hố sâu.


Case 1: Paste từ Word

Bạn paste text từ Microsoft Word.

Boom.

HTML rác xuất hiện everywhere:

  • inline style
  • font tag
  • span lồng nhau
  • weird metadata

Case 2: Cursor jump

Trong React:

  • user đang gõ
  • component re-render
  • caret nhảy mất

Tự nhiên editor unusable.


Case 3: IME

Tiếng Việt, tiếng Nhật, tiếng Trung…

Input method editor khiến:

  • composition event
  • beforeinput
  • selection sync

trở nên cực kỳ đau đầu.


Case 4: Nested formatting

Ví dụ:

<strong>
  hello <em>world</em>
</strong>

Giờ user chỉ un-bold chữ world.

Bạn sẽ xử lý DOM kiểu gì?

Đây là lúc mình hiểu:

Rich text editor không còn là “input nâng cấp”.

Nó gần giống một mini document engine.


7. Vì sao CKEditor, Slate hay Notion editor tồn tại

Trước đây mình từng nghĩ:

“Sao editor library nặng thế?”

Sau khi tự thử build một editor mini…

Mình bắt đầu thấy:

  • CKEditor
  • Slate
  • ProseMirror
  • DraftJS
  • Lexical

thật ra đang giải quyết những vấn đề rất khó.

Không chỉ là:

  • nhập text
  • bold text

Mà còn:

  • normalize document
  • mapping selection
  • collaborative editing
  • history stack
  • schema validation
  • serialization
  • plugin system

Ví dụ Notion editor nhìn rất đơn giản.

Nhưng phía dưới là:

  • block model
  • selection engine
  • keyboard shortcut system
  • async state sync
  • drag/drop structure
  • realtime collaboration

Đó gần như là một document operating system chạy trong browser.


8. Kết luận

Điều thú vị nhất sau khi tự build một editor mini không phải là:

“Mình clone được CKEditor.”

Mà là:

“Mình bắt đầu hiểu browser hoạt động như thế nào.”

Chỉ với:

contenteditable="true"

browser đã expose ra:

  • editing engine
  • selection system
  • caret management
  • DOM mutation capability

Và từ đó cả một thế giới editor được xây dựng phía trên:

  • Google Docs
  • Notion
  • Slack
  • CKEditor
  • Medium editor

Đôi khi chỉ cần thử tự build một thứ nhỏ như editor…

là mình hiểu vì sao frontend hiện đại lại phức tạp đến vậy.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí