Skip to content

Source: lib/dom/plugins/domSmithInputRange.js

/**
 * DomSmith plugin that enhances `input type="range"` elements with touch-drag support.
 * It injects `pointerdown`, `pointermove` and `pointerup` handlers via the nodeDef object,
 * while preserving any pre-existing handlers. State is held in closures per element.
 * @module lib/dom/plugins/domSmithInputRange
 * @author   Frank Kudermann - alphanull
 * @version  1.0.0
 * @license  MIT
 */

/**
 * Hook called by DomSmith during node creation.
 * @param   {module:lib/dom/DomSmith~NodeDefinition}           nodeDef  The original node definition object.
 * @returns {module:lib/dom/DomSmith~NodeDefinition|undefined}          The patched nodeDef or undefined.
 */
function addNode(nodeDef) {

    if (!nodeDef._tag || nodeDef._tag.toLowerCase() !== 'input' || nodeDef.type !== 'range' || nodeDef.$rangeFixDisable) return;

    let isDragging = false,
        rect = null,
        hasNativeChange = false;

    const ele = nodeDef._ele,
          isVertical = nodeDef.orient === 'vertical';

    /**
     * Calculates the value of a range input based on pointer event position and orientation.
     * @param   {PointerEvent} event  The pointer event.
     * @returns {number}              The computed slider value.
     */
    function calculateSliderValue(event) {
        const raw = isVertical ? 1 - (event.clientY - rect.top) / rect.height : (event.clientX - rect.left) / rect.width,
              clamped = Math.max(0, Math.min(1, raw)),
              max = parseFloat(ele.max) || 1;
        return clamped * max;
    }

    /**
     * Appends an event handler to a nodeDef entry without overwriting existing handlers.
     * If no handler exists, the function is assigned directly.
     * If one handler exists, it is converted into an array containing both.
     * If multiple handlers already exist (as an array), the new one is appended.
     * @param {string}   eventName  The name of the event (e.g. 'pointerdown').
     * @param {Function} fn         The event handler function to append.
     */
    function appendHandler(eventName, fn) {
        if (!nodeDef[eventName]) nodeDef[eventName] = fn;
        else if (Array.isArray(nodeDef[eventName])) nodeDef[eventName].push(fn);
        else nodeDef[eventName] = [nodeDef[eventName], fn];
    }

    // Listen for native change events to avoid duplicates
    ele.addEventListener('change', () => {
        hasNativeChange = true;
    }, { passive: true });

    appendHandler('pointerdown', event => {
        if (event.pointerType !== 'touch') return;

        isDragging = true;
        hasNativeChange = false;
        rect = ele.getBoundingClientRect();

        ele.value = calculateSliderValue(event);
        ele.setPointerCapture(event.pointerId);
        ele.dispatchEvent(new Event('input', { bubbles: true }));

        event.preventDefault();
    });

    appendHandler('pointermove', event => {
        if (event.pointerType !== 'touch' || !isDragging || !rect) return;

        ele.value = calculateSliderValue(event);
        ele.dispatchEvent(new Event('input', { bubbles: true }));
        event.preventDefault();
    });

    appendHandler('pointerup', event => {
        isDragging = false;
        if (event.pointerType === 'touch') ele.releasePointerCapture(event.pointerId);

        const playerEle = ele.closest('vision-player');
        if (playerEle) playerEle.focus();

        // Use setTimeout to ensure native change event has time to fire
        setTimeout(() => {
            // Only fire change event if no native change was detected
            if (!hasNativeChange) {
                ele.dispatchEvent(new Event('change', { bubbles: true }));
            }
        }, 0);

        event.preventDefault();
    });

    return nodeDef;
}

export default {
    addNode
};