RangeSlider

一个双滑块范围选择器组件库

Acest script nu ar trebui instalat direct. Aceasta este o bibliotecă pentru alte scripturi care este inclusă prin directiva meta a // @require https://update.greasyfork.org/scripts/593075/1913652/RangeSlider.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         RangeSlider
// @namespace    https://greasyfork.org/users/1570630
// @version      1.0.0
// @description  一个双滑块范围选择器组件库
// @author       ryxel
// @license      MIT
// @grant        GM_addElement
// ==/UserScript==

var RangeSlider = (function () {
    'use strict';

    const addStyle = typeof GM_addElement === 'function'
        ? (parent, textContent, id) => GM_addElement(parent, 'style', { textContent, id })
        : (parent, textContent, id) => parent.appendChild(Object.assign(document.createElement('style'), { textContent, id }));

    class RangeSlider {
        static get styleId() {
            return 'cs-slider-style';
        }

        static get css() {
            return `
            .cs-slider-container {
                --cs-track-bg: #e4e7ed;
                --cs-progress-bg: #409eff;
                --cs-thumb-bg: #fff;
                --cs-thumb-border: #409eff;
                --cs-thumb-shadow: rgba(0, 0, 0, 0.2);
                --cs-thumb-focus-shadow: rgba(64, 158, 255, 0.3);

                position: relative;
                width: 100%;
                height: 20px;
                display: flex;
                align-items: center;
                cursor: pointer;
                user-select: none;
                touch-action: none;
                -webkit-tap-highlight-color: transparent;
            }

            .cs-slider-track {
                position: absolute;
                width: 100%;
                height: 4px;
                background-color: var(--cs-track-bg);
                border-radius: 2px;
            }

            .cs-slider-progress {
                position: absolute;
                height: 4px;
                background-color: var(--cs-progress-bg);
                border-radius: 2px;
            }

            .cs-slider-thumb {
                box-sizing: border-box;
                position: absolute;
                width: 16px;
                height: 16px;
                background-color: var(--cs-thumb-bg);
                border: 2px solid var(--cs-thumb-border);
                border-radius: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                cursor: pointer;
                box-shadow: 0 1px 3px var(--cs-thumb-shadow);
                transition: box-shadow 0.15s;
                z-index: 10;
                outline: none;
            }

            .cs-slider-thumb:focus {
                box-shadow: 0 1px 3px var(--cs-thumb-shadow), 0 0 0 3px var(--cs-thumb-focus-shadow);
            }
        `;
        }

        constructor(container, options = {}) {
            this.container = container;

            this.min = options.min !== undefined ? options.min : 0;
            this.max = options.max !== undefined ? options.max : 100;
            this.step = options.step !== undefined ? options.step : 1;
            this.range = this.max - this.min;

            this.values = [
                options.from !== undefined ? options.from : this.min,
                options.to !== undefined ? options.to : this.max
            ];

            this.target = [
                ((this.values[0] - this.min) / this.range) * 100,
                ((this.values[1] - this.min) / this.range) * 100
            ];
            this.current = [...this.target];
            this.animFrame = null;

            this.onChange = options.onChange || function() {};

            this.activeThumb = null;
            this.trackLeft = 0;
            this.trackWidth = 0;

            this.onStart = this.onStart.bind(this);
            this.onMove = this.onMove.bind(this);
            this.onEnd = this.onEnd.bind(this);
            this.render = this.render.bind(this);
            this.animate = this.animate.bind(this);

            this.initHandledKeys();
            this.injectStyle();
            this.initDOM();
            this.render();
        }

        initHandledKeys() {
            if (!RangeSlider.handledKeys) {
                RangeSlider.handledKeys = new Set([
                    'ArrowLeft', 'ArrowDown', 'ArrowRight', 'ArrowUp',
                    'PageDown', 'PageUp', 'Home', 'End'
                ]);
            }
        }

        injectStyle() {
            const rootNode = this.container.getRootNode();
            const root = rootNode instanceof ShadowRoot ? rootNode : document;
            const styleTarget = rootNode instanceof ShadowRoot ? rootNode : document.head;

            if (root.getElementById(RangeSlider.styleId)) return;
            addStyle(styleTarget, RangeSlider.css, RangeSlider.styleId);
        }

        initDOM() {
            this.container.classList.add('cs-slider-container');

            this.track = document.createElement('div');
            this.track.className = 'cs-slider-track';

            this.progress = document.createElement('div');
            this.progress.className = 'cs-slider-progress';

            const thumb0 = document.createElement('div');
            thumb0.className = 'cs-slider-thumb';
            thumb0.tabIndex = 0;

            const thumb1 = document.createElement('div');
            thumb1.className = 'cs-slider-thumb';
            thumb1.tabIndex = 0;

            this.container.appendChild(this.track);
            this.container.appendChild(this.progress);
            this.container.appendChild(thumb0);
            this.container.appendChild(thumb1);

            this.container.addEventListener('mousedown', this.onStart);
            this.container.addEventListener('touchstart', this.onStart, { passive: false });

            thumb0.addEventListener('keydown', (e) => this.onKeyDown(e, 0));
            thumb1.addEventListener('keydown', (e) => this.onKeyDown(e, 1));

            this.thumbs = [thumb0, thumb1];
        }

        getValueFromPointer(e) {
            const source = (e.touches && e.touches[0]) || e;
            const percent = Math.max(0, Math.min(1, (source.clientX - this.trackLeft) / this.trackWidth));
            return this.min + percent * this.range;
        }

        updateValue(index, value, fn) {
            const steps = Math.round((value - this.min) / this.step);
            const newValue = Math.min(this.max, Math.max(this.min, this.min + steps * this.step));

            if (this.values[index] === newValue) return;

            this.values[index] = newValue;
            this.target[index] = ((newValue - this.min) / this.range) * 100;

            if (fn === this.render) this.current[index] = this.target[index];

            this.onChange({
                from: Math.min(this.values[0], this.values[1]),
                to: Math.max(this.values[0], this.values[1])
            });

            if (fn) fn();
        }

        setValues({ from, to } = {}, animate = false) {
            const fn = animate ? this.animate : this.render;

            if (from !== undefined) this.updateValue(0, from, fn);
            if (to !== undefined) this.updateValue(1, to, fn);
        }

        onStart(e) {
            e.preventDefault();
            if (e.touches && e.touches.length > 1) return;

            const rect = this.container.getBoundingClientRect();
            this.trackLeft = rect.left;
            this.trackWidth = rect.width;

            const currentVal = this.getValueFromPointer(e);

            if (e.target === this.thumbs[0]) {
                this.activeThumb = 0;
            } else if (e.target === this.thumbs[1]) {
                this.activeThumb = 1;
            } else {
                this.activeThumb = Math.abs(currentVal - this.values[0]) <= Math.abs(currentVal - this.values[1]) ? 0 : 1;
            }

            this.thumbs[this.activeThumb].style.zIndex = 20;
            this.thumbs[1 - this.activeThumb].style.zIndex = 10;
            this.thumbs[this.activeThumb].focus();

            this.updateValue(this.activeThumb, currentVal, this.animate);

            document.addEventListener('mousemove', this.onMove);
            document.addEventListener('touchmove', this.onMove, { passive: false });
            document.addEventListener('mouseup', this.onEnd);
            document.addEventListener('touchend', this.onEnd);
            document.addEventListener('touchcancel', this.onEnd);
        }

        onMove(e) {
            if (this.activeThumb === null || !this.trackWidth) return;
            e.preventDefault();

            this.updateValue(this.activeThumb, this.getValueFromPointer(e), this.animFrame ? null : this.render);
        }

        onEnd(e) {
            if (e.touches && e.touches.length > 0) return;

            this.activeThumb = null;

            document.removeEventListener('mousemove', this.onMove);
            document.removeEventListener('touchmove', this.onMove);
            document.removeEventListener('mouseup', this.onEnd);
            document.removeEventListener('touchend', this.onEnd);
            document.removeEventListener('touchcancel', this.onEnd);
        }

        onKeyDown(e, index) {
            if (!RangeSlider.handledKeys.has(e.key)) return;

            e.preventDefault();

            const step = this.step;
            const bigStep = step * 10;
            let currentVal = this.values[index];

            switch (e.key) {
                case 'ArrowLeft':
                case 'ArrowDown':
                    currentVal -= step;
                    break;
                case 'ArrowRight':
                case 'ArrowUp':
                    currentVal += step;
                    break;
                case 'PageDown':
                    currentVal -= bigStep;
                    break;
                case 'PageUp':
                    currentVal += bigStep;
                    break;
                case 'Home':
                    currentVal = this.min;
                    break;
                case 'End':
                    currentVal = this.max;
                    break;
            }

            this.updateValue(index, currentVal, this.animate);
        }

        animate() {
            cancelAnimationFrame(this.animFrame);

            const startTime = performance.now();
            const s = [...this.current], t = this.target;

            const step = (now) => {
                const progress = Math.min((now - startTime) / 150, 1);
                const ease = progress * (2 - progress);

                this.current[0] = s[0] + (t[0] - s[0]) * ease;
                this.current[1] = s[1] + (t[1] - s[1]) * ease;
                this.render();

                if (progress < 1) {
                    this.animFrame = requestAnimationFrame(step);
                } else {
                    this.animFrame = null;
                }
            };

            this.animFrame = requestAnimationFrame(step);
        }

        render() {
            const p0 = this.current[0];
            const p1 = this.current[1];

            const minPercent = Math.min(p0, p1);
            const maxPercent = Math.max(p0, p1);

            this.thumbs[0].style.left = `${p0}%`;
            this.thumbs[1].style.left = `${p1}%`;

            this.progress.style.left = `${minPercent}%`;
            this.progress.style.width = `${maxPercent - minPercent}%`;
        }
    }

    return RangeSlider;
})();