Trong quá trình phát triển ứng dụng trên HarmonyOS bằng ArkWeb, việc theo dõi và kiểm soát các thao tác của người dùng trên giao diện Web là một yêu cầu kỹ thuật phổ biến. Thay vì chủ động đọc hoặc ghi dữ liệu, lập trình viên thường cần cơ chế lắng nghe (event listener) để bắt các hành động sao chép, cắt hoặc dán. Điều này cho phép hệ thống can thiệp, biến đổi dữ liệu đầu vào/đầu ra hoặc ghi lại nhật ký tương tác thông qua Clipboard Events.
Các sự kiện Clipboard tiêu chuẩn
Theo tiêu chuẩn của trình duyệt web, có ba sự kiện chính liên quan đến bảng tạm (clipboard):
| Tên sự kiện | Thời điểm kích hoạt | Khả năng can thiệp |
|---|---|---|
copy |
Người dùng thực hiện sao chép (Ctrl+C / Command+C hoặc qua menu). | Chỉnh sửa dữ liệu được sao chép hoặc hủy bỏ hành động mặc định. |
cut |
Người dùng thực hiện cắt (Ctrl+X / Command+X). | Thay đổi nội dung đưa vào clipboard hoặc chặn thao tác cắt. |
paste |
Người dùng thực hiện dán (Ctrl+V / Command+V). | Xử lý, lọc dữ liệu trước khi dán hoặc ngăn chặn việc dán mặc định. |
Các sự kiện này có thể được gắn trực tiếp vào các phần tử DOM cụ thể hoặc gắn vào đối tượng document để bắt toàn bộ thao tác trên trang.
Khởi tạo Web Component trong ArkTS
Ở phía ArkTS, cấu hình để tải trang HTML rất đơn giản. Web component không đòi hỏi các thiết lập đặc biệt nào liên quan đến quyền clipboard ở lớp framework cho tính năng này:
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct ClipboardMonitorView {
private webCtrl: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Web({ src: $rawfile('clipboard_interactions.html'), controller: this.webCtrl })
.width('100%')
.height('100%')
}
}
}
Triển khai logic bắt sự kiện ở phía H5
Đoạn mã dưới đây minh họa cách tích hợp và xử lý cả ba sự kiện clipboard. Logic đã được thiết kế để ghi lại nhật ký thao tác và biến đổi dữ liệu sao chép/dán theo các quy tắc định sẵn.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Giám sát Clipboard</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; background: #fafafa; }
.input-box {
width: 100%; min-height: 100px; border: 1px solid #ccc;
padding: 12px; font-size: 15px; border-radius: 6px; resize: vertical;
}
.log-panel {
margin-top: 20px; background: #2d2d2d; color: #f8f8f2;
padding: 15px; border-radius: 6px; font-family: monospace;
font-size: 13px; max-height: 200px; overflow-y: auto;
}
.log-copy { color: #66d9ef; }
.log-cut { color: #f92672; }
.log-paste { color: #a6e22e; }
</style>
</head>
<body>
<h3>Khu vực thao tác Clipboard</h3>
<textarea id="inputBox" class="input-box" placeholder="Nhập và bôi đen văn bản để test copy/cut, hoặc dán nội dung vào đây...">
Đây là văn bản mẫu. Hãy thử bôi đen và nhấn Ctrl+C.
Từ khóa bí mật: password123.
</textarea>
<h3>Nhật ký hệ thống</h3>
<div id="logPanel" class="log-panel">Đang chờ sự kiện...</div>
<script>
const txtArea = document.getElementById('inputBox');
const logBox = document.getElementById('logPanel');
let logEntries = [];
const updateUI = (type, msg) => {
const time = new Date().toLocaleTimeString();
logEntries.unshift({ type, time, msg });
if (logEntries.length > 15) logEntries.pop();
logBox.innerHTML = logEntries.map(entry =>
`<div class="log-${entry.type}">[${entry.time}] ${entry.msg}</div>`
).join('');
};
// Xử lý sự kiện COPY
txtArea.addEventListener('copy', (evt) => {
const selection = window.getSelection().toString();
updateUI('copy', `Phát hiện copy: "${selection.substring(0, 20)}..."`);
if (selection && evt.clipboardData) {
// Biến đổi dữ liệu: Thêm watermark
const payload = `${selection}\n\n[Watermark: Dữ liệu nội bộ]`;
evt.clipboardData.setData('text/plain', payload);
evt.preventDefault();
updateUI('copy', 'Đã chặn hành vi mặc định và gắn watermark.');
}
});
// Xử lý sự kiện CUT
txtArea.addEventListener('cut', (evt) => {
const selection = window.getSelection().toString();
updateUI('cut', `Phát hiện cut: "${selection}". Hệ thống cho phép cắt.`);
// Không gọi preventDefault(), để trình duyệt tự xóa text đã chọn
});
// Xử lý sự kiện PASTE
txtArea.addEventListener('paste', (evt) => {
const rawData = evt.clipboardData ? evt.clipboardData.getData('text/plain') : '';
updateUI('paste', `Nội dung paste nhận được: "${rawData.substring(0, 20)}..."`);
// Lọc từ khóa bảo mật
if (/password/i.test(rawData)) {
evt.preventDefault();
const safeData = rawData.replace(/password/ig, '[REDACTED]');
document.execCommand('insertText', false, safeData);
updateUI('paste', 'Đã chặn paste gốc, tiến hành sanitize dữ liệu.');
}
});
</script>
</body>
</html>
Phân tích các API cốt lõi
event.clipboardData: Đối tượngDataTransferchứa dữ liệu của clipboard. Ở sự kiệncopyvàcut, nó có thể đọc/ghi. Tuy nhiên, ở sự kiệnpaste, thuộc tính này thường ở trạng thái chỉ đọc (read-only).event.preventDefault(): Phương thức ngăn chặn hành vi mặc định của trình duyệt. Khi được gọi trongcopy, trình duyệt sẽ bỏ qua việc sao chép text đã chọn và thay vào đó sử dụng chuỗi được chỉ định quasetData().window.getSelection().toString(): Trích xuất chuỗi văn bản đang được người dùng bôi đen trên giao diện DOM.
Ứng dụng thực tế theo từng sự kiện
Sự kiện Copy:
- Tự động append thông tin bản quyền hoặc đường dẫn nguồn gốc.
- Chuẩn hóa định dạng văn bản (loại bỏ ký tự ẩn, căn chỉnh lại khoảng trắng) trước khi đưa vào clipboard.
- Theo dõi hành vi người dùng phục vụ cho các hệ thống phân tích dữ liệu (Analytics).
Sự kiện Cut:
- Ghi nhận lịch sử thay đổi trong các ứng dụng soạn thảo văn bản phức tạp (Rich Text Editor) để hỗ trợ tính năng Undo/Redo.
- Bảo vệ các khối nội dung quan trọng không cho phép người dùng cắt đi.
Sự kiện Paste:
- Kiểm duyệt và loại bỏ các mã độc, thẻ HTML không mong muốn hoặc từ khóa vi phạm chính sách.
- Ép buộc dữ liệu đầu vào phải là plain text, loại bỏ hoàn toàn định dạng style từ các nguồn như Microsoft Word.
- Tiền xử lý ảnh (nén, đổi định dạng) ngay khi người dùng paste hình ảnh vào khung chat.
Giải pháp ép buộc Paste Plain Text
Khi tích hợp các khung soạn thảo văn bản, việc người dùng dán các đoạn text mang theo định dạng CSS phức tạp thường làm vỡ layout. Sử dụng sự kiện paste để ép buộc dữ liệu về dạng thuần văn bản là phương pháp tối ưu:
const editorElement = document.getElementById('richEditor');
editorElement.addEventListener('paste', function(evt) {
evt.preventDefault();
// Trích xuất hoàn toàn plain text, bỏ qua mọi cấu trúc HTML
const rawText = evt.clipboardData.getData('text/plain');
// Chèn thủ công vào vị trí con trỏ
document.execCommand('insertText', false, rawText);
});
Các lỗi logic thường gặp
Sử dụng setData() trong sự kiện Paste:
Trong luồng xử lý paste, đối tượng event.clipboardData bị khóa ở chế độ read-only. Việc gọi setData() sẽ thất bại. Quy trình chuẩn để biến đổi dữ liệu khi dán là: gọi preventDefault() để chặn hành vi mặc định, đọc dữ liệu từ getData(), xử lý chuỗi, và cuối cùng dùng document.execCommand('insertText', ...) hoặc Selection API để đẩy nội dung đã xử lý vào DOM.
Nhầm lẫn giữa clipboardData và navigator.clipboard:
Đây là hai cơ chế hoàn toàn độc lập. event.clipboardData trong các event listener là đồng bộ (synchronous) và chỉ tồn tại trong phạm vi hàm callback của sự kiện. Ngược lại, navigator.clipboard là tập hợp các API bất đồng bộ (dựa trên Promise) dùng để chủ động thao tác với bảng tạm. Lập trình viên không nên trộn lẫn hai cơ chế này trong cùng một luồng xử lý sự kiện; nếu đang xử lý event copy, chỉ cần dùng event.clipboardData là đủ.