Repository: Stryzhevskyi/rangeSlider Branch: master Commit: 1284a994e108 Files: 19 Total size: 61.1 KB Directory structure: gitextract_68uuadx6/ ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ └── publish-from-tag.yaml ├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE.md ├── README.md ├── bower.json ├── example/ │ ├── index-esm.html │ ├── index-umd.html │ └── range-slider-flat.css ├── package.json ├── postcss.config.js ├── src/ │ ├── range-slider.css │ ├── range-slider.js │ └── utils/ │ ├── dom.js │ └── functions.js └── vite.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "standard", "rules": { "semi": 0, "no-undef": 0, "operator-linebreak": 0, "space-before-function-paren": 0, "indent": ["error", 2], "prefer-regex-literals": 0, "quote-props": 0, "prefer-const": 0, "dot-notation": 0 } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create bug report title: '' labels: bug assignees: '' --- **Example** Provide link to example, use [jsfiddle](https://jsfiddle.net/Stryzhevskyi/rpsa16fn/) as a template. **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Click on '....' 2. Move handle '....' 3. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/workflows/publish-from-tag.yaml ================================================ name: Publish Package on: push: tags: - '*' permissions: id-token: write # Required for OIDC contents: read jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '24' registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - run: npm publish ================================================ FILE: .gitignore ================================================ node_modules .idea/ .directory .history/ .vscode/ dist/ ================================================ FILE: .jshintrc ================================================ { "node": true, "browser": true, "esnext": true, "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "immed": true, "indent": 2, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "regexp": true, "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "globals": { "jQuery": true, "define": true } } ================================================ FILE: .npmignore ================================================ .eslintrc.json .git .github .gitignore .history .jshintrc .npmignore bower.json example node_modules postcss.config.js src vite.config.js ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015 Serhii Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # rangeSlider > Simple, small and fast vanilla JavaScript polyfill for the HTML5 `` slider element. > Forked from [André Ruffert's jQuery plugin](https://github.com/andreruffert/rangeslider.js) Check out the [examples](http://stryzhevskyi.github.io/rangeSlider/). * Touchscreen friendly * Recalculates `onresize` * Small and fast * Supports all major browsers * Buffer progressbar (for downloading progress etc.) ## Install Install with [npm](https://www.npmjs.com/package/rangeslider-pure): ``` npm install --save rangeslider-pure ``` ## Usage ```js // Initialize a new plugin instance for one element or NodeList of elements. const slider = document.querySelector('input[type="range"]'); rangeSlider.create(slider, { polyfill: true, // Boolean, if true, custom markup will be created root: document, rangeClass: 'rangeSlider', disabledClass: 'rangeSlider--disabled', fillClass: 'rangeSlider__fill', bufferClass: 'rangeSlider__buffer', handleClass: 'rangeSlider__handle', startEvent: ['mousedown', 'touchstart', 'pointerdown'], moveEvent: ['mousemove', 'touchmove', 'pointermove'], endEvent: ['mouseup', 'touchend', 'pointerup'], vertical: false, // Boolean, if true slider will be displayed in vertical orientation min: null, // Number, 0 max: null, // Number, 100 step: null, // Number, 1 value: null, // Number, center of slider buffer: null, // Number, in percent, 0 by default stick: null, // [Number stickTo, Number stickRadius] : use it if handle should stick to ${stickTo}-th value in ${stickRadius} borderRadius: 10, // Number, if you're using buffer + border-radius in css onInit: function () { console.info('onInit') }, onSlideStart: function (value, percent, position) { console.info('onSlideStart', 'value: ' + value, 'percent: ' + percent, 'position: ' + position); }, onSlide: function (value, percent, position) { console.log('onSlide', 'value: ' + value, 'percent: ' + percent, 'position: ' + position); }, onSlideEnd: function (value, percent, position) { console.warn('onSlideEnd', 'value: ' + value, 'percent: ' + percent, 'position: ' + position); } }); // update position const triggerEvents = true; // or false slider.rangeSlider.update({ min: 0, max: 20, step: 0.5, value: 1.5, buffer: 70 }, triggerEvents); ``` ```html ``` ### Internal APIs: ```js /* * @see src/utils/dom.js */ RangeSlider.dom; /* * @see src/utils/functions.js */ RangeSlider.functions; RangeSlider.version; ``` Use the [JSFiddle](https://jsfiddle.net/Stryzhevskyi/rpsa16fn/) template for issues. Alternative template on [StackBlitz](https://stackblitz.com/edit/rangeslider-pure-example). ## License MIT ================================================ FILE: bower.json ================================================ { "name": "rangeslider-pure", "version": "0.5.0", "homepage": "https://github.com/Stryzhevskyi/rangeSlider", "authors": [ "Stryzhevskyi (https://github.com/Stryzhevskyi)" ], "repository": { "type": "git", "url": "git@github.com:Stryzhevskyi/rangeSlider.git" }, "description": "Simple, small and fast vanilla JavaScript polyfill for the HTML5 slider element", "main": [ "dist/range-slider.js", "dist/range-slider.css" ], "license": "MIT", "keywords": [ "input", "range", "slider", "rangeSlider", "rangeSlider.js", "polyfill", "vanilla", "pure" ], "ignore": [ "**/.*", "*.json", "src", "example", "node_modules", "bower_components", "gulpfile.js" ], "dependencies": {} } ================================================ FILE: example/index-esm.html ================================================ range-slider.js

Comparison to native element





Negative attributes



Vertical mode



Floating point boundaries



Stick example

Handle will stick to Nth number in X radius, 10 and 0.7 in example



value="0"



disabled



max="0" disables slider



Programmatic value changes





Predefined buffer position data-buffer="60"





Update buffer position

%




Update range





Destroy a plugin instance



Consider initialization and update of hidden elements



Combination with native <details> element

Toggle

================================================ FILE: example/index-umd.html ================================================ range-slider.js

Comparison to native element





Negative attributes



Vertical mode



Floating point boundaries



Stick example

Handle will stick to Nth number in X radius, 10 and 0.15 in example



value="0"



disabled



max="0" disables slider



Programmatic value changes





Predefined buffer position data-buffer="60"





Update buffer position

%




Update range





Destroy a plugin instance



Consider initialization and update of hidden elements



Combination with native <details> element

Toggle

================================================ FILE: example/range-slider-flat.css ================================================ .rangeSlider, .rangeSlider__fill { background: #7f8c8d; display: block; height: 8px; width: 100%; -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); -moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.5); -webkit-border-radius: 10px; -moz-border-radius: 10px; -ms-border-radius: 10px; -o-border-radius: 10px; border-radius: 4px; } .rangeSlider { position: relative; } .rangeSlider--disabled { filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); opacity: 0.4; } .rangeSlider__fill { background: #FFFFFF; position: absolute; top: 0; } .rangeSlider__handle { background: white; border: 1px solid #ccc; cursor: pointer; display: inline-block; width: 22px; height: 21px; position: absolute; top: -7px; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(0, 0, 0, 0.1))); background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1)); background-image: -moz-linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1)); background-image: -o-linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1)); background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1)); -webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); -webkit-border-radius: 50%; -moz-border-radius: 50%; -ms-border-radius: 50%; -o-border-radius: 50%; border-radius: 50%; } .rangeSlider__handle:after { content: ""; display: block; width: 10px; height: 10px; margin: auto; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0, 0, 0, 0.13)), color-stop(100%, rgba(255, 255, 255, 0))); background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0.13), rgba(255, 255, 255, 0)); background-image: -moz-linear-gradient(rgba(0, 0, 0, 0.13), rgba(255, 255, 255, 0)); background-image: -o-linear-gradient(rgba(0, 0, 0, 0.13), rgba(255, 255, 255, 0)); background-image: linear-gradient(rgba(0, 0, 0, 0.13), rgba(255, 255, 255, 0)); -webkit-border-radius: 50%; -moz-border-radius: 50%; -ms-border-radius: 50%; -o-border-radius: 50%; border-radius: 50%; } .rangeSlider__handle:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0, 0, 0, 0.1)), color-stop(100%, rgba(0, 0, 0, 0.12))); background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.12)); background-image: -moz-linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.12)); background-image: -o-linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.12)); background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.12)); outline: none; } input[type="range"]:focus + .rangeSlider .rangeSlider__handle { -webkit-box-shadow: 0 0 8px rgba(142, 68, 173, 0.9); -moz-box-shadow: 0 0 8px rgba(142, 68, 173, 0.9); box-shadow: 0 0 8px rgba(142, 68, 173, 0.9); } .rangeSlider__buffer { position: absolute; top: 2px; height: 4px; background: #2c3e50; border-radius: 2px; } ================================================ FILE: package.json ================================================ { "name": "rangeslider-pure", "title": "range-slider", "description": "Simple, small and fast vanilla JavaScript polyfill for the HTML5 slider element", "version": "0.5.0", "type": "module", "main": "dist/range-slider.js", "module": "dist/range-slider.esm.js", "types": "dist/range-slider.d.ts", "exports": { ".": { "import": "./dist/range-slider.esm.js", "require": "./dist/range-slider.js" }, "./dist/range-slider.css": "./dist/range-slider.css" }, "files": [ "dist", "LICENSE.md", "README.md" ], "scripts": { "dev": "vite serve", "build": "vite build", "preview": "vite preview", "lint": "eslint --fix src/*.js" }, "homepage": "https://github.com/Stryzhevskyi/rangeSlider", "author": { "name": "Serhii Stryzhevskyi", "url": "https://github.com/Stryzhevskyi" }, "repository": { "type": "git", "url": "git@github.com:Stryzhevskyi/rangeSlider.git" }, "keywords": [ "input", "range", "slider", "rangeslider", "rangeslider.js", "polyfill", "browser", "pure", "vanilla" ], "licenses": [ { "type": "MIT", "url": "https://github.com/Stryzhevskyi/rangeSlider/blob/master/LICENSE.md" } ], "bugs": { "url": "https://github.com/Stryzhevskyi/rangeSlider/issues" }, "engines": { "node": ">=18.0.0" }, "browserslist": [ "ie >= 9", "last 3 versions" ], "devDependencies": { "autoprefixer": "^10.4.27", "cssnano": "^7.0.6", "eslint": "^10.1.0", "vite": "^8.0.1" } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { autoprefixer: {}, cssnano: {}, }, }; ================================================ FILE: src/range-slider.css ================================================ .rangeSlider, .rangeSlider__fill { display: block; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); border-radius: 10px; } .rangeSlider { position: relative; background: #7f8c8d; } .rangeSlider__horizontal { height: 20px; width: 100%; } .rangeSlider__vertical { height: 100%; width: 20px; } .rangeSlider--disabled { filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); opacity: 0.4; } .rangeSlider__fill { background: #16a085; position: absolute; } .rangeSlider__fill__horizontal { height: 100%; top: 0; left: 0; } .rangeSlider__fill__vertical { width: 100%; bottom: 0; left: 0; } .rangeSlider__handle { border: 1px solid #ccc; cursor: pointer; display: inline-block; width: 40px; height: 40px; position: absolute; background: white linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1)); box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); border-radius: 50%; } .rangeSlider__handle__horizontal { top: -10px; } .rangeSlider__handle__vertical { left: -10px; bottom: 0; } .rangeSlider__handle:after { content: ""; display: block; width: 18px; height: 18px; margin: auto; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-image: linear-gradient(rgba(0, 0, 0, 0.13), rgba(255, 255, 255, 0)); border-radius: 50%; } .rangeSlider__handle:active { background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.12)); } input[type="range"]:focus + .rangeSlider .rangeSlider__handle { box-shadow: 0 0 8px rgba(142, 68, 173, 0.9); } .rangeSlider__buffer { position: absolute; top: 3px; height: 14px; background: #2c3e50; border-radius: 10px; } ================================================ FILE: src/range-slider.js ================================================ import * as dom from './utils/dom'; import * as func from './utils/functions'; import './range-slider.css'; const newLineAndTabRegexp = /[\n\t]/g; const MAX_SET_BY_DEFAULT = 100; const HANDLE_RESIZE_DELAY = 300; const HANDLE_RESIZE_DEBOUNCE = 50; const pluginName = 'rangeSlider'; const inputrange = dom.supportsRange(); const defaults = { polyfill: true, root: document, rangeClass: 'rangeSlider', disabledClass: 'rangeSlider--disabled', fillClass: 'rangeSlider__fill', bufferClass: 'rangeSlider__buffer', handleClass: 'rangeSlider__handle', startEvent: ['mousedown', 'touchstart', 'pointerdown'], moveEvent: ['mousemove', 'touchmove', 'pointermove'], endEvent: ['mouseup', 'touchend', 'pointerup'], min: null, max: null, step: null, value: null, buffer: null, stick: null, borderRadius: 10, vertical: false }; let verticalSlidingFixRegistered = false; /** * Plugin * @param {HTMLElement} element * @param {this} options */ export default class RangeSlider { constructor(element, options) { let minSetByDefault; let maxSetByDefault; let stepSetByDefault; let stickAttribute; let stickValues; RangeSlider.instances.push(this); this.element = element; this.options = func.simpleExtend(defaults, options); this.polyfill = this.options.polyfill; this.vertical = this.options.vertical; this.onInit = this.options.onInit; this.onSlide = this.options.onSlide; this.onSlideStart = this.options.onSlideStart; this.onSlideEnd = this.options.onSlideEnd; this.onSlideEventsCount = -1; this.isInteractsNow = false; this.needTriggerEvents = false; this._addVerticalSlideScrollFix(); // Plugin should only be used as a polyfill if (!this.polyfill) { // Input range support? if (inputrange) { return; } } this.options.buffer = this.options.buffer || parseFloat(this.element.getAttribute('data-buffer')); this.identifier = 'js-' + pluginName + '-' + func.uuid(); this.min = func.getFirstNumberLike( this.options.min, parseFloat(this.element.getAttribute('min')), (minSetByDefault = 0) ); this.max = func.getFirstNumberLike( this.options.max, parseFloat(this.element.getAttribute('max')), (maxSetByDefault = MAX_SET_BY_DEFAULT) ); this.value = func.getFirstNumberLike(this.options.value, this.element.value, parseFloat(this.element.value || this.min + (this.max - this.min) / 2)); this.step = func.getFirstNumberLike(this.options.step, parseFloat(this.element.getAttribute('step')) || (stepSetByDefault = 1)); this.percent = null; if (func.isArray(this.options.stick) && this.options.stick.length >= 1) { this.stick = this.options.stick; } else if ((stickAttribute = this.element.getAttribute('stick'))) { stickValues = stickAttribute.split(' '); if (stickValues.length >= 1) { this.stick = stickValues.map(parseFloat); } } if (this.stick && this.stick.length === 1) { this.stick.push(this.step * 1.5); } this._updatePercentFromValue(); this.toFixed = this._toFixed(this.step); let directionClass; this.container = document.createElement('div'); dom.addClass(this.container, this.options.fillClass); directionClass = this.vertical ? this.options.fillClass + '__vertical' : this.options.fillClass + '__horizontal'; dom.addClass(this.container, directionClass); this.handle = document.createElement('div'); dom.addClass(this.handle, this.options.handleClass); directionClass = this.vertical ? this.options.handleClass + '__vertical' : this.options.handleClass + '__horizontal'; dom.addClass(this.handle, directionClass); this.range = document.createElement('div'); dom.addClass(this.range, this.options.rangeClass); this.range.id = this.identifier; const elementTitle = element.getAttribute('title'); if (elementTitle && elementTitle.length > 0) { this.range.setAttribute('title', elementTitle); } if (this.options.bufferClass) { this.buffer = document.createElement('div'); dom.addClass(this.buffer, this.options.bufferClass); this.range.appendChild(this.buffer); directionClass = this.vertical ? this.options.bufferClass + '__vertical' : this.options.bufferClass + '__horizontal'; dom.addClass(this.buffer, directionClass); } this.range.appendChild(this.container); this.range.appendChild(this.handle); directionClass = this.vertical ? this.options.rangeClass + '__vertical' : this.options.rangeClass + '__horizontal'; dom.addClass(this.range, directionClass); if (func.isNumberLike(this.options.value)) { this._setValue(this.options.value, true); this.element.value = this.options.value; } if (func.isNumberLike(this.options.buffer)) { this.element.setAttribute('data-buffer', this.options.buffer); } if (func.isNumberLike(this.options.min) || minSetByDefault) { this.element.setAttribute('min', '' + this.min); } if (func.isNumberLike(this.options.max) || maxSetByDefault) { this.element.setAttribute('max', '' + this.max); } if (func.isNumberLike(this.options.step) || stepSetByDefault) { this.element.setAttribute('step', '' + this.step); } dom.insertAfter(this.element, this.range); // hide the input visually dom.setCss(this.element, { 'position': 'absolute', 'width': '1px', 'height': '1px', 'overflow': 'hidden', 'opacity': '0' }); // Store context this._handleDown = this._handleDown.bind(this); this._handleMove = this._handleMove.bind(this); this._handleEnd = this._handleEnd.bind(this); this._startEventListener = this._startEventListener.bind(this); this._changeEventListener = this._changeEventListener.bind(this); this._handleResize = this._handleResize.bind(this); this._init(); // Attach Events window.addEventListener('resize', this._handleResize, false); dom.addEventListeners(this.options.root, this.options.startEvent, this._startEventListener); // Listen to programmatic value changes this.element.addEventListener('change', this._changeEventListener, false); } /** * A lightweight plugin wrapper around the constructor,preventing against multiple instantiations * @param {Element} el * @param {Object} options */ static create(el, options) { const createInstance = (el) => { let data = el[pluginName]; // Create a new instance. if (!data) { data = new RangeSlider(el, options); el[pluginName] = data; } }; if (el.length) { Array.prototype.slice.call(el).forEach(function (el) { createInstance(el); }); } else { createInstance(el); } } static _touchMoveScrollHandler (event) { if (RangeSlider.slidingVertically) { event.preventDefault(); } } /* public methods */ /** * @param {Object} obj like {min : Number, max : Number, value : Number, step : Number, buffer : [String|Number]} * @param {Boolean} triggerEvents * @returns {RangeSlider} */ update(obj, triggerEvents) { if (triggerEvents) { this.needTriggerEvents = true; } if (func.isObject(obj)) { if (func.isNumberLike(obj.min)) { this.element.setAttribute('min', '' + obj.min); this.min = obj.min; } if (func.isNumberLike(obj.max)) { this.element.setAttribute('max', '' + obj.max); this.max = obj.max; } if (func.isNumberLike(obj.step)) { this.element.setAttribute('step', '' + obj.step); this.step = obj.step; this.toFixed = this._toFixed(obj.step); } if (func.isNumberLike(obj.buffer)) { this._setBufferPosition(obj.buffer); } if (func.isNumberLike(obj.value)) { this._setValue(obj.value); } } this._update(); this.onSlideEventsCount = 0; this.needTriggerEvents = false; return this; }; destroy() { dom.removeAllListenersFromEl(this, this.options.root); window.removeEventListener('resize', this._handleResize, false); this.element.removeEventListener('change', this._changeEventListener, false); this.element.style.cssText = ''; delete this.element[pluginName]; // Remove the generated markup if (this.range) { this.range.parentNode.removeChild(this.range); } RangeSlider.instances = RangeSlider.instances.filter((plugin) => plugin !== this); if (!RangeSlider.instances.some((plugin) => plugin.vertical)) { this._removeVerticalSlideScrollFix(); } } /* private methods */ _toFixed(step) { return (step + '').replace('.', '').length - 1; } _init() { if (this.onInit && typeof this.onInit === 'function') { this.onInit(); } this._update(false); } _updatePercentFromValue() { this.percent = (this.value - this.min) / (this.max - this.min); } /** * This method check if this.identifier exists in ev.target's ancestors * @param ev * @param data */ _startEventListener(ev, data) { const el = ev.target; let isEventOnSlider = false; if (ev.which !== 1 && !('touches' in ev)) { return; } dom.forEachAncestors( el, el => (isEventOnSlider = el.id === this.identifier && !dom.hasClass(el, this.options.disabledClass)), true ); if (isEventOnSlider) { this._handleDown(ev, data); } } _changeEventListener(ev, data) { if (data && data.origin === this.identifier) { return; } const value = ev.target.value; const pos = this._getPositionFromValue(value); this._setPosition(pos); } _update(triggerEvent) { const sizeProperty = this.vertical ? 'offsetHeight' : 'offsetWidth'; this.handleSize = dom.getDimension(this.handle, sizeProperty); this.rangeSize = dom.getDimension(this.range, sizeProperty); this.maxHandleX = this.rangeSize - this.handleSize; this.grabX = this.handleSize / 2; this.position = this._getPositionFromValue(this.value); // Consider disabled state if (this.element.disabled) { dom.addClass(this.range, this.options.disabledClass); } else { dom.removeClass(this.range, this.options.disabledClass); } this._setPosition(this.position); if (this.options.bufferClass && this.options.buffer) { this._setBufferPosition(this.options.buffer); } this._updatePercentFromValue(); if (triggerEvent !== false) { dom.triggerEvent(this.element, 'change', { origin: this.identifier }); } } _addVerticalSlideScrollFix() { if (this.vertical && !verticalSlidingFixRegistered) { document.addEventListener('touchmove', RangeSlider._touchMoveScrollHandler, { passive: false }); verticalSlidingFixRegistered = true; } } _removeVerticalSlideScrollFix() { document.removeEventListener('touchmove', RangeSlider._touchMoveScrollHandler); verticalSlidingFixRegistered = false; } _handleResize() { return func.debounce(() => { // Simulate resizeEnd event. func.delay(() => { this._update(); }, HANDLE_RESIZE_DELAY); }, HANDLE_RESIZE_DEBOUNCE)(); } _handleDown(e) { this.isInteractsNow = true; e.preventDefault(); dom.addEventListeners(this.options.root, this.options.moveEvent, this._handleMove); dom.addEventListeners(this.options.root, this.options.endEvent, this._handleEnd); // If we click on the handle don't set the new position if ((' ' + e.target.className + ' ').replace(newLineAndTabRegexp, ' ').indexOf(this.options.handleClass) > -1) { return; } const boundingClientRect = this.range.getBoundingClientRect(); const posX = this._getRelativePosition(e); const rangeX = this.vertical ? boundingClientRect.bottom : boundingClientRect.left; const handleX = this._getPositionFromNode(this.handle) - rangeX; const position = posX - this.grabX; this._setPosition(position); if (posX >= handleX && posX < handleX + this.options.borderRadius * 2) { this.grabX = posX - handleX; } this._updatePercentFromValue(); } _handleMove(e) { const posX = this._getRelativePosition(e); this.isInteractsNow = true; e.preventDefault(); this._setPosition(posX - this.grabX); } _handleEnd(e) { e.preventDefault(); dom.removeEventListeners(this.options.root, this.options.moveEvent, this._handleMove); dom.removeEventListeners(this.options.root, this.options.endEvent, this._handleEnd); // Ok we're done fire the change event dom.triggerEvent(this.element, 'change', { origin: this.identifier }); if (this.isInteractsNow || this.needTriggerEvents) { if (this.onSlideEnd && typeof this.onSlideEnd === 'function') { this.onSlideEnd(this.value, this.percent, this.position); } if (this.vertical) { RangeSlider.slidingVertically = false; } } this.onSlideEventsCount = 0; this.isInteractsNow = false; } _setPosition(pos) { let position; let stickRadius; let restFromValue; let stickTo; // Snapping steps let value = this._getValueFromPosition(func.between(pos, 0, this.maxHandleX)); // Stick to stick[0] in radius stick[1] if (this.stick) { stickTo = this.stick[0]; stickRadius = this.stick[1] || 0.1; restFromValue = value % stickTo; if (restFromValue < stickRadius) { value = value - restFromValue; } else if (Math.abs(stickTo - restFromValue) < stickRadius) { value = value - restFromValue + stickTo; } } position = this._getPositionFromValue(value); // Update ui if (this.vertical) { this.container.style.height = (position + this.grabX) + 'px'; this.handle.style['webkitTransform'] = 'translateY(-' + position + 'px)'; this.handle.style['msTransform'] = 'translateY(-' + position + 'px)'; this.handle.style.transform = 'translateY(-' + position + 'px)'; } else { this.container.style.width = (position + this.grabX) + 'px'; this.handle.style['webkitTransform'] = 'translateX(' + position + 'px)'; this.handle.style['msTransform'] = 'translateX(' + position + 'px)'; this.handle.style.transform = 'translateX(' + position + 'px)'; } this._setValue(value); // Update globals this.position = position; this.value = value; this._updatePercentFromValue(); if (this.isInteractsNow || this.needTriggerEvents) { if (this.onSlideStart && typeof this.onSlideStart === 'function' && this.onSlideEventsCount === 0) { this.onSlideStart(this.value, this.percent, this.position); } if (this.onSlide && typeof this.onSlide === 'function') { this.onSlide(this.value, this.percent, this.position); } if (this.vertical) { RangeSlider.slidingVertically = true; } } this.onSlideEventsCount++; } _setBufferPosition(pos) { let isPercent = true; if (isFinite(pos)) { pos = parseFloat(pos); } else if (func.isString(pos)) { if (pos.indexOf('px') > 0) { isPercent = false; } pos = parseFloat(pos); } else { console.warn('New position must be XXpx or XX%'); return; } if (isNaN(pos)) { console.warn('New position is NaN'); return; } if (!this.options.bufferClass) { console.warn('You disabled buffer, it\'s className is empty'); return; } let bufferSize = isPercent ? pos : (pos / this.rangeSize * 100); if (bufferSize < 0) { bufferSize = 0; } if (bufferSize > 100) { bufferSize = 100; } this.options.buffer = bufferSize; let paddingSize = this.options.borderRadius / this.rangeSize * 100; let bufferSizeWithPadding = bufferSize - paddingSize; if (bufferSizeWithPadding < 0) { bufferSizeWithPadding = 0; } if (this.vertical) { this.buffer.style.height = bufferSizeWithPadding + '%'; this.buffer.style.bottom = paddingSize * 0.5 + '%'; } else { this.buffer.style.width = bufferSizeWithPadding + '%'; this.buffer.style.left = paddingSize * 0.5 + '%'; } this.element.setAttribute('data-buffer', bufferSize); } /** * * @param {Element} node * @returns {*} Returns element position relative to the parent * @private */ _getPositionFromNode(node) { let i = 0; while (node !== null) { i += this.vertical ? node.offsetTop : node.offsetLeft; node = node.offsetParent; } return i; } /** * * @param {(MouseEvent|TouchEvent)}e * @returns {number} */ _getRelativePosition(e) { const boundingClientRect = this.range.getBoundingClientRect(); // Get the offset relative to the viewport const rangeSize = this.vertical ? boundingClientRect.bottom : boundingClientRect.left; let pageOffset = 0; const pagePositionProperty = this.vertical ? 'pageY' : 'pageX'; if (typeof e[pagePositionProperty] !== 'undefined') { pageOffset = (e.touches && e.touches.length) ? e.touches[0][pagePositionProperty] : e[pagePositionProperty]; } else if (typeof e.originalEvent !== 'undefined') { if (typeof e.originalEvent[pagePositionProperty] !== 'undefined') { pageOffset = e.originalEvent[pagePositionProperty]; } else if (e.originalEvent.touches && e.originalEvent.touches[0] && typeof e.originalEvent.touches[0][pagePositionProperty] !== 'undefined') { pageOffset = e.originalEvent.touches[0][pagePositionProperty]; } } else if (e.touches && e.touches[0] && typeof e.touches[0][pagePositionProperty] !== 'undefined') { pageOffset = e.touches[0][pagePositionProperty]; } else if (e.currentPoint && (typeof e.currentPoint.x !== 'undefined' || typeof e.currentPoint.y !== 'undefined')) { pageOffset = this.vertical ? e.currentPoint.y : e.currentPoint.x; } if (this.vertical) { pageOffset -= window.pageYOffset; } return this.vertical ? rangeSize - pageOffset : pageOffset - rangeSize; } _getPositionFromValue(value) { const percentage = (value - this.min) / (this.max - this.min); const pos = percentage * this.maxHandleX; return isNaN(pos) ? 0 : pos; } _getValueFromPosition(pos) { const percentage = ((pos) / (this.maxHandleX || 1)); const value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min; return Number((value).toFixed(this.toFixed)); } _setValue(value, force) { if (value === this.value && !force) { return; } // Set the new value and fire the `input` event this.element.value = value; this.value = value; dom.triggerEvent(this.element, 'input', { origin: this.identifier }); } } RangeSlider.version = VERSION; RangeSlider.dom = dom; RangeSlider.functions = func; RangeSlider.instances = []; RangeSlider.slidingVertically = false; ================================================ FILE: src/utils/dom.js ================================================ import * as func from './functions'; const EVENT_LISTENER_LIST = 'eventListenerList'; export const detectIE = () => { const ua = window.navigator.userAgent; const msie = ua.indexOf('MSIE '); if (msie > 0) { return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); } const trident = ua.indexOf('Trident/'); if (trident > 0) { const rv = ua.indexOf('rv:'); return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); } const edge = ua.indexOf('Edge/'); if (edge > 0) { return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); } return false; }; const ieVersion = detectIE(); const eventCaptureParams = window.PointerEvent && !ieVersion ? {passive: false} : false; /** * Check if a `element` is visible in the DOM * * @param {Element} element * @return {Boolean} */ export const isHidden = (element) => ( element.offsetWidth === 0 || element.offsetHeight === 0 || element.open === false ); /** * Get hidden parentNodes of an `element` * * @param {Element} element * @return {Element[]} */ export const getHiddenParentNodes = (element) => { const parents = []; let node = element.parentNode; while (node && isHidden(node)) { parents.push(node); node = node.parentNode; } return parents; }; /** * Returns dimensions for an element even if it is not visible in the DOM. * * @param {Element} element * @param {string} key (e.g. offsetWidth …) * @return {Number} */ export const getDimension = (element, key) => { const hiddenParentNodes = getHiddenParentNodes(element); const hiddenParentNodesLength = hiddenParentNodes.length; const hiddenParentNodesStyle = []; let dimension = element[key]; // Used for native `
` elements const toggleOpenProperty = (element) => { if (typeof element.open !== 'undefined') { element.open = !element.open; } }; if (hiddenParentNodesLength) { for (let i = 0; i < hiddenParentNodesLength; i++) { // Cache the styles to restore then later. hiddenParentNodesStyle.push({ display: hiddenParentNodes[i].style.display, height: hiddenParentNodes[i].style.height, overflow: hiddenParentNodes[i].style.overflow, visibility: hiddenParentNodes[i].style.visibility }); hiddenParentNodes[i].style.display = 'block'; hiddenParentNodes[i].style.height = '0'; hiddenParentNodes[i].style.overflow = 'hidden'; hiddenParentNodes[i].style.visibility = 'hidden'; toggleOpenProperty(hiddenParentNodes[i]); } dimension = element[key]; for (let j = 0; j < hiddenParentNodesLength; j++) { toggleOpenProperty(hiddenParentNodes[j]); hiddenParentNodes[j].style.display = hiddenParentNodesStyle[j].display; hiddenParentNodes[j].style.height = hiddenParentNodesStyle[j].height; hiddenParentNodes[j].style.overflow = hiddenParentNodesStyle[j].overflow; hiddenParentNodes[j].style.visibility = hiddenParentNodesStyle[j].visibility; } } return dimension; }; /** * * @param {HTMLElement} el * @param {Object} cssObj * @returns {*} */ export const setCss = (el, cssObj) => { for (const key in cssObj) { el.style[key] = cssObj[key]; } return el.style; }; /** * * @param {HTMLElement} elem * @param {string} className */ export const hasClass = (elem, className) => new RegExp(' ' + className + ' ').test(' ' + elem.className + ' '); /** * * @param {HTMLElement} elem * @param {string} className */ export const addClass = (elem, className) => { if (!hasClass(elem, className)) { elem.className += ' ' + className; } }; /** * * @param {HTMLElement} elem * @param {string} className */ export const removeClass = (elem, className) => { let newClass = ' ' + elem.className.replace(/[\t\r\n]/g, ' ') + ' '; if (hasClass(elem, className)) { while (newClass.indexOf(' ' + className + ' ') >= 0) { newClass = newClass.replace(' ' + className + ' ', ' '); } elem.className = newClass.replace(/^\s+|\s+$/g, ''); } }; /** * * @param {HTMLElement} el * @param {Function} callback * @param {boolean} andForElement - apply callback for el * @returns {HTMLElement} */ export const forEachAncestors = (el, callback, andForElement) => { if (andForElement) { callback(el); } while (el.parentNode && !callback(el)) { el = el.parentNode; } return el; }; /** * * @param {HTMLElement} el * @param {string} name event name * @param {Object} data */ export const triggerEvent = (el, name, data) => { if (!func.isString(name)) { throw new TypeError('event name must be String'); } if (!(el instanceof HTMLElement)) { throw new TypeError('element must be HTMLElement'); } name = name.trim(); const event = document.createEvent('CustomEvent'); event.initCustomEvent(name, false, false, data); el.dispatchEvent(event); }; /** * @param {Object} referenceNode after this * @param {Object} newNode insert this */ export const insertAfter = (referenceNode, newNode) => referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); /** * Add event listeners and push them to el[EVENT_LISTENER_LIST] * @param {HTMLElement|Node|Document} el DOM element * @param {Array} events * @param {Function} listener */ export const addEventListeners = (el, events, listener) => { events.forEach((eventName) => { if (!el[EVENT_LISTENER_LIST]) { el[EVENT_LISTENER_LIST] = {}; } if (!el[EVENT_LISTENER_LIST][eventName]) { el[EVENT_LISTENER_LIST][eventName] = []; } el.addEventListener( eventName, listener, eventCaptureParams ); if (el[EVENT_LISTENER_LIST][eventName].indexOf(listener) < 0) { el[EVENT_LISTENER_LIST][eventName].push(listener); } }); }; /** * Remove event listeners and remove them from el[EVENT_LISTENER_LIST] * @param {HTMLElement} el DOM element * @param {Array} events * @param {Function} listener */ export const removeEventListeners = (el, events, listener) => { events.forEach((eventName) => { let index; el.removeEventListener( eventName, listener, eventCaptureParams ); if (el[EVENT_LISTENER_LIST] && el[EVENT_LISTENER_LIST][eventName] && (index = el[EVENT_LISTENER_LIST][eventName].indexOf(listener)) > -1 ) { el[EVENT_LISTENER_LIST][eventName].splice(index, 1); } }); }; /** * Remove ALL event listeners which exists in el[EVENT_LISTENER_LIST] * @param {RangeSlider} instance * @param {HTMLElement} el DOM element */ export const removeAllListenersFromEl = (instance, el) => { if (!el[EVENT_LISTENER_LIST]) { return; } /* jshint ignore:start */ /** * * @callback listener * @this {Object} event name */ function rm(listener) { if (listener === instance._startEventListener) { this.el.removeEventListener(this.eventName, listener, false); } } for (const eventName in el[EVENT_LISTENER_LIST]) { el[EVENT_LISTENER_LIST][eventName].forEach(rm, {eventName: eventName, el: el}); } el[EVENT_LISTENER_LIST] = {}; /* jshint ignore:end */ }; /** * Range feature detection * @return {Boolean} */ export const supportsRange = () => { const input = document.createElement('input'); input.setAttribute('type', 'range'); return input.type !== 'text'; }; ================================================ FILE: src/utils/functions.js ================================================ /** * Create a random uuid */ export const uuid = () => { const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); }; /** * Delays a function for the given number of milliseconds, and then calls * it with the arguments supplied. * * @param {Function} fn function * @param {Number} wait delay * @param {Number} args arguments * @return {Function} */ export const delay = (fn, wait, ...args) => setTimeout(() => fn.apply(null, args), wait); /** * Returns a debounced function that will make sure the given * function is not triggered too much. * * @param {Function} fn Function to debounce. * @param {Number} debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the * function. (defaults to 100ms) * @return {Function} */ export const debounce = (fn, debounceDuration = 100) => (...args) => { if (!fn.debouncing) { fn.lastReturnVal = fn.apply(window, args); fn.debouncing = true; } clearTimeout(fn.debounceTimeout); fn.debounceTimeout = setTimeout(() => { fn.debouncing = false; }, debounceDuration); return fn.lastReturnVal; }; export const isString = obj => obj === '' + obj; export const isArray = obj => Object.prototype.toString.call(obj) === '[object Array]'; export const isNumberLike = (obj) => (obj !== null && obj !== undefined && ((isString(obj) && isFinite(parseFloat(obj))) || (isFinite(obj)))); export const getFirstNumberLike = (...args) => { if (!args.length) { return null; } for (let i = 0, len = args.length; i < len; i++) { if (isNumberLike(args[i])) { return args[i]; } } return null; }; export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]'; export const simpleExtend = (defaultOpt, options) => { const opt = {}; for (let key in defaultOpt) { opt[key] = defaultOpt[key]; } for (let key in options) { opt[key] = options[key]; } return opt; }; export const between = (pos, min, max) => { if (pos < min) { return min; } if (pos > max) { return max; } return pos; }; ================================================ FILE: vite.config.js ================================================ import { defineConfig } from 'vite' import { resolve } from 'path' import pkg from './package.json' export default defineConfig({ define: { VERSION: JSON.stringify(pkg.version), }, build: { lib: { entry: resolve(__dirname, 'src/range-slider.js'), name: 'rangeSlider', formats: ['umd', 'es'], fileName: (format) => format === 'es' ? 'range-slider.esm.js' : 'range-slider.js', cssFileName: 'range-slider', }, sourcemap: true, cssCodeSplit: false, }, server: { port: 8000, open: '/example/index-esm.html', }, })