Support bulk layout review selection edits
This commit is contained in:
parent
1e55035f09
commit
e4d129cc06
|
|
@ -1933,6 +1933,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
<button type="button" class="layout-tool-btn" id="layout-select-all">Select All</button>
|
<button type="button" class="layout-tool-btn" id="layout-select-all">Select All</button>
|
||||||
<button type="button" class="layout-tool-btn" id="layout-multi-select">Multi</button>
|
<button type="button" class="layout-tool-btn" id="layout-multi-select">Multi</button>
|
||||||
<button type="button" class="layout-tool-btn" id="layout-clear-selection">Clear</button>
|
<button type="button" class="layout-tool-btn" id="layout-clear-selection">Clear</button>
|
||||||
|
<button type="button" class="layout-tool-btn" id="layout-invert-selection">Invert</button>
|
||||||
|
<button type="button" class="layout-tool-btn" id="layout-select-line">Line</button>
|
||||||
<button type="button" class="layout-tool-btn" id="layout-tool-pan">Pan</button>
|
<button type="button" class="layout-tool-btn" id="layout-tool-pan">Pan</button>
|
||||||
<button type="button" class="layout-tool-btn" id="layout-tool-add">Add</button>
|
<button type="button" class="layout-tool-btn" id="layout-tool-add">Add</button>
|
||||||
<button type="button" class="layout-tool-btn danger" id="layout-delete-word">Delete</button>
|
<button type="button" class="layout-tool-btn danger" id="layout-delete-word">Delete</button>
|
||||||
|
|
@ -2513,6 +2515,16 @@ const HANDLE_SIZE_PX = 14;
|
||||||
let selectedId = null;
|
let selectedId = null;
|
||||||
let selectedIds = new Set();
|
let selectedIds = new Set();
|
||||||
let multiSelectMode = false;
|
let multiSelectMode = false;
|
||||||
|
|
||||||
|
function getSelectedWords() {
|
||||||
|
if (selectedIds && selectedIds.size) return words.filter(w => selectedIds.has(String(w.id)));
|
||||||
|
const w = getSelectedWord();
|
||||||
|
return w ? [w] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchMultiAnchor = false;
|
||||||
|
let touchMultiTimer = null;
|
||||||
|
const activePointers = new Set();
|
||||||
let tool = "select";
|
let tool = "select";
|
||||||
let zoom = 1;
|
let zoom = 1;
|
||||||
let dragState = null;
|
let dragState = null;
|
||||||
|
|
@ -3205,8 +3217,9 @@ function refreshSelectionUI(opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyEditorValues(push = true) {
|
function applyEditorValues(push = true) {
|
||||||
|
const selectedWords = getSelectedWords();
|
||||||
const w = getSelectedWord();
|
const w = getSelectedWord();
|
||||||
if (!w) return null;
|
if (!selectedWords.length || !w) return null;
|
||||||
if (push) pushHistory();
|
if (push) pushHistory();
|
||||||
|
|
||||||
const readValue = (el, fallback) => {
|
const readValue = (el, fallback) => {
|
||||||
|
|
@ -3231,23 +3244,25 @@ function refreshSelectionUI(opts = {}) {
|
||||||
const nextColor = readValue(colorSource, w.text_color_guess || "#000000");
|
const nextColor = readValue(colorSource, w.text_color_guess || "#000000");
|
||||||
|
|
||||||
w.text = nextText;
|
w.text = nextText;
|
||||||
w.font_family_guess = nextFamily;
|
for (const item of selectedWords) {
|
||||||
w.font_size_guess = nextSize;
|
item.font_family_guess = nextFamily;
|
||||||
w.text_color_guess = nextColor;
|
item.font_size_guess = nextSize;
|
||||||
|
item.text_color_guess = nextColor;
|
||||||
|
|
||||||
w.override_style = w.override_style && typeof w.override_style === "object" ? w.override_style : {};
|
item.override_style = item.override_style && typeof item.override_style === "object" ? item.override_style : {};
|
||||||
w.override_style.font_family = nextFamily;
|
item.override_style.font_family = nextFamily;
|
||||||
w.override_style.font_size = nextSize;
|
item.override_style.font_size = nextSize;
|
||||||
w.override_style.text_color = nextColor;
|
item.override_style.text_color = nextColor;
|
||||||
|
|
||||||
w.resolved_style = w.resolved_style && typeof w.resolved_style === "object" ? w.resolved_style : {};
|
item.resolved_style = item.resolved_style && typeof item.resolved_style === "object" ? item.resolved_style : {};
|
||||||
w.resolved_style.font_family = nextFamily;
|
item.resolved_style.font_family = nextFamily;
|
||||||
w.resolved_style.font_size = nextSize;
|
item.resolved_style.font_size = nextSize;
|
||||||
w.resolved_style.text_color = nextColor;
|
item.resolved_style.text_color = nextColor;
|
||||||
|
|
||||||
w.manual_flags = w.manual_flags && typeof w.manual_flags === "object" ? w.manual_flags : {};
|
item.manual_flags = item.manual_flags && typeof item.manual_flags === "object" ? item.manual_flags : {};
|
||||||
w.manual_flags.text_edited = true;
|
item.manual_flags.style_edited = true;
|
||||||
w.manual_flags.style_edited = true;
|
if (String(item.id) === String(w.id)) item.manual_flags.text_edited = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (textInput) textInput.value = nextText;
|
if (textInput) textInput.value = nextText;
|
||||||
if (popoverTextInput) popoverTextInput.value = nextText;
|
if (popoverTextInput) popoverTextInput.value = nextText;
|
||||||
|
|
@ -3270,7 +3285,7 @@ function refreshSelectionUI(opts = {}) {
|
||||||
refreshSelectionUI({ forceText: true });
|
refreshSelectionUI({ forceText: true });
|
||||||
syncReviewedTextarea();
|
syncReviewedTextarea();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
setStatus("Applied changes: " + nextFamily + " " + nextSize);
|
setStatus("Applied style to " + selectedWords.length + " selected");
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3316,34 +3331,40 @@ function refreshSelectionUI(opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function nudge(dx, dy) {
|
function nudge(dx, dy) {
|
||||||
const w = getSelectedWord();
|
const selectedWords = getSelectedWords();
|
||||||
if (!w) return;
|
if (!selectedWords.length) return;
|
||||||
pushHistory();
|
pushHistory();
|
||||||
w.bbox = normalizeBBox([
|
for (const item of selectedWords) {
|
||||||
Number(w.bbox[0]) + dx,
|
item.bbox = normalizeBBox([
|
||||||
Number(w.bbox[1]) + dy,
|
Number(item.bbox[0]) + dx,
|
||||||
Number(w.bbox[2]) + dx,
|
Number(item.bbox[1]) + dy,
|
||||||
Number(w.bbox[3]) + dy,
|
Number(item.bbox[2]) + dx,
|
||||||
]);
|
Number(item.bbox[3]) + dy,
|
||||||
w.bbox = snapBBox(w.bbox, w.id);
|
]);
|
||||||
|
item.bbox = snapBBox(item.bbox, item.id);
|
||||||
|
item.manual_flags = item.manual_flags && typeof item.manual_flags === "object" ? item.manual_flags : {};
|
||||||
|
item.manual_flags.geometry_edited = true;
|
||||||
|
}
|
||||||
refreshSelectionUI({ geometryOnly: true });
|
refreshSelectionUI({ geometryOnly: true });
|
||||||
syncReviewedTextarea();
|
syncReviewedTextarea();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
setStatus("Nudged selection");
|
setStatus("Nudged " + selectedWords.length + " selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelectedWord() {
|
function deleteSelectedWord() {
|
||||||
const w = getSelectedWord();
|
const selectedWords = getSelectedWords();
|
||||||
if (!w) return;
|
if (!selectedWords.length) return;
|
||||||
pushHistory();
|
pushHistory();
|
||||||
words = words.filter(item => String(item.id) !== String(selectedId));
|
const deleteIds = new Set(selectedWords.map(w => String(w.id)));
|
||||||
|
words = words.filter(item => !deleteIds.has(String(item.id)));
|
||||||
|
selectedIds.clear();
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
nextWordId = Math.max(0, ...words.map(item => Number(item.id || 0))) + 1;
|
nextWordId = Math.max(0, ...words.map(item => Number(item.id || 0))) + 1;
|
||||||
snapGuides = null;
|
snapGuides = null;
|
||||||
refreshSelectionUI({ forceText: true });
|
refreshSelectionUI({ forceText: true });
|
||||||
syncReviewedTextarea();
|
syncReviewedTextarea();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
setStatus("Deleted selected box");
|
setStatus("Deleted " + deleteIds.size + " selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLayoutReviewPayload() {
|
function buildLayoutReviewPayload() {
|
||||||
|
|
@ -3540,6 +3561,14 @@ function refreshSelectionUI(opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerDown(ev) {
|
function handlePointerDown(ev) {
|
||||||
|
activePointers.add(ev.pointerId);
|
||||||
|
if (ev.pointerType === "touch" && activePointers.size >= 2) {
|
||||||
|
multiSelectMode = true;
|
||||||
|
document.body.classList.add("layout-multi-select-active");
|
||||||
|
document.getElementById("layout-multi-select")?.classList.add("active");
|
||||||
|
hidePopover();
|
||||||
|
setStatus("Touch multi-select on");
|
||||||
|
}
|
||||||
if (tool === "pan") {
|
if (tool === "pan") {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
beginPan(ev);
|
beginPan(ev);
|
||||||
|
|
@ -3573,13 +3602,29 @@ function refreshSelectionUI(opts = {}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click selects immediately. Drag starts only if pointer moves enough.
|
// Click selects immediately. Drag starts only if pointer moves enough.
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
selectedId = String(hit.word.id);
|
|
||||||
refreshSelectionUI({ forceText: true });
|
|
||||||
renderCanvas();
|
|
||||||
|
|
||||||
dragState = {
|
const id = String(hit.word.id);
|
||||||
|
const additiveSelect = multiSelectMode || ev.ctrlKey || ev.metaKey || ev.shiftKey || activePointers.size >= 2 || touchMultiAnchor;
|
||||||
|
|
||||||
|
if (additiveSelect) {
|
||||||
|
if (selectedIds.has(id)) selectedIds.delete(id);
|
||||||
|
else selectedIds.add(id);
|
||||||
|
selectedId = Array.from(selectedIds).at(-1) || null;
|
||||||
|
hidePopover();
|
||||||
|
refreshSelectionUI({ forceText: true });
|
||||||
|
renderCanvas();
|
||||||
|
setStatus("Selected: " + selectedIds.size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIds = new Set([id]);
|
||||||
|
selectedId = id;
|
||||||
|
refreshSelectionUI({ forceText: true });
|
||||||
|
renderCanvas();
|
||||||
|
|
||||||
|
dragState = {
|
||||||
mode: "pending-move",
|
mode: "pending-move",
|
||||||
wordId: hit.word.id,
|
wordId: hit.word.id,
|
||||||
startX: ev.clientX,
|
startX: ev.clientX,
|
||||||
|
|
@ -3684,7 +3729,8 @@ function refreshSelectionUI(opts = {}) {
|
||||||
document.body.classList.remove("layout-review-dragging");
|
document.body.classList.remove("layout-review-dragging");
|
||||||
}
|
}
|
||||||
|
|
||||||
function endDrag() {
|
function endDrag(ev) {
|
||||||
|
if (ev && ev.pointerId != null) activePointers.delete(ev.pointerId);
|
||||||
if (dragState && dragState.mode === "pending-move") {
|
if (dragState && dragState.mode === "pending-move") {
|
||||||
dragState = null;
|
dragState = null;
|
||||||
snapGuides = null;
|
snapGuides = null;
|
||||||
|
|
@ -3752,6 +3798,35 @@ function refreshSelectionUI(opts = {}) {
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
setStatus("Selection cleared");
|
setStatus("Selection cleared");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("layout-invert-selection")?.addEventListener("click", () => {
|
||||||
|
const next = new Set();
|
||||||
|
for (const w of words) {
|
||||||
|
const id = String(w.id);
|
||||||
|
if (!selectedIds.has(id)) next.add(id);
|
||||||
|
}
|
||||||
|
selectedIds = next;
|
||||||
|
selectedId = Array.from(selectedIds).at(-1) || null;
|
||||||
|
refreshSelectionUI({ forceText: true });
|
||||||
|
renderCanvas();
|
||||||
|
setStatus("Inverted selection: " + selectedIds.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("layout-select-line")?.addEventListener("click", () => {
|
||||||
|
const w = getSelectedWord();
|
||||||
|
if (!w) return;
|
||||||
|
const b = normalizeBBox(w.bbox || [0,0,0,0]);
|
||||||
|
const cy = (b[1] + b[3]) / 2;
|
||||||
|
selectedIds = new Set(words.filter(item => {
|
||||||
|
const ib = normalizeBBox(item.bbox || [0,0,0,0]);
|
||||||
|
const icy = (ib[1] + ib[3]) / 2;
|
||||||
|
return Math.abs(icy - cy) <= LINE_GROUP_TOLERANCE;
|
||||||
|
}).map(item => String(item.id)));
|
||||||
|
selectedId = Array.from(selectedIds).at(-1) || null;
|
||||||
|
refreshSelectionUI({ forceText: true });
|
||||||
|
renderCanvas();
|
||||||
|
setStatus("Selected line: " + selectedIds.size);
|
||||||
|
});
|
||||||
document.getElementById("layout-multi-select")?.addEventListener("click", () => {
|
document.getElementById("layout-multi-select")?.addEventListener("click", () => {
|
||||||
multiSelectMode = !multiSelectMode;
|
multiSelectMode = !multiSelectMode;
|
||||||
document.getElementById("layout-multi-select")?.classList.toggle("active", multiSelectMode);
|
document.getElementById("layout-multi-select")?.classList.toggle("active", multiSelectMode);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue