Search code examples
javascriptcssasp.net-core-mvc

How to create custom range sliders with min and max using CSS and JavaScript


I have a requirement to have a range slider with min and max and 2 textboxes to show those selected values in the given design of the range slider. I tried to refer below links. https://jsfiddle.net/b68fp7qd/ . But no css is mentioned in that code snippet. And in this link, https://jqueryui.com/slider/#range there is no tooltip and bit difference in the design and color to change. Can anyone help to achieve this.

enter image description here

I have tried below code

HTML:

<div class="filter-container">
    <div class="filter-section">

        <div class="slider-wrapper">
            <div id="slider" class="store-slider"></div>
        </div>
        <div class="row filter-row">
            <div class="col-6">
                <div class="input-group">
                    <input type="text" class="form-control price-input" id="price_from">
                </div>
            </div>
            <div class="col-6">
                <div class="input-group">
                    <input type="text" class="form-control price-input" id="price_to">
                </div>
            </div>
        </div>
    </div>
</div>

CSS:

.filter-container {
max-width: 280px;
margin: 50px auto;
background: #fff;
border-radius: 6px;
}

.filter-section {
    padding: 20px;
}

.filter-section + .filter-section {
    border-top: 1px solid #f1f3f4;
}

.slider-wrapper {
margin: 60px auto 0;
padding: 0 8px;
}

.store-slider.ui-slider {
display: flex;
align-items: center;
justify-content: center;
margin-top: 30px;
}

.store-slider .ui-slider-range {
display: flex;
align-items: center;
justify-content: center;
background: rgb(233, 236, 239);
}

.store-slider .ui-slider-handle {
top: auto;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
margin: 0;
margin-left: -10px;
border-radius: 50%;
border: none;
background: #fff;
box-shadow: inset 0 0 0 transparent, 0 0 2px rgba(103, 118, 125, 1), 0 4px 10px rgba(91, 111, 144, 0.3);
cursor: pointer;
}

.store-slider .ui-slider-handle:hover,
.store-slider .ui-slider-handle.ui-state-hover {
    background: #89cd20;
    box-shadow: inset 0 0 0 1px #75b316, 0 0 2px rgba(103, 118, 125, 0.5), 0 4px 10px rgba(91, 111, 144, 0.3);
}

.store-slider.ui-widget.ui-widget-content {
border: 1px solid rgb(206, 212, 218);
}

.ui-slider-handle:hover,
.ui-slider-handle:active,
.ui-slider-handle:focus {
outline: none;
}

.ui-slider-handle:active {
background: #89cd20;
border-color: #75b316;
}

.tippy-content {
font-size: 13px;
}

.row.filter-row {
margin-top: 15px;
margin-left: -7px;
margin-right: -8px;
}

.row.filter-row [class*="col-"] {
    padding-left: 7px;
    padding-right: 8px;
}

JS:

console.clear();

var currentValue = {
from: 1106,
to: 10000
}

var slider = $('#slider').slider({
range: true,
min: 0,
max: 11850,
values: [currentValue.from, currentValue.to],
create: function (event, ui) {
    var $slider = $(this);
    var $sliderWrapper = $slider.closest('.store-slider-wrapper');
    var $sliderHandlers = $slider.find('.ui-slider-handle');
    var $sliderHandlersMin = $sliderHandlers.eq(0);
    var $sliderHandlersMax = $sliderHandlers.eq(1);
    var $sliderRange = $slider.find('.ui-slider-range');
    var values = {
        from: currentValue.from,
        to: currentValue.to
    }

    $sliderHandlersMin.attr('title', values.from);
    $sliderHandlersMax.attr('title', values.to);
    $sliderRange
        .attr('title', values.from + ' - ' + values.to)
        .attr('data-tippy-distance', 15);;

    var tippyOptions = {
        trigger: 'manual',
        sticky: true,
        dynamicTitle: true,
        hideOnClick: false,
        arrow: true,
        arrowType: 'round',
        animation: 'fade',
        placement: 'top',
        livePlacement: false,
        flipBehavior: [],
        createPopperInstanceOnInit: true,
        appendTo: $(this).closest('.filter-section')[0],
        popperOptions: {
            modifiers: {
                // preventOverflow: {
                //     boundariesElement: $(this).closest('.filter-section')[0],
                // }
                computeStyle: {
                    gpuAcceleration: true
                }
            },
        }
    };

    tippy($sliderHandlersMin[0], tippyOptions);
    tippy($sliderHandlersMax[0], tippyOptions);
    tippy($sliderRange[0], tippyOptions);

    updateSliderTooltip($slider, values);
},
slide: function (event, ui) {
    var $slider = $(this);
    var $sliderWrapper = $slider.closest('.store-slider-wrapper');
    var values = {
        from: ui.values[0],
        to: ui.values[1]
    }

    updateSliderTooltip($slider, values, true);
}
});

