Đúp chuột vào chữ từ viewer thường → vào thẳng chế độ sửa (chuẩn Foxit Editor)

Trước đây phải bấm nút '✏️ Sửa nội dung' rồi mới double-click được. Giờ:
- Đang xem bình thường, đúp chuột vào chữ → hit-test qua edit_list_objects
  tại điểm đúp (toạ độ PDF qua cssToPdf); có text object → tự vào chế độ Sửa
  nội dung đúng trang + mở NGAY ô sửa tại đoạn đó (startTextEdit: đoạn nhiều
  dòng ra textarea reflow, 1 dòng ra ô sửa dòng)
- Đúp vùng trống: không làm gì (như Foxit). Đang cầm công cụ chú thích /
  organize / redact: bỏ qua. Element chú thích (freetext/note/markup) giữ
  handler đúp riêng của chúng
- pickTextAt: ưu tiên box chứa điểm (nới 3pt, chọn box nhỏ nhất khi chồng),
  fallback box gần nhất trong 12pt; enterEditMode nhận pageIndex tuỳ chọn;
  editPendingPoint tiêu thụ sau loadEditPage; bỏ selection 1 từ do browser
  tự bôi khi đúp trên text layer

Docs: 13 (E31–E33), README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
This commit is contained in:
2026-07-16 04:18:16 +00:00
co-authored by Claude Fable 5
parent a67f22df1e
commit 77ed917913
3 changed files with 78 additions and 2 deletions
+1
View File
@@ -41,6 +41,7 @@
- [x] UI chế độ "Sửa nội dung": overlay đối tượng, sửa text tại chỗ WYSIWYG, thêm chữ/ảnh, xoá, cỡ chữ/màu, undo/redo, lưu
- [x] **Iteration 2 — GIỮ FONT khi sửa (chuẩn Foxit)**: sửa tại chỗ giữ nguyên font gốc/nhúng (kể cả tiếng Việt); thiếu glyph mới thay font CÙNG HỌ (`fontmatch.rs`); đổi cỡ/màu không đụng font (fix bug phóng đại kép); sửa CẢ DÒNG (gộp run); WYSIWYG khi gõ; B/I + đổi font family; kéo-thả live; dọn file tạm
- [x] **Iteration 3 — Reflow đoạn "như Word"**: double-click đoạn nhiều dòng → sửa cả đoạn, tự bẻ dòng theo bề rộng khối (đo hmtx bằng ttf-parser), giữ font (nhúng lại bytes gốc / font chuẩn base-14 / cùng họ), giữ nhịp baseline; Enter = ngắt cứng
- [x] **Đúp chuột từ viewer thường (chuẩn Foxit)**: đang xem, đúp vào chữ → tự vào chế độ sửa đúng trang + mở ngay ô sửa tại đoạn đó; đúp vùng trống không làm gì
- [ ] Follow-up: reflow v2 (justify, khối xoay, đa font), xoay/lật/clip, viền/opacity/căn lề, convert text→path — xem [docs/12-phase4-summary.md](docs/12-phase4-summary.md)
- **Checklist test cho bạn**: [docs/13-phase4-user-tests.md](docs/13-phase4-user-tests.md) (mới: E17–E30)
- [x] **Phase 5 — Bảo mật** ✅ Iteration 1 + 2 hoàn tất (62/62 test engine xanh)
+70 -2
View File
@@ -37,6 +37,7 @@ const state = {
editScale: 1, // px/pt của ảnh stage
editArm: null, // 'text' | 'image' khi đang chờ click đặt; null = không
editPendingImage: null, // đường dẫn ảnh chờ đặt (Thêm ảnh)
editPendingPoint: null, // điểm PDF chờ mở ô sửa (đúp chuột từ viewer thường)
editColor: [0, 0, 0], // màu chữ áp khi sửa/thêm text
editTemps: [], // mọi file tạm đã materialize trong phiên sửa (để dọn)
secMode: false, // thanh Bảo mật (Phase 5) đang mở
@@ -2033,11 +2034,11 @@ function openCropDialog(pageIdx, rectPdf) {
const EDIT_STAGE_W = 820; // px bề rộng ảnh trang khi sửa
function enterEditMode() {
function enterEditMode(pageIndex) {
if (!state.path) { $("status").textContent = "Hãy mở file trước khi sửa nội dung"; return; }
if (state.organizeMode) exitOrganizeMode();
state.editMode = true;
state.editPage = state.current;
state.editPage = Number.isInteger(pageIndex) ? pageIndex : state.current;
state.editBase = state.path;
state.editUndo = [];
state.editRedo = [];
@@ -2079,6 +2080,34 @@ function toggleEditMode() {
else enterEditMode();
}
// Đúp chuột vào CHỮ trên viewer thường (chuẩn Foxit Editor): tự vào chế độ
// Sửa nội dung đúng trang + mở ngay ô sửa tại đoạn vừa đúp. Đúp vùng trống
// hoặc đang cầm công cụ chú thích thì không làm gì.
async function onViewerDblClick(e) {
if (state.editMode || state.organizeMode || state.tool) return;
// Chú thích có handler đúp riêng (freetext) hoặc là element tương tác — bỏ qua.
if (e.target.closest(".a-editor, .a-hl, .a-box, .a-text, .a-note, .redact-mark")) return;
const slot = e.target.closest(".page-slot");
if (!slot) return;
const idx = Number(slot.dataset.index);
const r = slot.getBoundingClientRect();
const p = cssToPdf(idx, e.clientX - r.left, e.clientY - r.top);
let objs;
try {
objs = await invoke("edit_list_objects", { path: state.path, page: idx, password: null });
} catch (_) {
return; // trang không đọc được object → giữ nguyên viewer
}
if (!pickTextAt(objs, p.x, p.y)) return; // đúp vùng trống: như Foxit, không làm gì
// Bỏ selection từ đúp chuột (browser tự bôi 1 từ trong text layer).
const sel = window.getSelection();
if (sel) sel.removeAllRanges();
state.editPendingPoint = { x: p.x, y: p.y };
enterEditMode(idx);
}
// Đọc lại object + render ảnh trang hiện tại từ editBase, dựng overlay box.
async function loadEditPage() {
const p = state.pages[state.editPage];
@@ -2097,6 +2126,44 @@ async function loadEditPage() {
}
$("edSave").disabled = state.editBase === state.path; // chưa có thay đổi nào
updateEditUndoButtons();
// Đúp chuột từ viewer thường (kiểu Foxit): vào thẳng ô sửa tại điểm đã đúp.
const pending = state.editPendingPoint;
if (pending) {
state.editPendingPoint = null;
const o = pickTextAt(state.editObjects, pending.x, pending.y);
if (o) {
selectEditObject(o.index);
startTextEdit(o);
}
}
}
// Text object dưới điểm (x,y) điểm PDF: ưu tiên box CHỨA điểm (nới 3pt, chọn
// box nhỏ nhất khi chồng nhau), fallback box gần nhất trong 12pt.
function pickTextAt(objs, x, y) {
const texts = objs.filter((o) => o.kind === "text");
const pad = 3;
const containing = texts.filter(
(o) => x >= o.rect.left - pad && x <= o.rect.right + pad && y >= o.rect.bottom - pad && y <= o.rect.top + pad
);
if (containing.length) {
containing.sort(
(a, b) =>
(a.rect.right - a.rect.left) * (a.rect.top - a.rect.bottom) -
(b.rect.right - b.rect.left) * (b.rect.top - b.rect.bottom)
);
return containing[0];
}
let best = null;
let bestD = 12;
for (const o of texts) {
const dx = Math.max(o.rect.left - x, 0, x - o.rect.right);
const dy = Math.max(o.rect.bottom - y, 0, y - o.rect.top);
const d = Math.hypot(dx, dy);
if (d < bestD) { bestD = d; best = o; }
}
return best;
}
function editBoxStyle(rect) {
@@ -3299,6 +3366,7 @@ $("saveAnnots").addEventListener("click", saveAnnots);
$("undoBtn").addEventListener("click", () => (state.editMode ? editUndo() : undo()));
$("redoBtn").addEventListener("click", () => (state.editMode ? editRedo() : redo()));
$("pages").addEventListener("mousedown", onPagesMouseDown);
$("pages").addEventListener("dblclick", onViewerDblClick);
window.addEventListener("mousemove", onPagesMouseMove);
window.addEventListener("mouseup", onPagesMouseUp);
+7
View File
@@ -41,6 +41,13 @@ cargo run
| E24 | Sửa text tiếng Việt trên file có font nhúng (file Word xuất PDF) | Đúng dấu **và giữ nguyên font nhúng** (mở lại ở Foxit xem tên font trong Edit) | |
| E25 | Sửa nhiều lần rồi thoát chế độ sửa (không lưu) | Không còn file `ff_edit_*.pdf` rác trong thư mục %TEMP% | |
## Bổ sung — Đúp chuột vào chữ từ viewer thường (chuẩn Foxit Editor)
| # | Thao tác | Kỳ vọng | KQ |
|---|----------|---------|----|
| E31 | Đang XEM bình thường (không bật chế độ sửa), **đúp chuột vào 1 dòng chữ** | Tự vào chế độ Sửa nội dung đúng trang đó và Ô SỬA mở sẵn ngay tại dòng/đoạn vừa đúp | |
| E32 | Đúp chuột vào VÙNG TRỐNG của trang | Không có gì xảy ra (như Foxit) | |
| E33 | Đang cầm công cụ chú thích (highlight/note...) rồi đúp vào chữ | KHÔNG nhảy vào chế độ sửa (công cụ chú thích được ưu tiên) | |
## Bổ sung Iteration 3 — Sửa cả đoạn với reflow "như Word"
| # | Thao tác | Kỳ vọng | KQ |
|---|----------|---------|----|