Files
odysseus/static/js/editor/build/transform-popup.js
pewdiepie-archdaemon e5c99a5eee Odysseus v1.0
2026-05-31 23:58:26 +09:00

110 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Static markup for the Transform popup that floats over the canvas
* when the user activates the Resize/Transform tool.
*
* Pure DOM — no module state, no event listeners. The caller wires all
* IDs via document.getElementById / pop.querySelector.
*
* @returns {string}
*/
export function transformPopupHTML() {
return `
<div class="ge-adj-head ge-transform-popup-head" data-transform-drag>
<span class="ge-adj-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 7 3 11 7"/><line x1="7" y1="3" x2="7" y2="21"/><polyline points="21 17 17 21 13 17"/><line x1="17" y1="21" x2="17" y2="3"/></svg>
</span>
<span class="ge-adj-title">Transform</span>
<button type="button" id="ge-transform-aspect" class="ge-transform-aspect-btn" title="Lock aspect ratio" aria-pressed="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</button>
<span class="ge-head-btns">
<button class="ge-adj-min" type="button" title="Minimise" id="ge-transform-min">&minus;</button>
<button class="ge-adj-close" type="button" title="Cancel" id="ge-transform-cancel">&times;</button>
</span>
</div>
<div class="ge-transform-popup-body">
<div class="ge-transform-field">
<label>W</label>
<input type="number" class="ge-transform-popup-input" id="ge-transform-w" step="1" />
<span class="ge-transform-spin" data-spin-for="ge-transform-w">
<button type="button" data-spin="down" tabindex="-1" aria-label="Decrease width"></button>
<button type="button" data-spin="up" tabindex="-1" aria-label="Increase width">+</button>
</span>
</div>
<div class="ge-transform-field">
<label>H</label>
<input type="number" class="ge-transform-popup-input" id="ge-transform-h" step="1" />
<span class="ge-transform-spin" data-spin-for="ge-transform-h">
<button type="button" data-spin="down" tabindex="-1" aria-label="Decrease height"></button>
<button type="button" data-spin="up" tabindex="-1" aria-label="Increase height">+</button>
</span>
</div>
<div class="ge-row-break"></div>
<div class="ge-transform-field">
<label>↻</label>
<input type="number" class="ge-transform-popup-input ge-transform-popup-input-rot" id="ge-transform-rot" step="1" value="0" />
<span class="ge-transform-spin" data-spin-for="ge-transform-rot">
<button type="button" data-spin="down" tabindex="-1" aria-label="Rotate -1°"></button>
<button type="button" data-spin="up" tabindex="-1" aria-label="Rotate +1°">+</button>
</span>
</div>
<button type="button" class="ge-btn ge-btn-sm" id="ge-transform-cancel-btn">Cancel</button>
<button type="button" class="ge-btn ge-btn-sm ge-btn-primary" id="ge-transform-apply">Apply</button>
</div>
<p class="ge-transform-popup-hint">Type <strong>-</strong> before W / H to flip.</p>
`;
}
/**
* Wire a `<span class="ge-transform-spin">…<button data-spin="up|down"/>…</span>`
* group with tap-to-tick + hold-to-repeat. After 1.5 s the repeat
* accelerates from 70ms→30ms intervals so users can rapidly scrub a
* numeric field without mashing the button.
*
* On each tick, the helper looks up the target `<input>` by the
* spin-group's `data-spin-for` attribute and dispatches an `input`
* event so the rest of the popup's wiring picks up the change.
*
* @param {HTMLElement} root Element that owns one or more spin groups
* (e.g. the transform popup).
*/
export function attachSpinRepeat(root) {
root.querySelectorAll('.ge-transform-spin button').forEach(btn => {
const tick = (shift) => {
const targetId = btn.parentElement?.dataset?.spinFor;
if (!targetId) return;
const input = root.querySelector('#' + CSS.escape(targetId));
if (!input || input.readOnly) return;
const step = shift ? 10 : 1;
const cur = parseInt(input.value, 10) || 0;
const next = btn.dataset.spin === 'up' ? cur + step : cur - step;
input.value = String(next);
input.dispatchEvent(new Event('input', { bubbles: true }));
};
let holdTimeout = null, repeatInterval = null, started = 0;
btn.addEventListener('pointerdown', (e) => {
e.preventDefault();
tick(e.shiftKey);
started = Date.now();
holdTimeout = setTimeout(() => {
repeatInterval = setInterval(() => {
tick(false);
if (Date.now() - started > 1500 && repeatInterval) {
clearInterval(repeatInterval);
repeatInterval = setInterval(() => tick(false), 30);
}
}, 70);
}, 350);
});
const endHold = () => {
if (holdTimeout) clearTimeout(holdTimeout);
if (repeatInterval) clearInterval(repeatInterval);
holdTimeout = null; repeatInterval = null;
};
btn.addEventListener('pointerup', endHold);
btn.addEventListener('pointerleave', endHold);
btn.addEventListener('pointercancel', endHold);
});
}