function updateSliderTooltip($slider, values, updateInput) {
if (typeof updateInput === "undefined")
    var updateInput = true;

var $sliderWrapper = $slider.closest('.store-slider-wrapper');
var $sliderHandlers = $slider.find('.ui-slider-handle');
var $sliderHandlersMin = $sliderHandlers.eq(0);
var $sliderHandlersMax = $sliderHandlers.eq(1);
var $sliderRange = $slider.find('.ui-slider-range');

$sliderHandlersMin.attr('title', values.from);
$sliderHandlersMax.attr('title', values.to);
$sliderRange.attr('title', values.from + '  ' + values.to);

if (updateInput) {
    $('#price_from').val(values.from);
    $('#price_to').val(values.to);
}

setInterval(function () {
    var tooltipMinLeft = getCoords($sliderHandlersMin[0]).left;
    var tooltipMaxLeft = getCoords($sliderHandlersMax[0]).left;

    var tooltipRange = tooltipMaxLeft - tooltipMinLeft;
    var singleTooltip = tooltipRange < 80;

    if (!singleTooltip) {
        if (!$sliderHandlersMin[0]._tippy.state.visible)
            $sliderHandlersMin[0]._tippy.show(0);
        if (!$sliderHandlersMax[0]._tippy.state.visible)
            $sliderHandlersMax[0]._tippy.show(0);
        if ($sliderRange[0]._tippy.state.visible)
            $sliderRange[0]._tippy.hide(0);
    } else {
        if ($sliderHandlersMin[0]._tippy.state.visible)
            $sliderHandlersMin[0]._tippy.hide(0);
        if ($sliderHandlersMax[0]._tippy.state.visible)
            $sliderHandlersMax[0]._tippy.hide(0);
        if (!$sliderRange[0]._tippy.state.visible)
            $sliderRange[0]._tippy.show(0);
    }
}, 1);
}

function updateValues() {
var from = $('#price_from').val();
var to = $('#price_to').val();

updateSliderTooltip(slider, {
    from: from,
    to: to
}, false);

slider.slider({
    values: [from, to]
});
}

$('#price_from').on('blur', function () {
$(this).val($(this).val().replace(/[^0-9]/i, ''));

if (+$(this).val() > +$('#price_to').val()) {
    $(this).val($('#price_to').val())
}

updateValues();
});

$('#price_to').on('blur', function () {
$(this).val($(this).val().replace(/[^0-9]/gi, ''));

if (+$(this).val() < +$('#price_from').val()) {
    $(this).val($('#price_from').val())
}

updateValues();
});

function getCoords(elem) {
var box = elem.getBoundingClientRect();

var body = document.body;
var docEl = document.documentElement;

var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;

var clientTop = docEl.clientTop || body.clientTop || 0;
var clientLeft = docEl.clientLeft || body.clientLeft || 0;

var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;

return { top: Math.round(top), left: Math.round(left) };
}

But with this I'm unable to see the slider. Design is coming as per the below image. Also the code for the tooltip is breaking and getting below console error:

VM391:620 Uncaught TypeError: Cannot read properties of undefined (reading 'getBoundingClientRect') at getCoords (:620:20) at :561:30

enter image description here


