1/** 2 * Static markup for the Transform popup that floats over the canvas 3 * when the user activates the Resize/Transform tool. 4 * 5 * Pure DOM â no module state, no event listeners. The caller wires all 6 * IDs via document.getElementById / pop.querySelector. 7 * 8 * @returns {string} 9 */ 10export function transformPopupHTML() { 11 return ` 12 <div class="ge-adj-head ge-transform-popup-head" data-transform-drag> 13 <span class="ge-adj-icon"> 14 <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> 15 </span> 16 <span class="ge-adj-title">Transform</span> 17 <button type="button" id="ge-transform-aspect" class="ge-transform-aspect-btn" title="Lock aspect ratio" aria-pressed="true"> 18 <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> 19 </button> 20 <span class="ge-head-btns"> 21 <button class="ge-adj-min" type="button" title="Minimise" id="ge-transform-min">−</button> 22 <button class="ge-adj-close" type="button" title="Cancel" id="ge-transform-cancel">×</button> 23 </span> 24 </div> 25 <div class="ge-transform-popup-body"> 26 <div class="ge-transform-field"> 27 <label>W</label> 28 <input type="number" class="ge-transform-popup-input" id="ge-transform-w" step="1" /> 29 <span class="ge-transform-spin" data-spin-for="ge-transform-w"> 30 <button type="button" data-spin="down" tabindex="-1" aria-label="Decrease width">â</button> 31 <button type="button" data-spin="up" tabindex="-1" aria-label="Increase width">+</button> 32 </span> 33 </div> 34 <div class="ge-transform-field"> 35 <label>H</label> 36 <input type="number" class="ge-transform-popup-input" id="ge-transform-h" step="1" /> 37 <span class="ge-transform-spin" data-spin-for="ge-transform-h"> 38 <button type="button" data-spin="down" tabindex="-1" aria-label="Decrease height">â</button> 39 <button type="button" data-spin="up" tabindex="-1" aria-label="Increase height">+</button> 40 </span> 41 </div> 42 <div class="ge-row-break"></div> 43 <div class="ge-transform-field"> 44 <label>â»</label> 45 <input type="number" class="ge-transform-popup-input ge-transform-popup-input-rot" id="ge-transform-rot" step="1" value="0" /> 46 <span class="ge-transform-spin" data-spin-for="ge-transform-rot"> 47 <button type="button" data-spin="down" tabindex="-1" aria-label="Rotate -1°">â</button> 48 <button type="button" data-spin="up" tabindex="-1" aria-label="Rotate +1°">+</button> 49 </span> 50 </div> 51 <button type="button" class="ge-btn ge-btn-sm" id="ge-transform-cancel-btn">Cancel</button> 52 <button type="button" class="ge-btn ge-btn-sm ge-btn-primary" id="ge-transform-apply">Apply</button> 53 </div> 54 <p class="ge-transform-popup-hint">Type <strong>-</strong> before W / H to flip.</p> 55 `; 56} 57 58 59/** 60 * Wire a `<span class="ge-transform-spin">â¦<button data-spin="up|down"/>â¦</span>` 61 * group with tap-to-tick + hold-to-repeat. After 1.5 s the repeat 62 * accelerates from 70msâ30ms intervals so users can rapidly scrub a 63 * numeric field without mashing the button. 64 * 65 * On each tick, the helper looks up the target `<input>` by the 66 * spin-group's `data-spin-for` attribute and dispatches an `input` 67 * event so the rest of the popup's wiring picks up the change. 68 * 69 * @param {HTMLElement} root Element that owns one or more spin groups 70 * (e.g. the transform popup). 71 */ 72export function attachSpinRepeat(root) { 73 root.querySelectorAll('.ge-transform-spin button').forEach(btn => { 74 const tick = (shift) => { 75 const targetId = btn.parentElement?.dataset?.spinFor; 76 if (!targetId) return; 77 const input = root.querySelector('#' + CSS.escape(targetId)); 78 if (!input || input.readOnly) return; 79 const step = shift ? 10 : 1; 80 const cur = parseInt(input.value, 10) || 0; 81 const next = btn.dataset.spin === 'up' ? cur + step : cur - step;
82 input.value = String(next); 83 input.dispatchEvent(new Event('input', { bubbles: true })); 84 }; 85 let holdTimeout = null, repeatInterval = null, started = 0; 86 btn.addEventListener('pointerdown', (e) => { 87 e.preventDefault(); 88 tick(e.shiftKey); 89 started = Date.now(); 90 holdTimeout = setTimeout(() => { 91 repeatInterval = setInterval(() => { 92 tick(false); 93 if (Date.now() - started > 1500 && repeatInterval) { 94 clearInterval(repeatInterval); 95 repeatInterval = setInterval(() => tick(false), 30); 96 } 97 }, 70); 98 }, 350); 99 }); 100 const endHold = () => { 101 if (holdTimeout) clearTimeout(holdTimeout); 102 if (repeatInterval) clearInterval(repeatInterval); 103 holdTimeout = null; repeatInterval = null; 104 }; 105 btn.addEventListener('pointerup', endHold); 106 btn.addEventListener('pointerleave', endHold); 107 btn.addEventListener('pointercancel', endHold); 108 }); 109}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.