Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <h1>(WORK IN PROGRESS)</h1>
<component class='multiRange'></component>
<component class='multiRange type1'></component>
              
            
!

CSS

              
                html, body{ height:100%; }
body{ 
    font:16px Arial;
    display: flex; 
    flex-flow: column;
    align-items: center;
    justify-content: center; 
}
.multiRange{ margin:3em 0; width:40%; min-width:150px; 
    &.type1{
        $C: #EC5564;
        .multiRange__range{ 
            color:#EEE; 
            transition:50ms;
            
            &:nth-child(2){ color:$C; }
            
            .multiRange__handle{ 
                box-shadow:none; 
                background:$C;
                &__value{
                    background:$C;
                    color:white;
                    &::after{
                        border-color:$C transparent transparent;
                    }
                }
            }
        }
    }
}

//// COMPONENT //////////////////////////////

body.multiRange-grabbing{ cursor:grabbing; }
.multiRange{
    $height: 12px;
    
    // can be used to set automatic color to each range slice
    @mixin rangesColors( $size:6 ){
        @for $i from 1 through $size{
            &:nth-child(#{$i}){ 
                color:hsl($i * 35, 70%, 66%);
            }
        }
    }
    
    user-select:none;

    &__rangeWrap{
        height: $height;
        background:#E8E8E8;
        border-radius:3px;
        position:relative;
        z-index:5;
    }
    
    &__range{
        @include rangesColors(5);
        
        height: 100%;
        position:absolute;
        right:0;
        background:currentColor; 

        // hide the first handle
        &:first-child{
            > .multiRange__handle{ display:none; }
        }
        
        // a class is added when a handle is grabbed (mousedown)
        &.grabbed{
            > .handle{ background:black; }
        }

        .multiRange__handle{ 
            $out: -3px;
            width: 2px;
            
            position: absolute;
            top: $out;
            bottom: $out;
            left: -1px;
            
            cursor: grab;
            background: currentColor; 
            box-shadow: 1px 0 white, -1px 0 white;
            transition: .2s;
            
            &:active{ cursor:inherit; }
            
            &__value{
                $C: #333;
                position: absolute;
                transform: translate(-50%, -6px);
                min-width:10px;
                background: $C;
                color: white;
                padding: 2px 6px;
                top: -100%;
                left: 0;
                white-space: nowrap;
                font-size: 11px;
                text-align:center;
                border-radius:4px;
                cursor:default;
              //  pointer-events:none;

                &::after{
                    content: "";
                    position: absolute;
                    left: 50%;
                    bottom: -3px;
                    border-color: $C transparent transparent;
                    border-style: solid;
                    border-width: 3px 4px;
                    transform: translate(-50%, 50%);
                    color: $C;
                    font-size: 15px;
                }
                
                &--bottom{
                    &::after{
                    }
                }
            }
        }
        
        &:hover{
         //   box-shadow:0 0 0 1000px rgba(black, .1) inset;
        }
    }
    
    &__ticks{
        display:flex;
        justify-content: space-between;
        height: 6px;
        margin: 2px 0 0 0;
        font:10px Arial;
        cursor:default;
        
        > div{
            height:100%;
            width:1px;
            background:#DDD;
            color: #888;

            &:nth-child(5n - 4){
                height: 200%;
                &::before{
                    display:block;

                    content: attr(data-value);
                    transform:translate(-50%, 100%);
                    text-align: center;
                    width:40px;
                }
            }
        }
    }
}
              
            
!

JS

              
                // (C) Yair Even-Or 2017
// DO NOT COPY 