Solution

  • I just implement it in my sample project with no error. You can modify it according your needs.

    1. Create a Slider.cshtml and define it in HomeController.

    enter image description here

    Slider.cshtml

    @{
        ViewData["Title"] = "jQuery UI range slider with tooltip tippy.js";
    }
    <style>
        * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-size: 14px;
      background: #f1f3f4;
      height: 10000px
    }
    
    .filter-container {
      max-width: 280px;
      margin: 50px auto;
      background: #fff;
      border-radius: 6px;
    }
    
    .filter-section {
        padding: 20px;
    }
    
    .filter-section + .filter-section {
      border-top: 1px solid #f1f3f4;
    }
    
    .slider-wrapper {
      margin: 60px auto 0;
      padding: 0 8px;
    }
    
    .store-slider.ui-slider {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-top: 30px;
    }
    
    .store-slider .ui-slider-range {
        display: flex;
        align-items: center;
        justify-content: center;
        background: rgb(233, 236, 239);
    }
    
    .store-slider .ui-slider-handle {
        top: auto;
        width: 20px;
        height: 20px;
        display: flex;
        justify-content: center;
        margin: 0;
        margin-left: -10px;
        border-radius: 50%;
        border: none;
        background: #fff;
        box-shadow: 
            inset 0 0 0 transparent,
            0 0 2px rgba(103, 118, 125, 1), 
            0 4px 10px rgba(91, 111, 144, 0.3);
        cursor: pointer;
    }
    
    .store-slider .ui-slider-handle:hover,
    .store-slider .ui-slider-handle.ui-state-hover {
        background: #89cd20;
        box-shadow: 
            inset 0 0 0 1px #75b316,
            0 0 2px rgba(103, 118, 125, 0.5), 
            0 4px 10px rgba(91, 111, 144, 0.3);
    }
    
    .store-slider.ui-widget.ui-widget-content {
        border: 1px solid rgb(206, 212, 218);
    }
    
    .ui-slider-handle:hover,
    .ui-slider-handle:active,
    .ui-slider-handle:focus {
        outline: none;
    }
    
    .ui-slider-handle:active {
        background: #89cd20;
        border-color: #75b316;
    }
    
    .tippy-content {
      font-size: 13px;
    }
    
    .row.filter-row {
      margin-top: 15px;
      margin-left: -7px;
      margin-right: -8px;
    }
    
    .row.filter-row [class*="col-"] {
      padding-left: 7px;
      padding-right: 8px;
    }
    </style>
    
    
    <div class="filter-container">
        <div class="filter-section">
            <div class="h5">По цене</div>
            <div class="slider-wrapper">
                <div id="slider" class="store-slider"></div>
            </div>
            <div class="row filter-row">
                <div class="col-6">
                    <div class="input-group">
                        <div class="input-group-prepend">
                            <div class="input-group-text">от</div>
                        </div>
                        <input type="text" class="form-control price-input" id="price_from">
                    </div>
                </div>
                <div class="col-6">
                    <div class="input-group">
                        <div class="input-group-prepend">
                            <div class="input-group-text">до</div>
                        </div>
                        <input type="text" class="form-control price-input" id="price_to">
                    </div>
                </div>
            </div>
        </div>
        <div class="filter-section">
            <div class="h5">Мы ценим Вас</div>
            Главное преймущество нашего магазина - это качество товаров и то как мы относимся к нашим клиентам. Если Вы остались не довольны работой наших сотрудников или возникли проблемы с качеством товара, пожалуйста, сходите лесом.
        </div>
    
    </div>
    
    @section Scripts {
        <script src="https://unpkg.com/tippy.js@2.3.0/dist/tippy.all.min.js"></script>
        <script>
            console.clear();
    
            var currentValue = {
                from: 1106,
                to: 10000
            }
    
            var slider = $('#slider').slider({
                range: true,
                min: 0,
                max: 11850,
                values: [currentValue.from, currentValue.to],
                create: function (event, ui) {
                    var $slider = $(this);
                    var $sliderWrapper = $slider.closest('.store-slider-wrapper');
                    var $sliderHandlers = $slider.find('.ui-slider-handle');
                    var $sliderHandlersMin = $sliderHandlers.eq(0);
                    var $sliderHandlersMax = $sliderHandlers.eq(1);
                    var $sliderRange = $slider.find('.ui-slider-range');
                    var values = {
                        from: currentValue.from,
                        to: currentValue.to
                    }
    
                    $sliderHandlersMin.attr('title', 'от ' + values.from + ' грн');
                    $sliderHandlersMax.attr('title', 'до ' + values.to + ' грн');
                    $sliderRange
                        .attr('title', values.from + ' - ' + values.to + ' грн')
                        .attr('data-tippy-distance', 15);;
    
                    var tippyOptions = {
                        trigger: 'manual',
                        sticky: true,
                        dynamicTitle: true,
                        hideOnClick: false,
                        arrow: true,
                        arrowType: 'round',
                        animation: 'fade',
                        placement: 'top',
                        livePlacement: false,
                        flipBehavior: [],
                        createPopperInstanceOnInit: true,
                        appendTo: $(this).closest('.filter-section')[0],
                        popperOptions: {
                            modifiers: {
                                // preventOverflow: {
                                //     boundariesElement: $(this).closest('.filter-section')[0],
                                // }
                                computeStyle: {
                                    gpuAcceleration: true
                                }
                            },
                        }
                    };
    
                    tippy($sliderHandlersMin[0], tippyOptions);
                    tippy($sliderHandlersMax[0], tippyOptions);
                    tippy($sliderRange[0], tippyOptions);
    
                    updateSliderTooltip($slider, values);
                },
                slide: function (event, ui) {
                    var $slider = $(this);
                    var $sliderWrapper = $slider.closest('.store-slider-wrapper');
                    var values = {
                        from: ui.values[0],
                        to: ui.values[1]
                    }
    
                    updateSliderTooltip($slider, values, true);
                }
            });
    
            function updateSliderTooltip($slider, values, updateInput) {
                if (typeof updateInput === "undefined")
                    var updateInput = true;
    
                var $sliderWrapper = $slider.closest('.store-slider-wrapper');
                var $sliderHandlers = $slider.find('.ui-slider-handle');
                var $sliderHandlersMin = $sliderHandlers.eq(0);
                var $sliderHandlersMax = $sliderHandlers.eq(1);
                var $sliderRange = $slider.find('.ui-slider-range');
    
                $sliderHandlersMin.attr('title', 'от ' + values.from + ' грн');
                $sliderHandlersMax.attr('title', 'до ' + values.to + ' грн');
                $sliderRange.attr('title', 'от ' + values.from + ' и до ' + values.to + ' грн');
    
                if (updateInput) {
                    $('#price_from').val(values.from);
                    $('#price_to').val(values.to);
                }
    
                setInterval(function () {
                    var tooltipMinLeft = getCoords($sliderHandlersMin[0]).left;
                    var tooltipMaxLeft = getCoords($sliderHandlersMax[0]).left;
    
                    var tooltipRange = tooltipMaxLeft - tooltipMinLeft;
                    var singleTooltip = tooltipRange < 80;
    
                    if (!singleTooltip) {
                        if (!$sliderHandlersMin[0]._tippy.state.visible)
                            $sliderHandlersMin[0]._tippy.show(0);
                        if (!$sliderHandlersMax[0]._tippy.state.visible)
                            $sliderHandlersMax[0]._tippy.show(0);
                        if ($sliderRange[0]._tippy.state.visible)
                            $sliderRange[0]._tippy.hide(0);
                    } else {
                        if ($sliderHandlersMin[0]._tippy.state.visible)
                            $sliderHandlersMin[0]._tippy.hide(0);
                        if ($sliderHandlersMax[0]._tippy.state.visible)
                            $sliderHandlersMax[0]._tippy.hide(0);
                        if (!$sliderRange[0]._tippy.state.visible)
                            $sliderRange[0]._tippy.show(0);
                    }
                }, 1);
            }
    
            function updateValues() {
                var from = $('#price_from').val();
                var to = $('#price_to').val();
    
                updateSliderTooltip(slider, {
                    from: from,
                    to: to
                }, false);
    
                slider.slider({
                    values: [from, to]
                });
            }
    
            $('#price_from').on('blur', function () {
                $(this).val($(this).val().replace(/[^0-9]/i, ''));
    
                if (+$(this).val() > +$('#price_to').val()) {
                    $(this).val($('#price_to').val())
                }
    
                updateValues();
            });
    
            $('#price_to').on('blur', function () {
                $(this).val($(this).val().replace(/[^0-9]/gi, ''));
    
                if (+$(this).val() < +$('#price_from').val()) {
                    $(this).val($('#price_from').val())
                }
    
                updateValues();
            });
    
            function getCoords(elem) {
                var box = elem.getBoundingClientRect();
    
                var body = document.body;
                var docEl = document.documentElement;
    
                var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
                var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
    
                var clientTop = docEl.clientTop || body.clientTop || 0;
                var clientLeft = docEl.clientLeft || body.clientLeft || 0;
    
                var top = box.top + scrollTop - clientTop;
                var left = box.left + scrollLeft - clientLeft;
    
                return { top: Math.round(top), left: Math.round(left) };
            }
    
        </script>
    }

    2. Add jquery and jquery-ui in _Layout.cshtml.

    enter image description here

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>@ViewData["Title"] - signalr_iwa</title>
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
        <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
        <link rel="stylesheet" href="~/signalr_iwa.styles.css" asp-append-version="true" />
        @* add this line *@
        <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" asp-append-version="true" />
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container-fluid">
                    <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">signalr_iwa</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                            aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                        <ul class="navbar-nav flex-grow-1">
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                            </li>
                        </ul>
                        <p class="nav navbar-text">Hello, @User.Identity?.Name!</p>
                    </div>
                </div>
            </nav>
        </header>
        <div class="container">
            <main role="main" class="pb-3">
                @RenderBody()
            </main>
        </div>
    
        <footer class="border-top footer text-muted">
            <div class="container">
                &copy; 2023 - signalr_iwa - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
            </div>
        </footer>
        <script src="~/lib/jquery/dist/jquery.min.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
        @* add this line *@
        <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
        @* add this line *@
        <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
        @await RenderSectionAsync("Scripts", required: false)
    </body>
    </html>

    3. Then we can test it

    enter image description here