PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/slider-ux.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:15 UTC 7,778 bytes, 191 lines download raw bytes

1/**
2 * Slider-UX wiring shared across the editor:
3 *
4 *   1. `is-using` class while a slider is being dragged (eraser-rows
5 *      expand to a wider track when in use). Cleared 0.5s after
6 *      pointerup so a quick click doesn't snap back instantly.
7 *   2. Floating value bubble above the thumb during drag.
8 *      Desktop: only the layer-opacity slider gets a bubble (the
9 *      eraser-row sliders already show a value chip on the right).
10 *      Mobile: every slider in the editor gets a bubble.
11 *   3. Click the value chip to type a number directly — replaces
12 *      the span with an inline input until blur/Enter.
13 *
14 * Wired ONCE on editor open; the listeners stay alive for the whole
15 * session via state.container delegation.
16 *
17 * @param {{
18 *   registerDocClickAway: (handler: (e: Event) => void) => void,
19 * }} deps
20 */
21import { state } from './state.js';
22
23export function wireSliderUx({ registerDocClickAway }) {
24  const container = state.container;
25  if (!container) return;
26
27  // ── Floating bubble ──
28  const sliderBubble = document.createElement('div');
29  sliderBubble.className = 'ge-slider-bubble';
30  sliderBubble.hidden = true;
31  let sliderBubbleSlider = null;
32
33  // Find the container row for any slider — works for ge-eraser-row
34  // sliders AND the layer-opacity slider on each layer item.
35  function bubbleRowFor(slider) {
36    return slider.closest('.ge-eraser-row, .ge-layer-item, .ge-control-row, .ge-adj-row');
37  }
38  function bubbleText(slider) {
39    const row = bubbleRowFor(slider);
40    // Pulled-out value chip (after the slider) wins; fall back to
41    // the various `<label> <span>` styles used across the editor.
42    const chip = row?.querySelector('.ge-slider-value')
43      || row?.querySelector('label > span[id$="-label"]')
44      || row?.querySelector('label > .ge-size-label')
45      || row?.querySelector('.ge-adj-value');
46    if (chip) return chip.textContent;
47    if (slider.classList.contains('ge-layer-opacity')) {
48      return Math.round(parseFloat(slider.value)) + '%';
49    }
50    return slider.value;
51  }
52  function bubblePos(slider, cursorX) {
53    // Bubble is fixed-positioned on document.body so it escapes any
54    // overflow:hidden / overflow:auto on the row's ancestors. The
55    // bubble's X is CLAMPED to the slider's track so it can't follow
56    // a finger that drags way past either end.
57    const sliderRect = slider.getBoundingClientRect();
58    const minX = sliderRect.left + 8;
59    const maxX = sliderRect.right - 8;
60    const x = Math.max(minX, Math.min(maxX, cursorX));
61    sliderBubble.style.left = x + 'px';
62    sliderBubble.style.top  = (sliderRect.top - 8) + 'px';
63  }
64  function showSliderBubble(slider, e) {
65    if (sliderBubble.parentElement !== document.body) document.body.appendChild(sliderBubble);
66    sliderBubble.textContent = bubbleText(slider);
67    bubblePos(slider, e ? e.clientX : slider.getBoundingClientRect().left + slider.offsetWidth / 2);
68    sliderBubble.hidden = false;
69    sliderBubble.classList.add('visible');
70    sliderBubbleSlider = slider;
71  }
72  function hideSliderBubble() {
73    sliderBubble.classList.remove('visible');
74    sliderBubble.hidden = true;
75    sliderBubbleSlider = null;
76  }
77
78  const slidingTimers = new WeakMap();
79  // Desktop: only the layer-opacity slider gets the bubble (eraser-
80  // rows have their own chip). Mobile: every slider gets one.
81  const isMobileSliders = window.matchMedia('(max-width: 820px)').matches;
82  const SLIDER_SEL = isMobileSliders
83    ? '.ge-layer-opacity, .ge-eraser-row input[type="range"], .ge-control-row input[type="range"], .ge-adj-row input[type="range"]'
84    : '.ge-layer-opacity';
85
86  container.addEventListener('pointerdown', (e) => {
87    const slider = e.target.closest(SLIDER_SEL);
88    if (!slider) return;
89    const t = slidingTimers.get(slider);
90    if (t) { clearTimeout(t); slidingTimers.delete(slider); }
91    slider.classList.add('is-using');
92    showSliderBubble(slider, e);
93    // Compensate for the leftward-expanding eraser sliders so the
94    // thumb lands at the cursor's X on the new (wider) track. Layer-
95    // opacity doesn't shift left when it grows, so it uses the
96    // browser default.
97    if (slider.matches('.ge-eraser-row input[type="range"]')) {
98      const rect = slider.getBoundingClientRect();
99      const valFrac = Math.max(0, Math.min(1, 1 - (rect.right - e.clientX) / 140));
100      const min = parseFloat(slider.min) || 0;
101      const max = parseFloat(slider.max) || 100;
102      const step = parseFloat(slider.step) || 1;
103      const raw = min + valFrac * (max - min);
104      const stepped = Math.round(raw / step) * step;
105      requestAnimationFrame(() => {
106        slider.value = String(stepped);
107        slider.dispatchEvent(new Event('input', { bubbles: true }));
108        sliderBubble.textContent = bubbleText(slider);
109      });
110    } else {
111      requestAnimationFrame(() => {
112        sliderBubble.textContent = bubbleText(slider);
113      });
114    }
115  }, true);
116  document.addEventListener('pointermove', (e) => {
117    if (!sliderBubbleSlider) return;
118    bubblePos(sliderBubbleSlider, e.clientX);
119    sliderBubble.textContent = bubbleText(sliderBubbleSlider);
120  });
121  const scheduleSliderRelease = (slider) => {
122    if (!slider) return;
123    const old = slidingTimers.get(slider);
124    if (old) clearTimeout(old);
125    const t = setTimeout(() => {
126      slider.classList.remove('is-using');
127      slidingTimers.delete(slider);
128    }, 500);
129    slidingTimers.set(slider, t);
130  };
131  document.addEventListener('pointerup', () => {
132    container.querySelectorAll('input[type="range"].is-using').forEach(scheduleSliderRelease);
133    hideSliderBubble();
134  });
135
136  // ── Click value chip to type a number ──
137  // Replaces the chip with a tiny inline input until blur/Enter,
138  // then writes back to the slider and dispatches `input` so
139  // previews react. Matches the legacy chip AND the pulled-out
140  // `.ge-slider-value` chip so every slider row in the editor is
141  // click-to-type editable.
142  registerDocClickAway((e) => {
143    const chip = e.target.closest(
144      '.ge-eraser-row .ge-slider-value, ' +
145      '.ge-eraser-row label > span[id$="-label"], ' +
146      '.ge-eraser-row > span[id$="-label"], ' +
147      '.ge-adj-row .ge-adj-value'
148    );
149    if (!chip) return;
150    const row = chip.closest('.ge-eraser-row, .ge-adj-row');
151    const slider = row?.querySelector('input[type="range"]');
152    if (!slider) return;
153    e.preventDefault();
154    e.stopPropagation();
155    const numeric = (slider.value ?? '').toString();
156    const inp = document.createElement('input');
157    inp.type = 'text';
158    inp.value = numeric;
159    inp.className = 'ge-slider-edit';
160    chip.style.visibility = 'hidden';
161    row.appendChild(inp);
162    // Position the input over where the chip sits.
163    const crect = chip.getBoundingClientRect();
164    const rrect = row.getBoundingClientRect();
165    inp.style.left = (crect.left - rrect.left) + 'px';
166    inp.style.top = (crect.top - rrect.top - 1) + 'px';
167    inp.style.width = Math.max(40, crect.width + 8) + 'px';
168    inp.focus();
169    inp.select();
170    const commit = () => {
171      const v = parseFloat(inp.value);
172      if (!Number.isNaN(v)) {
173        const min = parseFloat(slider.min) || 0;
174        const max = parseFloat(slider.max) || 100;
175        const clamped = Math.max(min, Math.min(max, v));
176        slider.value = String(clamped);
177        slider.dispatchEvent(new Event('input', { bubbles: true }));
178      }
179      cleanup();
180    };
181    const cleanup = () => {
182      inp.remove();
183      chip.style.visibility = '';
184    };
185    inp.addEventListener('blur', commit);
186    inp.addEventListener('keydown', (ev) => {
187      if (ev.key === 'Enter') { ev.preventDefault(); commit(); }
188      if (ev.key === 'Escape') { ev.preventDefault(); cleanup(); }
189    });
190  });
191}

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.