fix: editor

This commit is contained in:
2026-04-22 20:20:09 +07:00
parent c828bf7767
commit 994a13da52
8 changed files with 286 additions and 127 deletions
+133 -106
View File
@@ -19,7 +19,6 @@
#toolbar button:hover { background: #0f3460; }
#toolbar button.primary { background: #533483; border-color: #7b2d8e; }
#toolbar button.save { background: #1b8a5a; border-color: #239b6e; }
#toolbar button[disabled] { opacity: 0.4; cursor: not-allowed; }
#page-indicator { color: #a0a0c0; font-weight: 600; min-width: 100px; text-align: center; }
.spacer { flex: 1; }
#statusbar {
@@ -37,6 +36,7 @@
padding: 2px 12px; border-radius: 4px;
}
.editor-selected { outline: 3px solid #7c3aed !important; outline-offset: 4px; }
.editor-changed { position: relative; }
.editor-changed::after {
content: ''; position: absolute; top: -2px; right: -2px;
width: 8px; height: 8px; background: #f97316; border-radius: 50%; z-index: 10;
@@ -63,8 +63,10 @@
<div class="spacer"></div>
<span id="selection-info" style="color: #c4b5fd; font-size: 12px;">Loading...</span>
<div class="spacer"></div>
<button id="btn-reset">Reset Page</button>
<button id="btn-copy" class="primary">Copy JSON</button>
<button id="btn-save" class="save" disabled>Save</button>
<button id="btn-copy-changes" class="primary">Copy Changes</button>
<button id="btn-save" class="save">Save</button>
</div>
<div id="worksheet-container"></div>
<div id="statusbar">
@@ -75,21 +77,19 @@
<script src="../../src/editor/editor-core.js"></script>
<script>
// ============================================================
// Simple approach: ALL state lives in the DOM (img.style.transform).
// No in-memory state. buildConfig reads from DOM.
// Like collecting-asteroids editor pattern.
// ============================================================
(function() {
'use strict';
var sectionEls = []; // [{container, bigImg, smallImgs, pageNum, secIndex}]
var selected = null; // currently selected section
var ready = false;
const sectionEls = []; // [{container, bigImg, smallImgs, pageNum, secIndex}]
const originalTransforms = new Map(); // "pageNum-secIndex" → {scale, rotate}
let selected = null;
// --- Transform helpers ---
// --- Parse transform string from DOM ---
function parseTransform(img) {
var t = img.style.transform || '';
var sm = t.match(/scale\(([\d.]+)\)/);
var rm = t.match(/rotate\(([-\d.]+)deg\)/);
const t = img.style.transform || '';
const sm = t.match(/scale\(([\d.]+)\)/);
const rm = t.match(/rotate\(([-\d.]+)deg\)/);
return {
scale: sm ? parseFloat(sm[1]) : 1,
rotate: rm ? parseFloat(rm[1]) : 0
@@ -97,29 +97,42 @@
}
function setTransform(img, scale, rotate) {
img.style.transform = 'scale(' + scale + ') rotate(' + rotate + 'deg)';
if (scale === 1 && rotate === 0) {
img.style.transform = '';
} else {
img.style.transform = 'scale(' + scale + ') rotate(' + rotate + 'deg)';
}
}
function secKey(sec) {
return sec.pageNum + '-' + sec.secIndex;
}
// --- Find all sections in loaded pages ---
function findSections(pages) {
sectionEls = [];
sectionEls.length = 0;
pages.forEach(function(page, pi) {
var glows = page.querySelectorAll('.asteroid-glow');
const glows = page.querySelectorAll('.asteroid-glow');
glows.forEach(function(container, si) {
var bigImg = container.querySelector('img');
var sectionRow = container.parentElement;
const bigImg = container.querySelector('img');
// Walk up to find the section row containing formula small imgs
let sectionRow = container.parentElement;
while (sectionRow && !sectionRow.classList.contains('gap-[8mm]')) {
sectionRow = sectionRow.parentElement;
}
if (!sectionRow) sectionRow = container.parentElement;
var allImgs = sectionRow.querySelectorAll('img[src*="pack3-asteroids"]');
var smallImgs = [];
const allImgs = sectionRow.querySelectorAll('img[src*="pack3-asteroids"]');
const smallImgs = [];
allImgs.forEach(function(img) { if (img !== bigImg) smallImgs.push(img); });
var sec = { container: container, bigImg: bigImg, smallImgs: smallImgs, pageNum: pi + 1, secIndex: si };
const sec = { container, bigImg, smallImgs, pageNum: pi + 1, secIndex: si };
sectionEls.push(sec);
// Store original transform (from template, before data.json applied)
originalTransforms.set(secKey(sec), { scale: 1, rotate: 0 });
container.style.cursor = 'pointer';
container.addEventListener('click', function(e) {
e.stopPropagation();
@@ -127,94 +140,124 @@
});
});
});
document.getElementById('status-text').textContent =
sectionEls.length + ' sections. Keys: +/- scale \u2022 [/] rotate (Shift=fine) \u2022 0 reset';
document.getElementById('selection-info').textContent = 'Click a big asteroid to select';
}
// --- Selection ---
function selectSection(sec) {
if (selected) selected.container.classList.remove('editor-selected');
deselectAll();
selected = sec;
sec.container.classList.add('editor-selected');
showInfo();
updateInfo();
}
function showInfo() {
var el = document.getElementById('selection-info');
function deselectAll() {
if (selected) {
selected.container.classList.remove('editor-selected');
selected = null;
}
updateInfo();
}
function updateInfo() {
const el = document.getElementById('selection-info');
if (!selected) { el.textContent = 'Click a big asteroid to select'; return; }
var t = parseTransform(selected.bigImg);
const t = parseTransform(selected.bigImg);
el.textContent = 'Page ' + selected.pageNum + ' Sec ' + (selected.secIndex + 1) +
' \u2014 scale: ' + t.scale.toFixed(2) + ' rotate: ' + t.rotate + '\u00B0';
' \u2014 scale: ' + t.scale.toFixed(2) + ' rotate: ' + t.rotate.toFixed(1) + '\u00B0';
}
// --- Apply scale/rotate to big + all small imgs ---
// --- Apply transform to section (big + small imgs) ---
function applyToSection(sec, scale, rotate) {
setTransform(sec.bigImg, scale, rotate);
sec.smallImgs.forEach(function(img) { setTransform(img, scale, rotate); });
sec.container.classList.toggle('editor-changed', scale !== 1 || rotate !== 0);
showInfo();
markChanged(sec);
updateInfo();
}
// --- Load data.json and apply transforms to DOM ---
function loadData(docId, cb) {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'docs/' + docId + '.data.json', true);
xhr.onload = function() {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
var n = 0;
(data.pages || []).forEach(function(p) {
(p.sections || []).forEach(function(s) {
if (s.scale === 1 && s.rotate === 0) return;
var sec = sectionEls.find(function(el) {
return el.pageNum === p.page && el.secIndex === s.index;
});
if (sec) { applyToSection(sec, s.scale, s.rotate); n++; }
});
});
if (n > 0) core.showToast('Loaded ' + n + ' edits');
} catch(e) {}
}
if (cb) cb();
};
xhr.onerror = function() { if (cb) cb(); };
xhr.send();
// --- Change tracking ---
function markChanged(sec) {
const orig = originalTransforms.get(secKey(sec));
const cur = parseTransform(sec.bigImg);
const changed = Math.abs(cur.scale - orig.scale) > 0.001 || Math.abs(cur.rotate - orig.rotate) > 0.1;
sec.container.classList.toggle('editor-changed', changed);
}
// --- Serialize: read ALL transforms from DOM ---
function buildConfig() {
var pagesMap = {};
// --- Apply data.json to DOM (called by EditorCore.onDataLoaded) ---
function applyData(data) {
if (!data || !data.pages) return;
let count = 0;
data.pages.forEach(function(p) {
(p.sections || []).forEach(function(s) {
const sec = sectionEls.find(function(el) {
return el.pageNum === p.page && el.secIndex === s.index;
});
if (sec) {
const scale = s.scale ?? 1;
const rotate = s.rotate ?? 0;
applyToSection(sec, scale, rotate);
// After applying saved data, update originals so "changed" is relative to saved state
originalTransforms.set(secKey(sec), { scale, rotate });
sec.container.classList.remove('editor-changed');
count++;
}
});
});
if (count > 0) core.showToast('Loaded ' + count + ' edits');
}
// --- Serialization ---
function buildConfig(changesOnly) {
const pagesMap = {};
sectionEls.forEach(function(sec) {
var t = parseTransform(sec.bigImg);
const cur = parseTransform(sec.bigImg);
const orig = originalTransforms.get(secKey(sec));
if (changesOnly) {
const changed = Math.abs(cur.scale - orig.scale) > 0.001 || Math.abs(cur.rotate - orig.rotate) > 0.1;
if (!changed) return;
}
if (!pagesMap[sec.pageNum]) pagesMap[sec.pageNum] = { page: sec.pageNum, sections: [] };
pagesMap[sec.pageNum].sections.push({
index: sec.secIndex,
scale: +t.scale.toFixed(3),
rotate: +t.rotate.toFixed(1)
scale: +cur.scale.toFixed(3),
rotate: +cur.rotate.toFixed(1)
});
});
return { pages: Object.values(pagesMap) };
}
// --- Save: sync XHR, read from DOM ---
function saveToServer() {
if (!ready) return;
var data = buildConfig();
try {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/save-edits', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ taskType: 'asteroid-splitting', docId: core.docId, data: data }));
core.showToast(xhr.status === 200 ? 'Saved!' : 'Error: ' + xhr.statusText);
} catch(e) {
core.showToast('Save failed: ' + e.message);
}
// --- Reset ---
function resetCurrentPage(pageNum) {
sectionEls.forEach(function(sec) {
if (sec.pageNum !== pageNum) return;
const orig = originalTransforms.get(secKey(sec));
applyToSection(sec, orig.scale, orig.rotate);
sec.container.classList.remove('editor-changed');
});
if (selected && selected.pageNum === pageNum) updateInfo();
}
// --- Keyboard ---
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { deselectAll(); return; }
if (!selected) return;
var t = parseTransform(selected.bigImg);
var step = e.shiftKey ? 0.01 : 0.05;
var rotStep = e.shiftKey ? 1 : 5;
const t = parseTransform(selected.bigImg);
const step = e.shiftKey ? 0.01 : 0.05;
const rotStep = e.shiftKey ? 1 : 5;
switch (e.key) {
case '+': case '=':
applyToSection(selected, Math.min(2, +(t.scale + step).toFixed(3)), t.rotate);
@@ -223,53 +266,37 @@
applyToSection(selected, Math.max(0.3, +(t.scale - step).toFixed(3)), t.rotate);
e.preventDefault(); break;
case '[':
applyToSection(selected, t.scale, t.rotate - rotStep);
applyToSection(selected, t.scale, +(t.rotate - rotStep).toFixed(1));
e.preventDefault(); break;
case ']':
applyToSection(selected, t.scale, t.rotate + rotStep);
applyToSection(selected, t.scale, +(t.rotate + rotStep).toFixed(1));
e.preventDefault(); break;
case '0':
applyToSection(selected, 1, 0);
e.preventDefault(); break;
case 'Escape':
if (selected) { selected.container.classList.remove('editor-selected'); selected = null; showInfo(); }
break;
}
});
document.addEventListener('click', function(e) {
if (selected && !e.target.closest('.asteroid-glow')) {
selected.container.classList.remove('editor-selected');
selected = null; showInfo();
deselectAll();
}
});
// --- Init ---
// NOTE: do NOT pass serialize to EditorCore — we handle Save ourselves
// to avoid double-handler (EditorCore's async + ours sync) race condition
var core = EditorCore.init({
// --- Init via EditorCore ---
const core = EditorCore.init({
taskType: 'asteroid-splitting',
serialize: buildConfig,
onReset: resetCurrentPage,
onReady: function(pages) {
findSections(pages);
document.getElementById('status-text').textContent =
sectionEls.length + ' sections. Keys: +/- scale \u2022 [/] rotate (Shift=fine) \u2022 0 reset';
// Load saved data, then enable Save
loadData(core.docId, function() {
ready = true;
document.getElementById('btn-save').disabled = false;
document.getElementById('selection-info').textContent = 'Click a big asteroid to select';
});
// Wire Save button directly (EditorCore won't touch it without serialize)
document.getElementById('btn-save').addEventListener('click', function(e) {
saveToServer();
});
// Wire Copy JSON
document.getElementById('btn-copy').addEventListener('click', function(e) {
var json = JSON.stringify(buildConfig(), null, 2);
navigator.clipboard.writeText(json).then(function() { core.showToast('Copied!'); });
});
},
onDataLoaded: function(data) {
if (data) applyData(data);
}
});
})();
</script>
</body>
</html>