Đú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
+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);