Spaces:
Paused
Paused
| # """ | |
| # slider Layout | |
| # """ | |
| # # Needed for the fall-back radio layout | |
| # from potato.ai.ai_help_wrapper import get_ai_wrapper, get_dynamic_ai_help | |
| # from .radio import generate_radio_layout | |
| # from .identifier_utils import ( | |
| # safe_generate_layout, | |
| # generate_element_identifier, | |
| # escape_html_content | |
| # ) | |
| # def test_and_get(key, d): | |
| # val = d[key] | |
| # try: | |
| # return int(val) | |
| # except: | |
| # raise Exception( | |
| # 'Slider scale %s\'s value for "%s" is not an int' % (d["name"], key) | |
| # ) | |
| # def generate_slider_layout(annotation_scheme): | |
| # """ | |
| # Generate HTML for a slider input interface. | |
| # Args: | |
| # annotation_scheme (dict): Configuration including: | |
| # - name: Schema identifier | |
| # - description: Display description | |
| # - starting_value: Initial slider value | |
| # - min_value: Minimum allowed value | |
| # - max_value: Maximum allowed value | |
| # - show_labels: Whether to show min/max labels | |
| # - labels: If present, fall back to radio layout | |
| # Returns: | |
| # tuple: (html_string, key_bindings) | |
| # html_string: Complete HTML for the slider interface | |
| # key_bindings: Empty list (no keyboard shortcuts) | |
| # """ | |
| # return safe_generate_layout(annotation_scheme, generate_slider_layout_internal) | |
| # def generate_slider_layout_internal(annotation_scheme): | |
| # from .identifier_utils import escape_html_content, generate_element_identifier | |
| # if "labels" in annotation_scheme: | |
| # return generate_radio_layout(annotation_scheme, horizontal=False) | |
| # for required in ["starting_value", "min_value", "max_value"]: | |
| # if required not in annotation_scheme: | |
| # raise Exception( | |
| # f'Slider scale for "{annotation_scheme["name"]}" did not include {required}' | |
| # ) | |
| # min_value = test_and_get("min_value", annotation_scheme) | |
| # max_value = test_and_get("max_value", annotation_scheme) | |
| # starting_value = test_and_get("starting_value", annotation_scheme) | |
| # if min_value >= max_value: | |
| # raise Exception( | |
| # f'Slider scale for "{annotation_scheme["name"]}" must have minimum value < max value ({min_value} >= {max_value})' | |
| # ) | |
| # show_labels = annotation_scheme.get("show_labels", True) | |
| # min_label = str(min_value) if show_labels else '' | |
| # max_label = str(max_value) if show_labels else '' | |
| # identifiers = generate_element_identifier(annotation_scheme["name"], "slider", "range") | |
| # # Get step from annotation_scheme or default to 5 | |
| # step_value = annotation_scheme.get("step", 1) | |
| # print(step_value) | |
| # max_tick = annotation_scheme.get("maxTick", 8) | |
| # print("max_tick") | |
| # print(max_tick) | |
| # schematic = f""" | |
| # <form id="{identifiers['schema']}" class="annotation-form slider" action="javascript:void(0)" data-annotation-id="{annotation_scheme.get("annotation_id", "")}" > | |
| # {get_ai_wrapper()} | |
| # <fieldset schema="{identifiers['schema']}"> | |
| # <legend class="custom-slider-title">{annotation_scheme['description']}</legend> | |
| # <div class="custom-slider-container" id="customSlider_{identifiers['id']}" tabindex="0"> | |
| # <!-- Hidden actual input for form submission --> | |
| # <input type="range" | |
| # min="{min_value}" | |
| # max="{max_value}" | |
| # step="{step_value}" | |
| # value="{starting_value}" | |
| # class="custom-slider-input annotation-input" | |
| # onclick="registerAnnotation(this);" | |
| # oninput="updateCustomSlider(this);" | |
| # label_name="{identifiers['label_name']}" | |
| # name="{identifiers['name']}" | |
| # id="{identifiers['id']}" | |
| # schema="{identifiers['schema']}"> | |
| # <!-- Custom visual elements --> | |
| # <div class="custom-slider-track"> | |
| # <div class="custom-slider-track-active" id="sliderTrackActive_{identifiers['id']}"></div> | |
| # <div class="custom-slider-thumb" id="sliderThumb_{identifiers['id']}"></div> | |
| # </div> | |
| # <div class="custom-slider-ticks" id="sliderTicks_{identifiers['id']}"></div> | |
| # </div> | |
| # </fieldset> | |
| # </form> | |
| # <script> | |
| # // Initialize the slider on page load | |
| # (function() {{ | |
| # const sliderId = "{identifiers['id']}"; | |
| # const sliderInput = document.getElementById(sliderId); | |
| # if (!sliderInput) return; | |
| # // Set up the slider initially | |
| # setupCustomSlider(sliderInput); | |
| # // Update positions on input change | |
| # sliderInput.addEventListener('input', function() {{ | |
| # updateCustomSlider(this); | |
| # }}); | |
| # }})(); | |
| # function setupCustomSlider(sliderInput) {{ | |
| # const sliderId = sliderInput.id; | |
| # const min = parseInt(sliderInput.min); | |
| # const max = parseInt(sliderInput.max); | |
| # const step = parseInt(sliderInput.step) || 1; | |
| # // Get references to custom elements | |
| # const container = document.getElementById('customSlider_' + sliderId); | |
| # const sliderThumb = document.getElementById('sliderThumb_' + sliderId); | |
| # const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId); | |
| # const sliderTicks = document.getElementById('sliderTicks_' + sliderId); | |
| # if (!container || !sliderThumb || !sliderTrackActive || !sliderTicks) return; | |
| # // Initialize slider position | |
| # updateSliderPosition({starting_value}, min, max, sliderThumb, sliderTrackActive); | |
| # // Create tick marks | |
| # createTicks(min, max, step, sliderTicks); | |
| # // Add direct click handling for better UX | |
| # container.addEventListener('click', function(e) {{ | |
| # // Skip if the click is on the input or thumb (to prevent jumps during dragging) | |
| # if (e.target === sliderThumb || e.target === sliderInput) return; | |
| # // Get click position relative to the track | |
| # const rect = container.getBoundingClientRect(); | |
| # const clickPosition = e.clientX - rect.left; | |
| # const percentClicked = (clickPosition / rect.width) * 100; | |
| # // Calculate the new value based on the click position | |
| # let newValue = min + (percentClicked / 100) * (max - min); | |
| # // Snap to the nearest step | |
| # newValue = Math.round(newValue / step) * step; | |
| # // Ensure the value is within bounds | |
| # newValue = Math.max(min, Math.min(max, newValue)); | |
| # // Update the input value | |
| # sliderInput.value = newValue; | |
| # // Update the visual position | |
| # updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive); | |
| # // Trigger change event for any listeners | |
| # const event = new Event('input', {{ bubbles: true }}); | |
| # sliderInput.dispatchEvent(event); | |
| # // If registerAnnotation exists, call it | |
| # if (typeof registerAnnotation === 'function') {{ | |
| # registerAnnotation(sliderInput); | |
| # }} | |
| # }}); | |
| # // Add keyboard control | |
| # container.addEventListener('keydown', function(e) {{ | |
| # // Only process arrow keys | |
| # if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; | |
| # // Prevent default to avoid page scrolling | |
| # e.preventDefault(); | |
| # // Get current value | |
| # let currentValue = parseFloat(sliderInput.value); | |
| # // Update value based on arrow key | |
| # if (e.key === 'ArrowDown') {{ | |
| # currentValue = Math.max(min, currentValue - step); | |
| # }} else if (e.key === 'ArrowUp') {{ | |
| # currentValue = Math.min(max, currentValue + step); | |
| # }} | |
| # // Update the input value | |
| # sliderInput.value = currentValue; | |
| # // Update the visual position | |
| # updateSliderPosition(currentValue, min, max, sliderThumb, sliderTrackActive); | |
| # // Trigger change event for any listeners | |
| # const event = new Event('input', {{ bubbles: true }}); | |
| # sliderInput.dispatchEvent(event); | |
| # // If registerAnnotation exists, call it | |
| # if (typeof registerAnnotation === 'function') {{ | |
| # registerAnnotation(sliderInput); | |
| # }} | |
| # }}); | |
| # // Add focus styles for keyboard users | |
| # container.addEventListener('focus', function() {{ | |
| # container.classList.add('focus'); | |
| # }}); | |
| # container.addEventListener('blur', function() {{ | |
| # container.classList.remove('focus'); | |
| # }}); | |
| # // Add drag and hold functionality | |
| # let isDragging = false; | |
| # // Mouse events for drag and hold | |
| # sliderThumb.addEventListener('mousedown', function(e) {{ | |
| # isDragging = true; | |
| # e.preventDefault(); // Prevent text selection during drag | |
| # // Add event listeners for drag and release | |
| # document.addEventListener('mousemove', handleMouseDrag); | |
| # document.addEventListener('mouseup', stopDrag); | |
| # // Add a class to indicate dragging state | |
| # sliderThumb.classList.add('dragging'); | |
| # }}); | |
| # // Touch events for mobile devices | |
| # sliderThumb.addEventListener('touchstart', function(e) {{ | |
| # isDragging = true; | |
| # e.preventDefault(); // Prevent scrolling during drag | |
| # // Add event listeners for drag and release | |
| # document.addEventListener('touchmove', handleTouchDrag); | |
| # document.addEventListener('touchend', stopDrag); | |
| # document.addEventListener('touchcancel', stopDrag); | |
| # // Add a class to indicate dragging state | |
| # sliderThumb.classList.add('dragging'); | |
| # }}); | |
| # // Handle mouse drag | |
| # function handleMouseDrag(e) {{ | |
| # if (!isDragging) return; | |
| # // Get the slider's position and dimensions | |
| # const rect = container.getBoundingClientRect(); | |
| # // Calculate position within the slider (constrain to slider width) | |
| # let position = e.clientX - rect.left; | |
| # position = Math.max(0, Math.min(position, rect.width)); | |
| # // Calculate value and update slider | |
| # updateSliderFromPosition(position, rect.width); | |
| # }} | |
| # // Handle touch drag | |
| # function handleTouchDrag(e) {{ | |
| # if (!isDragging || !e.touches[0]) return; | |
| # const touch = e.touches[0]; | |
| # const rect = container.getBoundingClientRect(); | |
| # // Calculate position within the slider (constrain to slider width) | |
| # let position = touch.clientX - rect.left; | |
| # position = Math.max(0, Math.min(position, rect.width)); | |
| # // Calculate value and update slider | |
| # updateSliderFromPosition(position, rect.width); | |
| # }} | |
| # // Helper function to update slider from mouse/touch position | |
| # function updateSliderFromPosition(position, width) {{ | |
| # // Convert position to a percentage | |
| # const percent = (position / width) * 100; | |
| # // Calculate the value based on percentage | |
| # let newValue = min + (percent / 100) * (max - min); | |
| # // Snap to the nearest step | |
| # newValue = Math.round(newValue / step) * step; | |
| # // Ensure the value is within bounds | |
| # newValue = Math.max(min, Math.min(max, newValue)); | |
| # // Update the input value | |
| # sliderInput.value = newValue; | |
| # // Update the visual position | |
| # updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive); | |
| # // Trigger change event | |
| # const event = new Event('input', {{ bubbles: true }}); | |
| # sliderInput.dispatchEvent(event); | |
| # // If registerAnnotation exists, call it | |
| # if (typeof registerAnnotation === 'function') {{ | |
| # registerAnnotation(sliderInput); | |
| # }} | |
| # }} | |
| # // Stop dragging | |
| # function stopDrag() {{ | |
| # if (!isDragging) return; | |
| # isDragging = false; | |
| # sliderThumb.classList.remove('dragging'); | |
| # // Remove the event listeners | |
| # document.removeEventListener('mousemove', handleMouseDrag); | |
| # document.removeEventListener('touchmove', handleTouchDrag); | |
| # document.removeEventListener('mouseup', stopDrag); | |
| # document.removeEventListener('touchend', stopDrag); | |
| # document.removeEventListener('touchcancel', stopDrag); | |
| # }} | |
| # }} | |
| # function updateCustomSlider(sliderInput) {{ | |
| # const sliderId = sliderInput.id; | |
| # const min = parseInt(sliderInput.min); | |
| # const max = parseInt(sliderInput.max); | |
| # const sliderThumb = document.getElementById('sliderThumb_' + sliderId); | |
| # const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId); | |
| # if (!sliderThumb || !sliderTrackActive) return; | |
| # updateSliderPosition(sliderInput.value, min, max, sliderThumb, sliderTrackActive); | |
| # }} | |
| # function updateSliderPosition(value, min, max, thumbElement, trackElement) {{ | |
| # const percent = ((value - min) / (max - min)) * 100; | |
| # thumbElement.style.left = `${{percent}}%`; | |
| # trackElement.style.width = `${{percent}}%`; | |
| # }} | |
| # function createTicks(min, max, step, tickContainer) {{ | |
| # // Clear existing ticks | |
| # tickContainer.innerHTML = ''; | |
| # // Calculate optimal ticks | |
| # const ticks = calculateOptimalTicks(min, max, step); | |
| # ticks.forEach(tick => {{ | |
| # const percent = ((tick.value - min) / (max - min)) * 100; | |
| # const tickElement = document.createElement('div'); | |
| # tickElement.className = 'custom-slider-tick'; | |
| # tickElement.style.left = `${{percent}}%`; | |
| # const tickMark = document.createElement('div'); | |
| # tickMark.className = tick.showLabel ? 'custom-slider-tick-mark major' : 'custom-slider-tick-mark'; | |
| # tickElement.appendChild(tickMark); | |
| # if (tick.showLabel) {{ | |
| # const tickLabel = document.createElement('div'); | |
| # tickLabel.className = 'custom-slider-tick-label'; | |
| # tickLabel.textContent = tick.value; | |
| # tickElement.appendChild(tickLabel); | |
| # }} | |
| # tickContainer.appendChild(tickElement); | |
| # }}); | |
| # }} | |
| # function calculateOptimalTicks(min, max, sliderStep, maxTicks = {max_tick}) {{ | |
| # const range = max - min; | |
| # // Generate all possible slider positions based on the step | |
| # const possibleValues = []; | |
| # for (let value = min; value <= max; value += sliderStep) {{ | |
| # possibleValues.push(Math.round(value)); | |
| # }} | |
| # // If we have few enough values, show them all | |
| # if (possibleValues.length <= maxTicks) {{ | |
| # return possibleValues.map(value => ({{ value, showLabel: true }})); | |
| # }} | |
| # // Calculate a "nice" interval for major ticks | |
| # const targetInterval = range / (maxTicks - 1); | |
| # // Find nice intervals: 1, 2, 5, 10, 20, 50, 100, etc. | |
| # const candidates = []; | |
| # for (let magnitude = 1; magnitude <= range; magnitude *= 10) {{ | |
| # candidates.push(magnitude); // 1, 10, 100... | |
| # candidates.push(2 * magnitude); // 2, 20, 200... | |
| # candidates.push(5 * magnitude); // 5, 50, 500... | |
| # }} | |
| # // Choose the candidate closest to our target | |
| # let bestInterval = candidates[0]; | |
| # let bestDiff = Math.abs(candidates[0] - targetInterval); | |
| # for (const candidate of candidates) {{ | |
| # const diff = Math.abs(candidate - targetInterval); | |
| # if (diff < bestDiff) {{ | |
| # bestInterval = candidate; | |
| # bestDiff = diff; | |
| # }} | |
| # }} | |
| # // Generate major ticks using the best interval | |
| # const majorTicks = []; | |
| # // Always start with min | |
| # majorTicks.push({{ value: min, showLabel: true }}); | |
| # // Add ticks at nice intervals | |
| # for (let tickValue = bestInterval; tickValue < max; tickValue += bestInterval) {{ | |
| # if (tickValue > min) {{ | |
| # majorTicks.push({{ value: tickValue, showLabel: true }}); | |
| # }} | |
| # }} | |
| # // Always end with max (if it's not the same as the last tick) | |
| # if (majorTicks[majorTicks.length - 1].value !== max) {{ | |
| # majorTicks.push({{ value: max, showLabel: true }}); | |
| # }} | |
| # // Add minor ticks for values between major ticks | |
| # const allTicks = [...majorTicks]; | |
| # // Only add minor ticks if there's reasonable spacing | |
| # if (majorTicks.length > 1) {{ | |
| # const majorSpacing = bestInterval; | |
| # if (majorSpacing > sliderStep * 2) {{ | |
| # possibleValues.forEach(value => {{ | |
| # if (!allTicks.some(t => t.value === value)) {{ | |
| # allTicks.push({{ value, showLabel: false }}); | |
| # }} | |
| # }}); | |
| # }} | |
| # }} | |
| # return allTicks.sort((a, b) => a.value - b.value); | |
| # }} | |
| # </script> | |
| # """ | |
| # key_bindings = [] | |
| # return schematic, key_bindings | |
| """ | |
| slider Layout | |
| """ | |
| import logging | |
| # Needed for the fall-back radio layout | |
| from potato.ai.ai_help_wrapper import get_ai_wrapper, get_dynamic_ai_help | |
| from .radio import generate_radio_layout | |
| from .identifier_utils import ( | |
| safe_generate_layout, | |
| generate_element_identifier, | |
| escape_html_content | |
| ) | |
| logger = logging.getLogger(__name__) | |
| def test_and_get(key, d): | |
| val = d[key] | |
| try: | |
| return int(val) | |
| except: | |
| raise Exception( | |
| 'Slider scale %s\'s value for "%s" is not an int' % (d["name"], key) | |
| ) | |
| def generate_slider_layout(annotation_scheme): | |
| """ | |
| Generate HTML for a slider input interface. | |
| Args: | |
| annotation_scheme (dict): Configuration including: | |
| - name: Schema identifier | |
| - description: Display description | |
| - starting_value: Initial slider value | |
| - min_value: Minimum allowed value | |
| - max_value: Maximum allowed value | |
| - show_labels: Whether to show min/max labels | |
| - labels: If present, fall back to radio layout | |
| Returns: | |
| tuple: (html_string, key_bindings) | |
| html_string: Complete HTML for the slider interface | |
| key_bindings: Empty list (no keyboard shortcuts) | |
| """ | |
| return safe_generate_layout(annotation_scheme, generate_slider_layout_internal) | |
| def generate_slider_layout_internal(annotation_scheme): | |
| from .identifier_utils import escape_html_content, generate_element_identifier, generate_validation_attribute | |
| if "labels" in annotation_scheme: | |
| return generate_radio_layout(annotation_scheme, horizontal=False) | |
| for required in ["starting_value", "min_value", "max_value"]: | |
| if required not in annotation_scheme: | |
| raise Exception( | |
| f'Slider scale for "{annotation_scheme["name"]}" did not include {required}' | |
| ) | |
| min_value = test_and_get("min_value", annotation_scheme) | |
| max_value = test_and_get("max_value", annotation_scheme) | |
| starting_value = test_and_get("starting_value", annotation_scheme) | |
| if min_value >= max_value: | |
| raise Exception( | |
| f'Slider scale for "{annotation_scheme["name"]}" must have minimum value < max value ({min_value} >= {max_value})' | |
| ) | |
| show_labels = annotation_scheme.get("show_labels", True) | |
| min_label = str(min_value) if show_labels else '' | |
| max_label = str(max_value) if show_labels else '' | |
| identifiers = generate_element_identifier(annotation_scheme["name"], "slider", "range") | |
| validation = generate_validation_attribute(annotation_scheme) | |
| # Get step from annotation_scheme or default to 1 | |
| step_value = annotation_scheme.get("step", 1) | |
| max_tick = annotation_scheme.get("maxTick", 8) | |
| schematic = f""" | |
| <form id="{identifiers['schema']}" class="annotation-form slider" action="javascript:void(0)" data-annotation-id="{escape_html_content(str(annotation_scheme.get("annotation_id", "")))}" > | |
| {get_ai_wrapper()} | |
| <fieldset schema="{identifiers['schema']}"> | |
| <legend class="custom-slider-title">{annotation_scheme['description']}</legend> | |
| <div class="custom-slider-container" id="customSlider_{identifiers['id']}" tabindex="0"> | |
| <!-- Hidden actual input for form submission --> | |
| <input type="range" | |
| min="{min_value}" | |
| max="{max_value}" | |
| step="{step_value}" | |
| value="{starting_value}" | |
| class="custom-slider-input annotation-input" | |
| onclick="registerAnnotation(this);" | |
| oninput="updateCustomSlider(this);" | |
| label_name="{identifiers['label_name']}" | |
| name="{identifiers['name']}" | |
| id="{identifiers['id']}" | |
| schema="{identifiers['schema']}" | |
| validation="{validation}"> | |
| <!-- Custom visual elements --> | |
| <div class="custom-slider-track"> | |
| <div class="custom-slider-track-active" id="sliderTrackActive_{identifiers['id']}"></div> | |
| <div class="custom-slider-thumb" id="sliderThumb_{identifiers['id']}"> | |
| <!-- Tooltip attached directly to thumb --> | |
| <div class="slider-tooltip" id="sliderTooltip_{identifiers['id']}">{starting_value}</div> | |
| </div> | |
| </div> | |
| <div class="custom-slider-ticks" id="sliderTicks_{identifiers['id']}"></div> | |
| </div> | |
| </fieldset> | |
| </form> | |
| <script> | |
| // Initialize the slider on page load | |
| (function() {{ | |
| const sliderId = "{identifiers['id']}"; | |
| const sliderInput = document.getElementById(sliderId); | |
| if (!sliderInput) return; | |
| // Set up the slider initially | |
| setupCustomSlider(sliderInput); | |
| // Update positions on input change | |
| sliderInput.addEventListener('input', function() {{ | |
| updateCustomSlider(this); | |
| }}); | |
| }})(); | |
| function setupCustomSlider(sliderInput) {{ | |
| const sliderId = sliderInput.id; | |
| const min = parseInt(sliderInput.min); | |
| const max = parseInt(sliderInput.max); | |
| const step = parseInt(sliderInput.step) || 1; | |
| // Get references to custom elements | |
| const container = document.getElementById('customSlider_' + sliderId); | |
| const sliderThumb = document.getElementById('sliderThumb_' + sliderId); | |
| const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId); | |
| const sliderTicks = document.getElementById('sliderTicks_' + sliderId); | |
| const tooltip = document.getElementById('sliderTooltip_' + sliderId); | |
| if (!container || !sliderThumb || !sliderTrackActive || !sliderTicks || !tooltip) return; | |
| // Initialize slider position | |
| updateSliderPosition({starting_value}, min, max, sliderThumb, sliderTrackActive, tooltip); | |
| // Create tick marks | |
| createTicks(min, max, step, sliderTicks); | |
| container.addEventListener('click', function(e) {{ | |
| // Skip if the click is on the input or thumb (to prevent jumps during dragging) | |
| if (e.target === sliderThumb || e.target === sliderInput) return; | |
| // Get click position relative to the track | |
| const rect = container.getBoundingClientRect(); | |
| const clickPosition = e.clientX - rect.left; | |
| const percentClicked = (clickPosition / rect.width) * 100; | |
| // Calculate the new value based on the click position | |
| let newValue = min + (percentClicked / 100) * (max - min); | |
| // Snap to the nearest step | |
| newValue = Math.round(newValue / step) * step; | |
| // Ensure the value is within bounds | |
| newValue = Math.max(min, Math.min(max, newValue)); | |
| // Update the input value | |
| sliderInput.value = newValue; | |
| // Update the visual position | |
| updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive, tooltip); | |
| // Trigger change event for any listeners | |
| const event = new Event('input', {{ bubbles: true }}); | |
| sliderInput.dispatchEvent(event); | |
| // If registerAnnotation exists, call it | |
| if (typeof registerAnnotation === 'function') {{ | |
| registerAnnotation(sliderInput); | |
| }} | |
| }}); | |
| // Add keyboard control | |
| container.addEventListener('keydown', function(e) {{ | |
| // Only process arrow keys | |
| if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; | |
| // Prevent default to avoid page scrolling | |
| e.preventDefault(); | |
| // Get current value | |
| let currentValue = parseFloat(sliderInput.value); | |
| // Update value based on arrow key | |
| if (e.key === 'ArrowDown') {{ | |
| currentValue = Math.max(min, currentValue - step); | |
| }} else if (e.key === 'ArrowUp') {{ | |
| currentValue = Math.min(max, currentValue + step); | |
| }} | |
| // Update the input value | |
| sliderInput.value = currentValue; | |
| // Update the visual position | |
| updateSliderPosition(currentValue, min, max, sliderThumb, sliderTrackActive, tooltip); | |
| // Trigger change event for any listeners | |
| const event = new Event('input', {{ bubbles: true }}); | |
| sliderInput.dispatchEvent(event); | |
| // If registerAnnotation exists, call it | |
| if (typeof registerAnnotation === 'function') {{ | |
| registerAnnotation(sliderInput); | |
| }} | |
| }}); | |
| // Add focus styles for keyboard users | |
| container.addEventListener('focus', function() {{ | |
| container.classList.add('focus'); | |
| }}); | |
| container.addEventListener('blur', function() {{ | |
| container.classList.remove('focus'); | |
| }}); | |
| // Add drag and hold functionality | |
| let isDragging = false; | |
| // Mouse events for drag and hold | |
| sliderThumb.addEventListener('mousedown', function(e) {{ | |
| isDragging = true; | |
| e.preventDefault(); // Prevent text selection during drag | |
| // Show tooltip during drag | |
| sliderThumb.classList.add('dragging'); | |
| // Add event listeners for drag and release | |
| document.addEventListener('mousemove', handleMouseDrag); | |
| document.addEventListener('mouseup', stopDrag); | |
| }}); | |
| // Touch events for mobile devices | |
| sliderThumb.addEventListener('touchstart', function(e) {{ | |
| isDragging = true; | |
| e.preventDefault(); // Prevent scrolling during drag | |
| // Show tooltip during drag | |
| sliderThumb.classList.add('dragging'); | |
| // Add event listeners for drag and release | |
| document.addEventListener('touchmove', handleTouchDrag); | |
| document.addEventListener('touchend', stopDrag); | |
| document.addEventListener('touchcancel', stopDrag); | |
| }}); | |
| // Handle mouse drag | |
| function handleMouseDrag(e) {{ | |
| if (!isDragging) return; | |
| // Get the slider's position and dimensions | |
| const rect = container.getBoundingClientRect(); | |
| // Calculate position within the slider (constrain to slider width) | |
| let position = e.clientX - rect.left; | |
| position = Math.max(0, Math.min(position, rect.width)); | |
| // Calculate value and update slider | |
| updateSliderFromPosition(position, rect.width); | |
| }} | |
| // Handle touch drag | |
| function handleTouchDrag(e) {{ | |
| if (!isDragging || !e.touches[0]) return; | |
| const touch = e.touches[0]; | |
| const rect = container.getBoundingClientRect(); | |
| // Calculate position within the slider (constrain to slider width) | |
| let position = touch.clientX - rect.left; | |
| position = Math.max(0, Math.min(position, rect.width)); | |
| // Calculate value and update slider | |
| updateSliderFromPosition(position, rect.width); | |
| }} | |
| // Helper function to update slider from mouse/touch position | |
| function updateSliderFromPosition(position, width) {{ | |
| // Convert position to a percentage | |
| const percent = (position / width) * 100; | |
| // Calculate the value based on percentage | |
| let newValue = min + (percent / 100) * (max - min); | |
| // Snap to the nearest step | |
| newValue = Math.round(newValue / step) * step; | |
| // Ensure the value is within bounds | |
| newValue = Math.max(min, Math.min(max, newValue)); | |
| // Update the input value | |
| sliderInput.value = newValue; | |
| // Update the visual position | |
| updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive, tooltip); | |
| // Trigger change event | |
| const event = new Event('input', {{ bubbles: true }}); | |
| sliderInput.dispatchEvent(event); | |
| // If registerAnnotation exists, call it | |
| if (typeof registerAnnotation === 'function') {{ | |
| registerAnnotation(sliderInput); | |
| }} | |
| }} | |
| // Stop dragging | |
| function stopDrag() {{ | |
| if (!isDragging) return; | |
| isDragging = false; | |
| sliderThumb.classList.remove('dragging'); | |
| // Remove the event listeners | |
| document.removeEventListener('mousemove', handleMouseDrag); | |
| document.removeEventListener('touchmove', handleTouchDrag); | |
| document.removeEventListener('mouseup', stopDrag); | |
| document.removeEventListener('touchend', stopDrag); | |
| document.removeEventListener('touchcancel', stopDrag); | |
| }} | |
| }} | |
| function updateCustomSlider(sliderInput) {{ | |
| const sliderId = sliderInput.id; | |
| const min = parseInt(sliderInput.min); | |
| const max = parseInt(sliderInput.max); | |
| const sliderThumb = document.getElementById('sliderThumb_' + sliderId); | |
| const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId); | |
| const tooltip = document.getElementById('sliderTooltip_' + sliderId); | |
| if (!sliderThumb || !sliderTrackActive || !tooltip) return; | |
| updateSliderPosition(sliderInput.value, min, max, sliderThumb, sliderTrackActive, tooltip); | |
| }} | |
| function updateSliderPosition(value, min, max, thumbElement, trackElement, tooltipElement) {{ | |
| const percent = ((value - min) / (max - min)) * 100; | |
| thumbElement.style.left = `${{percent}}%`; | |
| trackElement.style.width = `${{percent}}%`; | |
| // Update tooltip content only - it moves with the thumb automatically | |
| if (tooltipElement) {{ | |
| tooltipElement.textContent = value; | |
| }} | |
| }} | |
| function createTicks(min, max, step, tickContainer) {{ | |
| // Clear existing ticks | |
| tickContainer.innerHTML = ''; | |
| // Calculate optimal ticks | |
| const ticks = calculateOptimalTicks(min, max, step); | |
| ticks.forEach(tick => {{ | |
| const percent = ((tick.value - min) / (max - min)) * 100; | |
| const tickElement = document.createElement('div'); | |
| tickElement.className = 'custom-slider-tick'; | |
| tickElement.style.left = `${{percent}}%`; | |
| const tickMark = document.createElement('div'); | |
| tickMark.className = tick.showLabel ? 'custom-slider-tick-mark major' : 'custom-slider-tick-mark'; | |
| tickElement.appendChild(tickMark); | |
| if (tick.showLabel) {{ | |
| const tickLabel = document.createElement('div'); | |
| tickLabel.className = 'custom-slider-tick-label'; | |
| tickLabel.textContent = tick.value; | |
| tickElement.appendChild(tickLabel); | |
| }} | |
| tickContainer.appendChild(tickElement); | |
| }}); | |
| }} | |
| function calculateOptimalTicks(min, max, sliderStep, maxTicks = {max_tick}) {{ | |
| const range = max - min; | |
| // Generate all possible slider positions based on the step | |
| const possibleValues = []; | |
| for (let value = min; value <= max; value += sliderStep) {{ | |
| possibleValues.push(Math.round(value)); | |
| }} | |
| // If we have few enough values, show them all | |
| if (possibleValues.length <= maxTicks) {{ | |
| return possibleValues.map(value => ({{ value, showLabel: true }})); | |
| }} | |
| // Calculate a "nice" interval for major ticks | |
| const targetInterval = range / (maxTicks - 1); | |
| // Find nice intervals: 1, 2, 5, 10, 20, 50, 100, etc. | |
| const candidates = []; | |
| for (let magnitude = 1; magnitude <= range; magnitude *= 10) {{ | |
| candidates.push(magnitude); // 1, 10, 100... | |
| candidates.push(2 * magnitude); // 2, 20, 200... | |
| candidates.push(5 * magnitude); // 5, 50, 500... | |
| }} | |
| // Choose the candidate closest to our target | |
| let bestInterval = candidates[0]; | |
| let bestDiff = Math.abs(candidates[0] - targetInterval); | |
| for (const candidate of candidates) {{ | |
| const diff = Math.abs(candidate - targetInterval); | |
| if (diff < bestDiff) {{ | |
| bestInterval = candidate; | |
| bestDiff = diff; | |
| }} | |
| }} | |
| // Generate major ticks using the best interval | |
| const majorTicks = []; | |
| // Always start with min | |
| majorTicks.push({{ value: min, showLabel: true }}); | |
| // Add ticks at nice intervals | |
| for (let tickValue = bestInterval; tickValue < max; tickValue += bestInterval) {{ | |
| if (tickValue > min) {{ | |
| majorTicks.push({{ value: tickValue, showLabel: true }}); | |
| }} | |
| }} | |
| // Always end with max (if it's not the same as the last tick) | |
| if (majorTicks[majorTicks.length - 1].value !== max) {{ | |
| majorTicks.push({{ value: max, showLabel: true }}); | |
| }} | |
| // Add minor ticks for values between major ticks | |
| const allTicks = [...majorTicks]; | |
| // Only add minor ticks if there's reasonable spacing | |
| if (majorTicks.length > 1) {{ | |
| const majorSpacing = bestInterval; | |
| if (majorSpacing > sliderStep * 2) {{ | |
| possibleValues.forEach(value => {{ | |
| if (!allTicks.some(t => t.value === value)) {{ | |
| allTicks.push({{ value, showLabel: false }}); | |
| }} | |
| }}); | |
| }} | |
| }} | |
| return allTicks.sort((a, b) => a.value - b.value); | |
| }} | |
| </script> | |
| """ | |
| key_bindings = [] | |
| return schematic, key_bindings |