RangeSlider

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

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/593075/1915948/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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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.1
// @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.slice();
            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() {
            this.startTime = performance.now();
            this.start = this.current.slice();

            if (this.animFrame) return;

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

                this.current[0] = this.start[0] + (this.target[0] - this.start[0]) * ease;
                this.current[1] = this.start[1] + (this.target[1] - this.start[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;
})();