(function(){  
function extend(o1, o2){
    for( var key in o2 )
        if( o2.hasOwnProperty(key) )
            o1[key] = o2[key];
};
    
this.MultiRange = function MultiRange( placeholderElm, settings ){
    settings = typeof settings == 'object' ? settings : {}; // make sure settings is an 'object'

    this.settings = {
        minRange   : typeof settings.minRange == 'number' ? settings.minRange : 1,
        tickStep   : settings.tickStep || 5,
        step       : typeof settings.step == 'number' ? settings.step : 1,
        scale      : 100,
        min        : settings.min || 0,
        max        : settings.max || 100,
    }
    
    this.delta = this.settings.max - this.settings.min;
    
    // if "ticks" count was defined, re-calculate the "tickStep"
    if( settings.ticks )
        this.settings.tickStep = this.delta / settings.ticks;

    // a list of ranges (ex. [5,20])
    this.ranges = settings.ranges || [
        this.settings.min + this.settings.tickStep, 
        this.settings.max - this.settings.tickStep
    ]

    this.id = Math.random().toString(36).substr(2,9), // almost-random ID (because, fuck it)
    this.DOM = {}; // Store all relevant DOM elements in an Object
    extend(this, new this.EventDispatcher());
    this.build(placeholderElm);
    this.events.binding.call(this);
}

MultiRange.prototype = {
    build : function( placeholderElm ){
        var that = this,
            scopeClasses = placeholderElm.className.indexOf('multiRange') == -1 ? 
                            'multiRange ' + placeholderElm.className : 
                            placeholderElm.className; 

        this.DOM.scope = document.createElement('div');
        this.DOM.scope.className = scopeClasses;
      
        this.DOM.rangeWrap = document.createElement('div');
        this.DOM.rangeWrap.className = 'multiRange__rangeWrap';
        this.DOM.rangeWrap.innerHTML = this.getRangesHTML();

        this.DOM.ticks = document.createElement('div');
        this.DOM.ticks.className = 'multiRange__ticks';
        this.DOM.ticks.innerHTML = this.generateTicks();

        // append to Scope
        this.DOM.scope.appendChild(this.DOM.rangeWrap);
        this.DOM.scope.appendChild(this.DOM.ticks);

        // replace the placeholder component element with the real one
        placeholderElm.parentNode.replaceChild(this.DOM.scope, placeholderElm);
    },

    generateTicks(){
        var steps = (this.delta) / this.settings.tickStep,
            HTML = '',
            value, 
            i;

        for( i = 0; i <= steps; i++ ){
            value =(+this.settings.min) + this.settings.tickStep * i; // calculate tick value
            value = value.toFixed(1).replace('.0', ''); // cleaup
            HTML += '<div data-value="'+ value +'"></div>';
        }
        
        return HTML;
    },
    
    getRangesHTML(){
        var that = this,
            rangesHTML = '',
            ranges;
        
        this.ranges.unshift(0)
      //  if( this.ranges[0] > this.settings.min )
      //      this.ranges.unshift(this.settings.min)
        if( this.ranges[this.ranges.length - 1] < this.settings.max )
            this.ranges.push(this.settings.max);
        
        ranges = this.ranges;
      
        ranges.forEach(function(range, i){
            if( i == ranges.length - 1 ) return; // skip last ltem
            
            var leftPos = (range - that.settings.min) / (that.delta) * 100;
            
            // protection..
            if( leftPos < 0 )
                leftPos = 0;

           // range =  ranges[i+1] - range;
            rangesHTML += '<div data-idx="'+i+'" class="multiRange__range" \
                style="left:'+ leftPos +'%">\
                <div class="multiRange__handle">\
                    <div class="multiRange__handle__value">'+ range.toFixed(1).replace('.0', '') +'</div>\
                </div>\
            </div>';              
        })
        
        return rangesHTML;
    },
    
    /**
     * A constructor for exposing events to the outside
     */
    EventDispatcher : function(){
        // Create a DOM EventTarget object
        var target = document.createTextNode('');

        // Pass EventTarget interface calls to DOM EventTarget object
        this.off = target.removeEventListener.bind(target);
        this.on = target.addEventListener.bind(target);
        this.trigger = function(eventName, data){
            if( !eventName ) return;
            var e = new CustomEvent(eventName, {"detail":data});
            target.dispatchEvent(e);
        }
    },

    /**
     * DOM events listeners binding
     */
    events : {
        binding : function(){
            this.DOM.rangeWrap.addEventListener('mousedown', this.events.callbacks.onMouseDown.bind(this))
            //prevent anything from being able to be dragged
            this.DOM.scope.addEventListener("dragstart", function(e){ return false }); 
           // this.eventDispatcher.on('add', this.settings.callbacks.add)
        },
        callbacks : {
            onMouseDown : function(e){
                var target = e.target;
                if( !target ) return;
                
                if( target.className == 'multiRange__handle__value' )
                    target = target.parentNode;
                
                else if( target.className != 'multiRange__handle' )
                    return;
                
                // set some variables (so percentages could be calculated on mousemove)
                var _BCR = this.DOM.scope.getBoundingClientRect();
                this.offsetLeft = _BCR.left;
                this.scopeWidth = _BCR.width;
                this.DOM.currentSlice = target.parentNode;
                
                
                this.DOM.currentSlice.classList.add('grabbed');
                this.DOM.currentSliceValue = this.DOM.currentSlice.querySelector('.multiRange__handle__value');
                
                document.body.classList.add('multiRange-grabbing');
                
                // bind temporary events (save "bind" reference so events could later be removed)
                this.events.onMouseUpFunc = this.events.callbacks.onMouseUp.bind(this);
                this.events.mousemoveFunc = this.events.callbacks.onMouseMove.bind(this);

                window.addEventListener('mouseup', this.events.onMouseUpFunc)
                window.addEventListener('mousemove', this.events.mousemoveFunc)
            },

            onMouseUp : function(e){
                this.DOM.currentSlice.classList.remove('grabbed');
                window.removeEventListener('mousemove', this.events.mousemoveFunc);
                window.removeEventListener('mouseup', this.events.onMouseUpFunc);
                document.body.classList.remove('multiRange-grabbing');
   
                // publish the event
                var value = parseInt( this.DOM.currentSlice.style.left );
                this.trigger('changed', {idx:+this.DOM.currentSlice.dataset.idx, value:value, ranges:this.ranges})
                
                this.DOM.currentSlice = null;
            },

            onMouseMove : function(e){
                if( !this.DOM.currentSlice ){
                    window.removeEventListener('mouseup', this.events.onMouseUpFunc);
                    return;
                }
                
                // do not continue if the mouse was overflowing of the left or the right side of the range
                if(  e.clientX < this.offsetLeft || e.clientX > (this.offsetLeft + this.scopeWidth) )
                    return;
                
                var that = this,
                    value, // the numeric value
                    // minLeftPerc = this.settings.minRange/this.delta*100,
                    // minRightPerc = (this.delta - this.settings.minRange)/this.delta*100,
                    xPosScopeLeft = e.clientX - this.offsetLeft, // the left percentage value
                    leftPrecentage = xPosScopeLeft / this.scopeWidth * 100,
                    prevSliceValue = this.ranges[+this.DOM.currentSlice.dataset.idx - 1],
                    nextSliceValue = this.ranges[+this.DOM.currentSlice.dataset.idx + 1];    
                
                value = this.settings.min + (this.delta/100*leftPrecentage);
                
                if( this.settings.step ){
                   // if( value%this.settings.step > 1 ) return;
                    value = Math.round((value) / this.settings.step ) * this.settings.step
                }
                
                
                // make sure a slice value doesn't go above the next slice value and not below the previous one
                if( value < prevSliceValue + this.settings.minRange )             
                     value = prevSliceValue + this.settings.minRange;
                if( value > nextSliceValue - this.settings.minRange )             
                     value = nextSliceValue - this.settings.minRange;
                
                // define min and max move points 
                if( value < (this.settings.min + this.settings.minRange) )             
                     value = this.settings.min + this.settings.minRange;
                if( value > (this.settings.max - this.settings.minRange) )             
                     value = this.settings.max - this.settings.minRange;
                
                leftPrecentage = (value - this.settings.min) / this.delta * 100;

                // update the DOM
                window.requestAnimationFrame(function(){  
                    if( that.DOM.currentSlice ){
                        that.DOM.currentSlice.style.left = leftPrecentage + '%';
                        that.DOM.currentSliceValue.innerHTML = value.toFixed(1).replace('.0', '');
                    }
                })
                // update "ranged" Array
                this.ranges[this.DOM.currentSlice.dataset.idx] = +value.toFixed(1);  
                
                // publish the event
                this.trigger('change', {idx:+this.DOM.currentSlice.dataset.idx, value:value, ranges:this.ranges})
            }
        }
    }
}
})(this);

//////// USAGE //////
var multiRange1 = new MultiRange(document.querySelectorAll('.multiRange')[0], {
    ranges : [15, 44, 77, 88],
    step   : 0
});

var multiRange2 = new MultiRange(document.querySelectorAll('.multiRange')[1], {
    min      : 150,
    max      : 5000,
    ticks    : 80,
    step     : 10,
    minRange : 50
});

multiRange1.on('changed', onrangeChanged);
multiRange2.on('changed', onrangeChanged);
               
function onrangeChanged(e){
    console.log( e, e.detail )
}
              
            
!
999px

Console